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);
});