* feat(system): read-only health check and truthful overview counts POST /prompt_manager/system/health runs the health check off-loop as a read registry job of kind "health" (one at a time, beside a scan, not cancellable, no file outcomes) and keeps the last report; GET returns it or null. The check reads one snapshot on its own mode=ro connection, closed in finally: integrity_check(100), foreign_key_check, stored hashes against utils.hashing, duplicate pair rows, the legacy consistency queries (now shared with check_consistency), and image links whose file is gone, counted only under readable sources. An unavailable source, or the absent Needs-review queue, makes its check unavailable and the report incomplete instead of producing a count. GET /prompt_manager/system/overview returns Library-eligible positive prompts and active negative variants, indexed images, the last health summary or null, backup null, the Needs-review count null, the effective sources with whether they were auto-detected, and the thumbnail plan's missing count. GET /prompt_manager/system/statistics lists runs and average rating per category of Library parents, with the /stats totals. * feat(system): native System route, drafts and shared review dialog - #/system/<section> renders the concept's System shell: section nav, settings layout and Save row with truthful status (D92); each section not delivered yet hands off to its legacy tools (D94). Overview's System card opens #/system/overview (Q12). - A module-level draft store saves browser, then host (expected_revision), then runtime; failed domains stay dirty, a stale revision is a conflict. - System row builders (setting, operation, section nav) on the scope. - openReviewDialog and backupNotice extracted from the scan review, which is now a thin caller with identical markup. - Overview images gain a "rated" count (images whose linked prompt is rated); the missing-files health check reports per source, so partial availability is visible. * feat(system): native Overview with health report and statistics System > Overview renders the concept's metrics and its Library health and Workspace preferences surfaces from GET /system/overview: real counts, "Not checked" until a health check ran, no invented review count, backup or thumbnail status (D93). Explore opens "Usage statistics" with the real runs and ratings per category and the Library totals (D96); the statistics route now also returns rated_images. Maintenance shows the concept's notice with Run health checks above its hand-off: the read-only check runs as a registry read job and opens the "Health report" with a chip per real result, missing files per source (D95). A load failure shows Retry and no zeros; late answers never repaint a newer section. Adds the System concept comparison (shell, Overview, both dialogs, the notice, mutation checks), the System workflow spec and its fixture hooks, and the TESTING.md "Native System" section. * test(system): coverage floors for the native Overview Adds the system-overview.mjs baseline and raises the system.mjs floors to their measured coverage; no other floor changes. * fix(system): keep health reports per database and off the event loop - A restore that replaces the database forgets the stored health report, and a check that started before the swap is not kept (database generation stamped at start). - The Overview no longer walks every source for the missing-thumbnail count; the page reads /images/thumbnails/status on its own. - Image sources for the health check and the Overview resolve in the executor; a failure to start the check answers the coded envelope. - Any exception fails only its own check; missing files reads its rows in the snapshot and stats the files after it ended. - The read-only URI keeps Windows UNC paths (empty authority). - An integrity count at SQLite's limit is flagged as capped. * fix(system): thumbnail row read, tracked health job and guarded host save - The Overview's thumbnail row is its own read of the thumbnail status: "Checking thumbnails…" until it answers, unavailable when it fails. - The health job is tracked in the Activity store (new store.track) and followed through its event stream instead of polling every 150 ms. - A host save before a read returned a revision fails without a request; System reads the host values on mount and after a conflict. - "100+ problems found" when the integrity count is capped. - D-NEW-S2-5 is D123 everywhere; the unused Overview hand-off is gone. - The concept spec builds the health report from the concept's own rows and asserts the full unmapped path list; surface tops pin the 18px offset. - TESTING.md: D96's reason, the read-only (WAL/SHM) standard, the UNC URI and the new behavior. Coverage floors raised for the touched files. * fix(system): make two health tests portable to Windows - Windows forbids "?" in file names, so the URI-character open test uses "#", "%" and a space there. - ntpath.realpath on Python 3.11+ returns a NUL-byte path instead of raising, so the bad-path test raises as POSIX does to reach the missing-files failure path on every platform.
149 lines
7.1 KiB
JavaScript
149 lines
7.1 KiB
JavaScript
/**
|
||
* Native System (#249 System S2 Task 5): the concept's `renderSystem` shell (app.js 698–706) at `#/system/<section>`.
|
||
* Under the page heading it renders the section nav (`.section-nav`, one button per section, the shown one
|
||
* `aria-pressed`; a press navigates to its hash), then `.settings-layout` with the section's body and the Save row
|
||
* (`span.small#settings-status` + "Save preferences"). D92: no "Sample configuration" chip, the status copy is
|
||
* truthful and "Save preferences" is aria-disabled while nothing is unsaved. A section not delivered yet shows the
|
||
* per-section hand-off to its legacy tool (D94), never mock placeholders. Overview is native (system-overview.mjs).
|
||
*
|
||
* Switching sections keeps the shell (and focus on the pressed button) and disposes the old section's scope, so its
|
||
* listeners and timers never outlive it. The drafts are module-level (Q10): they live for the page session.
|
||
*/
|
||
import { createButton, createScope } from '../shared/toolkit/core.js';
|
||
import { SYSTEM_SECTIONS, navigateTo } from './state.mjs';
|
||
import { createSystemDrafts, DRAFT_DOMAINS } from './system-state.mjs';
|
||
import { sectionNav } from './system-controls.mjs';
|
||
import { healthNotice, mountOverview } from './system-overview.mjs';
|
||
|
||
const SETTINGS_URL = '/prompt_manager/settings';
|
||
const ADMIN = Object.freeze({ href: '/prompt_manager/admin', label: 'Open existing administration' });
|
||
const GALLERY = Object.freeze({ href: '/prompt_manager/gallery', label: 'Open existing image gallery' });
|
||
/**
|
||
* D94: where each section's capabilities live until its PR (the plan's legacy capability coverage): the admin page's
|
||
* dashboard, Settings, Maintenance, Backup, Restore, Scan, Logs, Diagnostics and Auto-tag tools, and the gallery's
|
||
* settings, thumbnails, duplicates and cache tools.
|
||
*/
|
||
export const SECTION_HANDOFFS = Object.freeze({
|
||
appearance: [GALLERY, ADMIN],
|
||
sources: [ADMIN, GALLERY],
|
||
integrations: [ADMIN],
|
||
tagging: [ADMIN],
|
||
maintenance: [ADMIN, GALLERY],
|
||
backups: [ADMIN],
|
||
diagnostics: [ADMIN],
|
||
});
|
||
export const NOT_DELIVERED = 'This setting is not available here yet.';
|
||
|
||
let drafts = null;
|
||
|
||
function el(tag, text, className) {
|
||
const node = document.createElement(tag);
|
||
if (text !== undefined) node.textContent = text;
|
||
if (className) node.className = className;
|
||
return node;
|
||
}
|
||
|
||
/** A domain whose settings no delivered section edits yet: nothing can be drafted, and a write would say it failed. */
|
||
const undelivered = async () => { throw new Error(NOT_DELIVERED); };
|
||
|
||
/** The page session's drafts (Q10); the host domain reads and saves the server settings with their revision (#220). */
|
||
export function systemDrafts(api) {
|
||
drafts ??= createSystemDrafts({
|
||
readBrowser: () => ({}),
|
||
readRuntime: () => ({}),
|
||
writeBrowser: undelivered,
|
||
writeRuntime: undelivered,
|
||
async readHost() {
|
||
const result = await api.get(SETTINGS_URL, { cache: 'no-store' });
|
||
// The store reads a missing value as none and a missing revision as unreadable (no host save).
|
||
return { values: result.settings, revision: result.revision };
|
||
},
|
||
writeHost: (changes, { expected_revision }) => api.post(SETTINGS_URL, { ...changes, expected_revision }),
|
||
});
|
||
return drafts;
|
||
}
|
||
|
||
/** D94: the shared hand-off panel, linking each legacy tool that still owns this section. */
|
||
function handoff(section) {
|
||
const panel = el('section', undefined, 'surface handoff');
|
||
panel.append(el('h2', 'Your current tools are ready'),
|
||
el('p', 'This workspace is being introduced in stages. Continue in the existing tool with your saved data.'));
|
||
for (const tool of SECTION_HANDOFFS[section]) {
|
||
const link = el('a', tool.label, 'workspace-link');
|
||
link.href = window.ApiClient.resolveUrl(tool.href);
|
||
panel.append(link);
|
||
}
|
||
return [panel];
|
||
}
|
||
|
||
/** D92: highlight the controls of the settings the last save could not save. */
|
||
function markFailed(layout, store) {
|
||
const failed = new Set(DRAFT_DOMAINS.flatMap(domain => [...store.failed(domain)]));
|
||
for (const control of layout.querySelectorAll('[data-setting]')) {
|
||
if (failed.has(control.dataset.setting)) control.setAttribute('aria-invalid', 'true');
|
||
else control.removeAttribute('aria-invalid');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The sections delivered so far: Overview (S2 Task 6), and Maintenance's notice with "Run health checks" above the
|
||
* hand-off to the maintenance tools that are not native yet (D123).
|
||
*/
|
||
export const DELIVERED_SECTIONS = Object.freeze({
|
||
overview: mountOverview,
|
||
maintenance: (container, { api, activity, scope }) => [healthNotice({ api, activity, scope }), ...handoff('maintenance')],
|
||
});
|
||
|
||
/**
|
||
* Mount System under the heading `renderHeading` wrote in `main`. Returns `{show(section)}`, which shows another
|
||
* section in place. `scope` is the page's view scope; `sections` maps a section id to a delivered section's mount
|
||
* (`(container, {api, activity, scope, drafts}) => nodes`), and every other section hands off. `activity` is the page's
|
||
* Activity store, which follows the jobs a section starts.
|
||
*
|
||
* The host values are read on mount (and by the store again after a conflict), so a save carries the current revision;
|
||
* until a read returned one, a host save fails without a request (system-state.mjs).
|
||
*/
|
||
export function mountSystem(main, api, scope, { section, activity, sections = DELIVERED_SECTIONS, navigate = navigateTo }) {
|
||
const store = systemDrafts(api);
|
||
store.reloadHost();
|
||
const nav = sectionNav({ label: 'System sections', sections: SYSTEM_SECTIONS, active: section, scope,
|
||
onSelect: id => navigate(`#/system/${id}`) });
|
||
const layout = el('div', undefined, 'settings-layout');
|
||
const saveRow = el('div', undefined, 'save-row');
|
||
const status = el('span', undefined, 'small');
|
||
status.id = 'settings-status';
|
||
const save = createButton('Save preferences', 'ca-primary');
|
||
saveRow.append(status, save);
|
||
layout.append(saveRow);
|
||
main.append(nav, layout);
|
||
|
||
const paintStatus = () => {
|
||
status.textContent = store.statusText();
|
||
if (store.dirty() && !store.saving()) save.removeAttribute('aria-disabled');
|
||
else save.setAttribute('aria-disabled', 'true');
|
||
markFailed(layout, store);
|
||
};
|
||
scope.cleanup(store.subscribe(paintStatus));
|
||
scope.listen(save, 'click', () => {
|
||
if (save.getAttribute('aria-disabled') === 'true') return;
|
||
const saving = store.save();
|
||
paintStatus();
|
||
saving.then(() => { if (!scope.signal.aborted) paintStatus(); });
|
||
});
|
||
|
||
let sectionScope = null;
|
||
scope.cleanup(() => sectionScope?.dispose());
|
||
function show(next) {
|
||
sectionScope?.dispose();
|
||
sectionScope = createScope();
|
||
for (const button of nav.children) button.setAttribute('aria-pressed', String(button.dataset.value === next));
|
||
while (layout.firstChild !== saveRow) layout.firstChild.remove();
|
||
const mount = sections[next];
|
||
const nodes = mount ? mount(layout, { api, activity, scope: sectionScope, drafts: store }) : handoff(next);
|
||
layout.prepend(...nodes);
|
||
paintStatus();
|
||
}
|
||
show(section);
|
||
return { show };
|
||
}
|