From 1b65b83f1fb783ad5fffff2ccf74e9b20bc70880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 16 Aug 2026 04:10:23 +0000 Subject: [PATCH] the plugins screen becomes /marketplace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/plugins` redirects rather than 404s: the word is in the dock, in the docs and in whatever anyone bookmarked, and a route that worked yesterday returning nothing reads as the feature having been removed. The API keeps its name — only the screen moved — so the permission entry lists both paths, because a client-side redirect is still a route that has to resolve before it can send anyone anywhere. Also: the fetch stubs in marketplace.test.ts needed `as unknown as typeof fetch`. Bun's `typeof fetch` carries `preconnect`, so a bare stub is not assignable — tests passed while tsgo did not, which is the wrong way round to find out. tsgo clean, frontend builds, 873 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 --- src/apps/officer-web/App.tsx | 6 +++++- src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx | 2 +- src/apps/officer-web/state/usePageTitle.ts | 3 ++- src/servers/permissions/registry.ts | 4 +++- src/servers/plugins/marketplace.test.ts | 10 +++++++--- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index e146ebee..2ec78918 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -69,7 +69,11 @@ export function App() { } /> } /> } /> - } /> + } /> + {/* The screen was `/plugins` until marketplace.officer.dev existed. Redirected rather than + dropped: the word appears in the dock, in docs, and in whatever anyone bookmarked, and a + 404 on a route that worked yesterday reads as the feature having been removed. */} + } /> {/* Installed plugins. Core routes above stay hand-written; everything below is generated from what is installed, because a bundler cannot follow a runtime import specifier. The wildcard hands the whole subtree to the plugin's own router, which react-router nests natively. */} diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx index d953e48d..ccaf691b 100644 --- a/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Layout/Dock.tsx @@ -186,7 +186,7 @@ export const CORE_DOCK_ITEMS: DockItem[] = [ // things that disappears when uninstalled. { label: 'App store', to: '/app-store', icon: Store, color: '#64748b' }, // Core, not contributed by a plugin: this is the screen that installs them, so it cannot arrive with one. - { label: 'Plugins', to: '/plugins', icon: Puzzle, color: '#94a3b8' }, + { label: 'Marketplace', to: '/marketplace', icon: Puzzle, color: '#94a3b8' }, ]; /** diff --git a/src/apps/officer-web/state/usePageTitle.ts b/src/apps/officer-web/state/usePageTitle.ts index 39c65323..82dfa291 100644 --- a/src/apps/officer-web/state/usePageTitle.ts +++ b/src/apps/officer-web/state/usePageTitle.ts @@ -22,7 +22,8 @@ const RULES: TitleRule[] = [ { match: (p) => p.startsWith('/calendar'), title: 'Calendar' }, { match: (p) => p.startsWith('/contacts'), title: 'Contacts' }, { match: (p) => p.startsWith('/app-store'), title: 'App store' }, - { match: (p) => p.startsWith('/plugins'), title: 'Plugins' }, + { match: (p) => p.startsWith('/marketplace'), title: 'Marketplace' }, + { match: (p) => p.startsWith('/plugins'), title: 'Marketplace' }, { match: (p) => p.startsWith('/photos'), title: 'Photos' }, { match: (p) => p.startsWith('/jellyfin'), title: 'Video' }, { match: (p) => p.startsWith('/soulseek'), title: 'Soulseek' }, diff --git a/src/servers/permissions/registry.ts b/src/servers/permissions/registry.ts index 312d30f3..b54926fd 100644 --- a/src/servers/permissions/registry.ts +++ b/src/servers/permissions/registry.ts @@ -385,7 +385,9 @@ const CORE_REGISTRY: Permission[] = [ // `offscale` at read without being able to install or remove anything. kind: 'admin', api: ['/plugins'], - routes: ['/plugins'], + // The API kept its name; only the screen was renamed. `/plugins` stays listed because the redirect + // is a real route — a client-side redirect still has to resolve before it can send anyone anywhere. + routes: ['/marketplace', '/plugins'], }, { key: 'server-admin', diff --git a/src/servers/plugins/marketplace.test.ts b/src/servers/plugins/marketplace.test.ts index f76de647..d9993d4b 100644 --- a/src/servers/plugins/marketplace.test.ts +++ b/src/servers/plugins/marketplace.test.ts @@ -24,13 +24,16 @@ const entry = (appName: string) => ({ const respondWith = (body: unknown, status = 200) => { globalThis.fetch = (async () => - new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })) as typeof fetch; + new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + })) as unknown as typeof fetch; }; const failWith = (message: string) => { globalThis.fetch = (async () => { throw new Error(message); - }) as typeof fetch; + }) as unknown as typeof fetch; }; beforeEach(() => clearCatalogueCache()); @@ -71,7 +74,8 @@ describe('fetchCatalogue', () => { }); test('a proxy serving HTML instead of JSON does not empty the store', async () => { - globalThis.fetch = (async () => new Response('502 Bad Gateway', { status: 200 })) as typeof fetch; + globalThis.fetch = (async () => + new Response('502 Bad Gateway', { status: 200 })) as unknown as typeof fetch; const entries = await fetchCatalogue(); expect(entries.length).toBeGreaterThan(0); });