Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/system-maintenance.mjs
T
Vito bdc38422a9 feat(system): Diagnostics & logs natively, reviewed log cleanup and Clear all logs (#329, PR 2) (#385)
* fix(logs): declare the compressed length so a log download finishes

The gzip middleware compressed a text body but kept the handler's declared
Content-Length (the uncompressed size), so a browser waited for bytes that
never came: downloading any log over 1 KB hung, in the workspace and on the
admin page. The middleware now drops a declared length before compressing.

Part of #329

* feat(system): show Diagnostics & logs natively and retire the hand-off panel

System › Diagnostics & logs is native (D109, D110): the concept's
Diagnostics surface with the live database, source, Viewer.js and renderer
lines, Run checks (the shared Health report), the diagnostic report, Inspect
and the confirmed test link; and the Logs surface with the minimum-level
viewer, Lines 1-500, statistics, the log download, the runtime level and
console setting saved by Save preferences, the log files with their
downloads, Review log cleanup (the rotated_logs maintenance plan, no backup
box, Listed files) and a confirmed Clear all logs.

D94's hand-off panel, SECTION_HANDOFFS and the undelivered runtime adapter
are gone. The browser server logs into its own temporary root.

Part of #329

* fix(system): confirm Clear all logs on a fresh file list and record coverage

Review fixes for the Diagnostics & logs section:
- Clear all logs reads the log files again before it confirms, so the
  dialog names the copies it deletes now (a list it cannot read gives the
  general sentence, never an old count); Refresh reads the files too.
- Runtime values that cannot be read stay disabled and say so.
- Create test link holds its button until the write answered, so a second
  press cannot write a second record.
- The Lines field names its range.
- The browser server builds its logger in its own root, so no log file is
  ever opened in another folder; the fixture closes every directory scan
  and replaces a link instead of writing through it.
- system.mjs drops the unused section fallback; frontend coverage floors
  recorded with coverage:update (system-diagnostics.mjs 97.5% lines).

Part of #329

* fix(system): read the database size the report sends

The Database line read database_info.size_bytes, a key the report does
not have (get_database_info answers database_size_bytes), so it always
read "Readable · 0 bytes · N images". The browser test now requires a
non-zero size. Found by PR 3's concept comparison.

Part of #329
2026-10-27 18:20:05 +00:00

170 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* System › Maintenance (#249 System S7 Task 19): the concept's `renderSystem` "maintenance" body (app.js 688–690).
*
* - The notice "Check first. Review changes. Then apply." with "Run health checks" (D123), then the surfaces
* "Database" and "Images & cache" with the concept's eight operations, in its order, labels and danger marks.
* - Integrity & relationship checks → Check runs the same read-only health check and opens the Health report (D95);
* its "Review missing links" reviews a fresh Missing-file links plan, never a cleanup.
* - Duplicate prompt text, Optimize database, Unused prompt components and Missing-file links → Review plan open the
* D100 plan review (maintenance-review.mjs), which applies only what it showed, as an Activity job.
* - Duplicate image files → Find duplicates starts the duplicate search as an Activity job and opens it; its view
* offers Review groups (D101) once it found some.
* - Thumbnail cache → Review options opens the S5 "Thumbnail maintenance" dialog (D99).
* - D102 (Q5): Metadata cache clears only the browser entries Prompt Manager owns as a cache (cache-ownership.js,
* #222). The inventory has none, so the row says "Nothing is cached in this browser." and its Review plan is
* aria-disabled; with owned entries it reviews and clears exactly those.
*/
import '../cache-ownership.js';
import { navigateTo } from './state.mjs';
import { showWorkspaceToast } from './toast.mjs';
import { operationRow } from './system-controls.mjs';
import { healthNotice, runHealthAction } from './system-overview.mjs';
import { openThumbnailReview } from './thumbnail-review.mjs';
import { busyToast } from './activity-scan.mjs';
import { openReviewDialog } from './workspace-dialogs.mjs';
import { reviewPlan } from './maintenance-review.mjs';
import { noun } from './plural.mjs';
export const DUPLICATES_KIND = 'duplicates';
export const FIND_FAILED = 'The duplicate search could not start. Try again.';
export const NOTHING_CACHED_REASON = 'Nothing is cached in this browser.';
export const STORAGE_UNCHECKED_REASON = 'This browser did not allow checking its storage.';
export const METADATA_CACHE_DESCRIPTION = 'Clear only Prompt Manager metadata cached by this browser.';
/** The concept's two surfaces and their rows: `[title, description, label, action, danger]`. */
export const MAINTENANCE_SURFACES = Object.freeze([
['Database', [
['Integrity & relationship checks', 'Validate foreign keys, text hashes, pair uniqueness and image associations.', 'Check', 'health'],
['Duplicate prompt text', 'Review canonical text reuse and dependency-safe merging.', 'Review plan', 'duplicate_prompts'],
['Optimize database', 'Reclaim unused SQLite space. This does not delete prompts or media.', 'Review plan', 'optimize'],
['Unused prompt components', 'Preview unreferenced components. Pair references and history remain protected.', 'Review plan',
'unused_components', true],
]],
['Images & cache', [
['Missing-file links', 'Remove stale database links only. Files on disk are never deleted by this operation.', 'Review plan',
'missing_links', true],
['Duplicate image files', 'Find matching file content, review groups and choose what to keep. Separate from duplicate prompt text.',
'Find duplicates', 'duplicates'],
['Thumbnail cache', 'Generate missing thumbnails or explicitly rebuild all.', 'Review options', 'thumbnails'],
['Metadata cache', METADATA_CACHE_DESCRIPTION, 'Review plan', 'metadata_cache'],
]],
]);
function el(tag, text, className) {
const node = document.createElement(tag);
if (text !== undefined) node.textContent = text;
if (className) node.className = className;
return node;
}
const ownership = () => window.CacheOwnership;
const browserStorage = () => window.localStorage;
const cacheStorage = () => ('caches' in window ? window.caches : null);
/** Start the duplicate search and open its job; a refused or failed start says why and stays. */
export async function findDuplicates({ store, scope, notify = showWorkspaceToast, navigate = navigateTo }) {
try {
const result = await store.start(DUPLICATES_KIND);
if (scope.signal.aborted) return;
if (result.busy !== undefined) {
notify(busyToast(result.busy));
return;
}
navigate(`#/activity/${result.job.id}`);
} catch {
if (!scope.signal.aborted) notify(FIND_FAILED);
}
}
/**
* D102: the Metadata cache row's description: the concept's, plus the reason its review is unavailable. A browser that
* refused to list its storage is never claimed empty.
*/
export function metadataCacheDescription(entries) {
if (entries.keys.length + entries.caches.length > 0) return METADATA_CACHE_DESCRIPTION;
return `${METADATA_CACHE_DESCRIPTION} ${entries.denied ? STORAGE_UNCHECKED_REASON : NOTHING_CACHED_REASON}`;
}
/** D102: review the owned browser entries, then clear exactly those and say what happened. */
export function openMetadataCacheReview({ scope, entries, trigger = null, notify = showWorkspaceToast,
cache = ownership(), onCleared = () => {} }) {
const count = entries.keys.length + entries.caches.length;
openReviewDialog({ scope, trigger, title: 'Review maintenance plan', heading: 'Metadata cache',
grid: [['Scope', `${count.toLocaleString('en-US')} cached ${noun(count, 'entry', 'entries')} in this browser`],
['Protected', 'Preferences and other tools’ data in this browser'],
['Backup', 'Not needed: the database is not changed'],
['Media on disk', 'No files are removed']],
sentence: [...entries.keys, ...entries.caches].join(' · '),
cancelLabel: 'Cancel', primary: { label: 'Clear browser cache' },
onStart: async () => {
const result = await cache.clearOwned({ storage: browserStorage, cacheStorage: cacheStorage() });
if (scope.signal.aborted) return;
notify(cache.clearMessage(result));
onCleared();
} });
}
/** The Metadata cache row: aria-disabled with its reason while nothing is owned (D102). */
function metadataCacheRow({ scope, notify, cache }) {
const row = operationRow({ scope, title: 'Metadata cache', description: METADATA_CACHE_DESCRIPTION, label: 'Review plan',
onClick: async button => {
if (button.getAttribute('aria-disabled') === 'true') return;
const entries = await cache.ownedEntries({ storage: browserStorage, cacheStorage: cacheStorage() });
if (scope.signal.aborted) return;
if (entries.keys.length + entries.caches.length === 0) {
paint(entries);
return;
}
openMetadataCacheReview({ scope, entries, trigger: button, notify, cache, onCleared: refresh });
} });
const button = row.querySelector('.row-actions > button');
const description = row.querySelector('p');
function paint(entries) {
description.textContent = metadataCacheDescription(entries);
if (entries.keys.length + entries.caches.length > 0) button.removeAttribute('aria-disabled');
else button.setAttribute('aria-disabled', 'true');
}
function refresh() {
cache.ownedEntries({ storage: browserStorage, cacheStorage: cacheStorage() }).then(entries => {
if (!scope.signal.aborted) paint(entries);
});
}
paint({ keys: [], caches: [] });
refresh();
return row;
}
/**
* The Health report's "Review missing links" (D95): a fresh Missing-file links plan, never a cleanup, holding the pressed
* button while it is read. Maintenance and Diagnostics & logs (S12) pass it to `runHealthAction`.
*/
export function reviewMissingLinks({ api, store, scope, notify = showWorkspaceToast, navigate = navigateTo }) {
return (trigger, pressed = trigger) => reviewPlan({ api, store, scope, operation: 'missing_links', trigger, hold: pressed,
notify, navigate });
}
/**
* The Maintenance section's mount (`mountSystem`'s `sections.maintenance`): the notice and the two surfaces. `activity`
* is the page's Activity store.
*/
export function mountMaintenance(container, { api, activity, scope, navigate = navigateTo, notify = showWorkspaceToast,
cache = ownership() }) {
const reviewMissing = reviewMissingLinks({ api, store: activity, scope, notify, navigate });
const actions = {
health: button => runHealthAction({ api, activity, scope, button, onReviewMissing: reviewMissing, notify }),
duplicates: () => findDuplicates({ store: activity, scope, notify, navigate }),
thumbnails: button => openThumbnailReview({ api, store: activity, scope, trigger: button, navigate }),
};
const surfaces = MAINTENANCE_SURFACES.map(([heading, rows]) => {
const surface = el('section', undefined, 'surface');
surface.append(el('h2', heading), ...rows.map(([title, description, label, action, danger = false]) => {
if (action === 'metadata_cache') return metadataCacheRow({ scope, notify, cache });
const onClick = actions[action]
?? (button => reviewPlan({ api, store: activity, scope, operation: action, trigger: button, notify, navigate }));
return operationRow({ scope, title, description, label, danger, onClick });
}));
return surface;
});
return [healthNotice({ api, activity, scope, navigate, notify, onReviewMissing: reviewMissing }), ...surfaces];
}