feat(workspace): stop linking the old pages (#620)

They show a removal notice since 3.5.1. System no longer links them, the load
failure offers a reload and ComfyUI instead, and the README says they are going
away. The pages still work at their URLs.
This commit is contained in:
Vito Sansevero
2026-11-02 10:28:42 -08:00
parent 6b4f2206a5
commit c4cc4f30a7
10 changed files with 28 additions and 93 deletions
+4 -4
View File
@@ -12,10 +12,10 @@ Prompt Manager saves your prompts as you generate, links each image back to the
## What's new: rebuilt from the ground up
- **A new UI.** One app, the workspace, replaces the separate dashboard, gallery and metadata pages. The old pages still work if you want them.
- **A new UI.** One app, the workspace, replaces the separate dashboard, gallery and metadata pages.
- **Classic and Nodes 2.0.** The nodes work in both ComfyUI renderers. CI starts a real ComfyUI and checks both.
- **Tested end to end.** Unit tests, browser tests in Chromium, Firefox, WebKit and touch emulation, and tests against a real ComfyUI.
- **Mac, Linux and Windows.** The Python suite runs on all three, on Python 3.12, 3.13 and 3.14.
- **Linux and Windows.** The Python suite runs on both, on Python 3.12, 3.13 and 3.14. macOS should work but is not tested.
## See it
@@ -212,9 +212,9 @@ Set `PROMPT_MANAGER_DATA_DIR` to an absolute folder to keep the library somewher
- **Older images:** a one-time notice offers a metadata scan that links them to prompts and negatives. Scans don't count as new runs.
- **Going back isn't supported.** Workflows are now stored once, and an older version shows new records without their workflow or prompt graph. Back up first in System > Backup & export. See [the old README's changelog](README-legacy.md#changelog) for the manual way back.
## The old pages still work
## The old pages will be removed
They stay at their URLs, and System's Overview section links to them:
They still work at their URLs for now and will be removed in a future release. Everything they do is moving to the workspace.
| Page | URL |
| --- | --- |
+8 -7
View File
@@ -77,7 +77,7 @@ PM_TEST_WORKERS=8 npm run --prefix tools/vendor test:coverage # whole
npm run --prefix tools/vendor test:coverage:shard -- 1/4 # node units, viewer suite, workflows 1/4
npm run --prefix tools/vendor test:coverage:shard -- 2/4 # workflows 2/4 (likewise 3/4, 4/4)
npm run --prefix tools/vendor coverage:merge # report and baseline check of every raw file
npm run --prefix tools/vendor test:workflows -- --project webkit --shard 2/2
npm run --prefix tools/vendor test:workflows -- --project webkit --shard 2/3
```
`coverage:merge` refuses an empty raw directory; it does not count shards, so run
@@ -193,8 +193,8 @@ waiting for the `frontend coverage` jobs.
The `viewerjs` check runs on every PR, including backend-only changes. It is an
aggregate job. It requires the four `frontend coverage (shard N/4)` jobs and
`frontend coverage (merge)` to succeed on every event. The four
`page workflows (firefox|webkit, shard N/2)` jobs run only in the merge queue,
`frontend coverage (merge)` to succeed on every event. The six
`page workflows (firefox|webkit, shard N/3)` jobs run only in the merge queue,
on `main` and on `workflow_dispatch` (see "Pull requests and the merge queue").
`viewerjs` requires them to be skipped on a pull request and to succeed
everywhere else. Any other failed, cancelled or skipped job fails it. Every browser job runs with `PM_TEST_WORKERS: 3` (the public
@@ -2735,7 +2735,7 @@ the grid) and D43 (sorted chips, checked against the concept with its chips sort
D44 needs no mapping: the comparison runs with no Refine filter active, so the button
reads "Filters" as the concept's does. Nothing else follows the grid: the legacy
"Current prompt library" link is removed from the shelf; the legacy Library stays at
`/prompt_manager/admin` by direct URL, linked only from System (#525, D165)
`/prompt_manager/admin` by direct URL (#525, #620)
(`tests/browser/workspace.spec.mjs`). `library comparison detects
mutations` injects `.card-footer { padding: 2px }` and removes `.card-meta .chip.red`.
@@ -3243,7 +3243,7 @@ npm run --prefix tools/vendor test:workflows -- history-concept.spec.mjs \
`#/inspect` ("Inspect a file") is a native workspace route; it no longer hands off to
the legacy metadata page, which stays at its URL
(`/prompt_manager/gallery.html`) linked only from System (#525, D165). The page is the concept's `#metadata`
(`/prompt_manager/gallery.html`) at its URL only (#525, #620). The page is the concept's `#metadata`
(app.js `renderMetadata` a740–748, selection a1715–1729; style.css 709–722, 1180–1194,
1543–1545, 1559–1564) in the concept's own classes (`.surface`, `.file-drop`,
`.spaced-20`, `.local-file-preview`, all from the synced `concept.css`), with no
@@ -7495,7 +7495,7 @@ Deploying this backend needs a ComfyUI restart (Python and route changes).
`#/tags` ("Tags & categories", page title "Tags") is a native workspace route; it no
longer hands off to the legacy tag manager. That page stays at
`/prompt_manager/admin#/tags` linked only from System (#525, D165); tag search and sort, the untagged
`/prompt_manager/admin#/tags` at its URL only (#525, #620); tag search and sort, the untagged
entry (D149) and multi-tag OR (the Library's Tag match, D150) moved to the workspace
(#444); bulk prompt delete stays on the legacy admin page (bulk tags: the Prompt
library's selection bar, D151). A prompt's category is changed in the Prompt library
@@ -7627,7 +7627,8 @@ The legacy pages (`/prompt_manager/admin`, `admin#/tags`, `gallery`, `gallery.ht
their URLs, and admin's Metadata and Gallery buttons still open their legacy pages (maintainer
ruling; pinned by `legacy-system-links.spec.mjs`).
- **D165** (beyond the concept, maintainer ruling 2026-10-30): System's Overview section ends
- **D165 retired** (#620): the old pages show their removal notice since 3.5.1, so System no longer links them and the load failure below offers a reload and ComfyUI. The original ruling:
**D165** (beyond the concept, maintainer ruling 2026-10-30): System's Overview section ends
with one small line, "Legacy pages: Dashboard · Tags · Gallery · Metadata viewer"
(`.legacy-pages`, new tabs), outside `.settings-layout`. The concept has no such line;
`mapOverviewConcept` adds it to the page tree.
+1 -3
View File
@@ -167,8 +167,6 @@ const IMAGES_COUNT_URL = '/prompt_manager/images/output?mode=workspace&limit=1';
/** The concept's shell and Overview with the recorded deviations applied, given the server's real overview. */
function mapOverviewConcept(reference, overview, images) {
// D165: the discreet legacy-pages line follows the settings layout.
reference = { ...reference, tree: [...reference.tree, 'p.legacy-pages.small'] };
const relabel = { 'Database integrity': 'Not checked', 'Positive prompts': number(overview.library.positive_prompts),
'Negative variants': number(overview.library.negative_variants), Images: number(images) };
const rows = {
@@ -265,7 +263,7 @@ const UNMAPPED_PATHS = [
'columns.surfaces.0.rows.length', 'columns.surfaces.0.tree.4', 'columns.surfaces.0.tree.length',
'columns.surfaces.1.rows.0.description', 'columns.surfaces.1.rows.1.title', 'columns.surfaces.1.rows.2.description',
'heading.actions.0', 'heading.actions.length', 'metrics.items.3.value', 'nav.label',
'save.button.cursor', 'save.button.opacity', 'save.status.text', 'tree.3', 'tree.length',
'save.button.cursor', 'save.button.opacity', 'save.status.text',
].sort();
test('system comparison detects mutations', async ({ page, request }) => {
+1 -1
View File
@@ -205,7 +205,7 @@ test('a view stored under older rules still lists and opens with its usable valu
return (await response.json()).id;
};
const imagesId = await raw({ page: 'images', name: 'Old cats', state: JSON.stringify({ q: 'cat', sort: 'shuffle', layout: 'grid' }) });
await raw({ page: 'library', name: '', state: JSON.stringify({ q: 'dog', sort: 'name-desc' }) });
await raw({ page: 'library', name: '', state: JSON.stringify({ q: 'dog', sort: 'shuffle' }) });
expect(await listed(request)).toEqual([
{ id: imagesId, page: 'images', name: 'Old cats', state: { ...IMAGE_DEFAULTS, q: 'cat' } },
expect.objectContaining({ page: 'library', name: 'Saved view', state: { q: 'dog', category: '', sort: 'newest', tags: [], tag_mode: 'all', folder: '' } }),
+11 -36
View File
@@ -135,34 +135,11 @@ test('/prompt_manager/web lands on the workspace Library and Back does not bounc
await expect(main.getByRole('heading', { level: 1, name: 'Overview', exact: true })).toBeVisible();
});
// D165 (#525, maintainer ruling): a discreet line under System is the workspace's only visible way to the legacy pages.
test('System Overview links the legacy pages discreetly, and each one loads (#525)', async ({ page, context }) => {
// #620: the old pages are on their way out (notice since 3.5.1), so the workspace no longer links them.
test('System no longer links the old pages (#620)', async ({ page }) => {
await page.goto(`${workspace}#/system/overview`);
const line = page.locator('#workspace-main .legacy-pages');
await expect(line).toBeVisible();
await expect(line).toContainText('Legacy pages');
const links = line.getByRole('link');
await expect(links).toHaveCount(4);
for (const [name, url, ready] of [
['Dashboard', '/prompt_manager/admin', '#addPromptBtn'],
['Tags', '/prompt_manager/admin#/tags', '#addPromptBtn'],
['Gallery', '/prompt_manager/gallery', 'body'],
['Metadata viewer', '/prompt_manager/gallery.html', 'body'],
]) {
const opened = context.waitForEvent('page');
await line.getByRole('link', { name, exact: true }).click();
const tab = await opened;
await tab.waitForLoadState('domcontentloaded');
const target = new URL(tab.url());
expect(target.pathname + target.hash).toBe(url);
await expect(tab.locator(ready).first()).toBeAttached();
await tab.close();
}
// Only on System's Overview section, and gone with the page.
await page.locator('#workspace-main .section-nav').getByRole('button', { name: 'Appearance', exact: true }).click();
await expect(line).toBeHidden();
await page.goto(`${workspace}#/library`);
await expect(page.locator('#workspace-main .legacy-pages')).toHaveCount(0);
await expect(page.locator('#workspace-main h1')).toHaveText('System');
await expect(page.locator('#workspace-main a[href*="/admin"], #workspace-main a[href*="/gallery"]')).toHaveCount(0);
});
const bootFailure = page => page.locator('#workspace-boot-failure');
@@ -175,7 +152,7 @@ test('a healthy workspace never shows the load failure message (#525)', async ({
});
for (const [label, pattern] of [['the entry module', '**/js/workspace/index.mjs*'], ['a dependency', '**/js/workspace/state.mjs*']]) {
test(`when ${label} fails to load the page still offers the legacy pages (#525)`, async ({ page }) => {
test(`when ${label} fails to load the page offers a reload and ComfyUI (#525, #620)`, async ({ page }) => {
const pageErrors = [];
page.on('pageerror', error => pageErrors.push(error.message));
await page.route(pattern, route => route.abort());
@@ -183,19 +160,17 @@ for (const [label, pattern] of [['the entry module', '**/js/workspace/index.mjs*
const failure = bootFailure(page);
await expect(failure).toBeVisible();
await expect(failure).toHaveAttribute('role', 'alert');
const dashboard = failure.getByRole('link', { name: 'Dashboard', exact: true });
await expect(dashboard).toBeVisible();
await expect(failure.getByRole('link')).toHaveCount(4);
await dashboard.click();
await expect(page).toHaveURL(/\/prompt_manager\/admin$/);
await expect(failure.getByRole('link', { name: 'Reload the page', exact: true })).toHaveAttribute('href', 'workspace');
await expect(failure.getByRole('link', { name: 'ComfyUI', exact: true })).toHaveAttribute('href', '../');
await expect(failure.getByRole('link')).toHaveCount(2);
expect(pageErrors).toEqual([]);
});
}
test('when a module throws while starting the page still offers the legacy pages (#525)', async ({ page }) => {
test('when a module throws while starting the page still offers a reload (#525, #620)', async ({ page }) => {
await page.route('**/js/workspace/state.mjs*', route => route.fulfill({ contentType: 'text/javascript', body: "throw new Error('boot failure');" }));
await page.goto(`${workspace}#/library`);
await expect(bootFailure(page).getByRole('link', { name: 'Dashboard', exact: true })).toBeVisible();
await expect(bootFailure(page).getByRole('link', { name: 'Reload the page', exact: true })).toBeVisible();
// The broken module's own error is this test's premise, not a failure of the page.
page.workspaceErrors.length = 0;
});
@@ -569,7 +544,7 @@ test('System is native: the concept shell, section navigation, sections and the
// D92: no "Sample configuration" chip.
await expect(main.locator('.heading .chip')).toHaveCount(0);
expect(await main.evaluate(node => [...node.children].map(child => `${child.localName}.${child.className}`)))
.toEqual(['div.heading', 'div.section-nav', 'div.settings-layout', 'p.small legacy-pages']);
.toEqual(['div.heading', 'div.section-nav', 'div.settings-layout']);
const nav = main.getByRole('group', { name: 'System sections', exact: true });
await expect(nav.getByRole('button')).toHaveText(SYSTEM_SECTION_LABELS);
await expect(nav.getByRole('button', { name: 'Overview', exact: true })).toHaveAttribute('aria-pressed', 'true');
-7
View File
@@ -93,13 +93,6 @@ test('every Overview card opens its own native page, never a legacy one (#525)',
}
});
test('the legacy pages System links are the four that stay at their URLs (#525 D165)', async () => {
const { LEGACY_PAGES } = await source;
assert.deepEqual(LEGACY_PAGES.map(page => page.href), ['/prompt_manager/admin', '/prompt_manager/admin#/tags',
'/prompt_manager/gallery', '/prompt_manager/gallery.html']);
for (const page of LEGACY_PAGES) assert.ok(page.label);
});
test('a job of the Activity page is its own deep link', async () => {
const { resolveRoute, routeTitle, isHandoff } = await source;
const id = '0123456789abcdef'.repeat(2);
-8
View File
@@ -25,14 +25,6 @@ export const routes = [
description: 'Configuration, maintenance and recovery—with browsing kept in the workspace.', action: 'Open System' },
];
/** D165 (#525): the legacy pages, which stay at their URLs; System's Overview section links them discreetly. */
export const LEGACY_PAGES = Object.freeze([
{ label: 'Dashboard', href: '/prompt_manager/admin' },
{ label: 'Tags', href: '/prompt_manager/admin#/tags' },
{ label: 'Gallery', href: '/prompt_manager/gallery' },
{ label: 'Metadata viewer', href: '/prompt_manager/gallery.html' },
].map(page => Object.freeze(page)));
const NATIVE_ROUTES = new Set(['images', 'library', 'tags', 'inspect', 'overview', 'saved', 'favorites', 'activity', 'review', 'system',
'collection']);
+1 -20
View File
@@ -12,7 +12,7 @@
* 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 { LEGACY_PAGES, SYSTEM_SECTIONS, navigateTo } from './state.mjs';
import { SYSTEM_SECTIONS, navigateTo } from './state.mjs';
import { createSystemDrafts, DRAFT_DOMAINS, INVALID_SETTING_CODE } from './system-state.mjs';
import { sectionNav } from './system-controls.mjs';
import { mountOverview } from './system-overview.mjs';
@@ -145,22 +145,6 @@ export const DELIVERED_SECTIONS = Object.freeze({
* under one of this tab's drafts flags that draft (D92's conflict status), including a change made while System was
* not mounted (the drafts outlive it, Q10).
*/
/** D165 (#525): one small line under System's Overview, the workspace's only visible way to the legacy pages. */
function legacyPagesLine() {
const line = el('p', 'small legacy-pages');
line.append('Legacy pages: ');
LEGACY_PAGES.forEach((page, index) => {
const link = document.createElement('a');
link.textContent = page.label;
link.href = window.ApiClient.resolveUrl(page.href);
link.target = '_blank';
link.rel = 'noopener';
if (index) line.append(' · ');
line.append(link);
});
return line;
}
export function mountSystem(main, api, scope, { section, activity, counts = null, sections = DELIVERED_SECTIONS,
navigate = navigateTo }) {
const store = systemDrafts(api);
@@ -187,7 +171,6 @@ export function mountSystem(main, api, scope, { section, activity, counts = null
saveRow.append(status, save);
layout.append(saveRow);
main.append(nav, layout);
const legacyPages = legacyPagesLine();
const paintStatus = () => {
status.textContent = store.statusText();
@@ -213,8 +196,6 @@ export function mountSystem(main, api, scope, { section, activity, counts = null
// Every section is delivered (S12), and the router only names known sections (state.mjs).
const nodes = sections[next](layout, { api, activity, scope: sectionScope, drafts: store, counts });
layout.prepend(...nodes);
if (next === 'overview') main.append(legacyPages);
else legacyPages.remove();
paintStatus();
}
show(section);
-1
View File
@@ -575,7 +575,6 @@ pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; margin-bloc
}
.more-menu-list[hidden] { display: none; }
.more-menu-list > button { justify-content: flex-start; text-align: left; width: 100%; white-space: nowrap; }
.legacy-pages { margin: 16px 0 0; color: var(--muted); /* D165 */ }
/* D164 (#524): the prompt page has four heading actions and titles of any length, which the concept never shows. From
the concept's sidebar breakpoint up the actions keep one row and the title takes what is left, clamped to two lines
+2 -6
View File
@@ -60,12 +60,8 @@
</div>
<div id="workspace-toast" class="toast" role="status" aria-live="polite" aria-atomic="true"></div>
<div id="workspace-boot-failure" role="alert" hidden style="position: fixed; inset: 16px 16px auto; z-index: 1000; padding: 16px; border: 1px solid #888; border-radius: 8px; background: #1b1b1f; color: #eee; font: 14px/1.5 system-ui, sans-serif;">
<p style="margin: 0;">The workspace couldn't load. Reload the page, or use a legacy page:
<a style="color: inherit;" href="admin">Dashboard</a> ·
<a style="color: inherit;" href="admin#/tags">Tags</a> ·
<a style="color: inherit;" href="gallery">Gallery</a> ·
<a style="color: inherit;" href="gallery.html">Metadata viewer</a></p>
<p style="margin: 0;">The workspace couldn't load. <a style="color: inherit;" href="workspace">Reload the page</a>, or go back to <a style="color: inherit;" href="../">ComfyUI</a>.</p>
</div>
<noscript><p>This workspace needs JavaScript. <a href="admin">Open the existing dashboard</a>.</p></noscript>
<noscript><p>This workspace needs JavaScript.</p></noscript>
</body>
</html>