jellyfin, extracted into plugins/jellyfin — the platform half

Eighth extraction, second resurrection. Same shape as soulseek: hono.ts had the
router import and its PROTECTED_MOUNTS entry commented out since 2026-08-13 and
the permission reduced to api: [], so assertPermissionTotality was satisfied,
the server booted clean, and every panel request 404'd. Also closes the same
data hazard — jellyfin_servers was absent from the drizzle barrel, so bun
db:push would have dropped it.

No frontend call site needed repointing: mountPrefix derives /jellyfin from the
appName, which is what the router already hardcoded.

Test fixtures fixed properly rather than re-anchored again. Extracting jellyfin
broke 18 tests, the FOURTH extraction in a row to break the same files — photos
and transmission yesterday, slskd an hour ago, jellyfin now. installer.test.ts
now builds literal CatalogueEntry fixtures and asks the catalogue nothing: it
pins planSteps and runInstall, which are installer logic, and a service leaving
for plugins/ says nothing about whether the installer still plans the right
steps. catalogue.test.ts stays the place real entries are checked well-formed.
members/catalogue/preflight re-anchored to memos, which has jellyfin's exact
shape.

The .prettierignore rule for the vendored hls.mjs moved into the plugin rather
than being left behind naming a deleted path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 03:17:43 +00:00
co-authored by Claude Opus 5
parent b196c9cb55
commit aebd6ff0c0
49 changed files with 52 additions and 41512 deletions
-2
View File
@@ -102,8 +102,6 @@ export function App() {
element={<Dashboard.PluginScreen {...plugin} />}
/>,
])}
<Route path="/jellyfin" element={<Dashboard.JellyfinScreen />} />
<Route path="/jellyfin/:section" element={<Dashboard.JellyfinScreen />} />
<Route path="/gitea" element={<Dashboard.GiteaScreen />} />
<Route path="/gitea/:section" element={<Dashboard.GiteaScreen />} />
<Route path="/gitea/repo/:owner/:name" element={<Dashboard.GiteaScreen />} />
@@ -1,37 +0,0 @@
import { Navigate, useParams } from 'react-router';
import type { LayoutNode } from 'officerdev';
import { WorkspaceView, DEFAULT_JELLYFIN_SECTION, jellyfinSectionPath, isJellyfinSection } from 'officerdev';
import { useDashboardState } from 'state/useDashboardState';
import { defaultLayout } from './defaultLayout';
// /jellyfin uses the Workspace/Panel system: the section and library nav (jellyfin-nav) on the left, the
// section view (jellyfin-view) on the right. Both talk to the officer-jellyfin sidecar through the
// /api/jellyfin auth proxy; the Jellyfin URL and its access token live in the sidecar and the browser never
// sees either.
//
// Video only — the owner's audio is Officer's own /music player, and the sidecar already filters music
// libraries out of everything this screen can ask for.
//
// The open section is :section in the URL; which library, which item and what is playing are in the query
// string. Nothing about "what is open" lives in a panel channel.
export const JellyfinScreen = () => {
const { section } = useParams();
const workspace = useDashboardState<LayoutNode>('screens/jellyfin', defaultLayout);
// Bare /jellyfin, or a section that doesn't exist, canonicalises rather than rendering a default behind a
// URL that names something else — the nav highlight comes from the router, so a bogus URL highlights nothing.
if (!isJellyfinSection(section)) {
return <Navigate to={jellyfinSectionPath(DEFAULT_JELLYFIN_SECTION)} replace />;
}
return (
<div className="h-full w-full pt-2">
<WorkspaceView
workspace={workspace}
locked
appTypes={{ allowed: ['jellyfin-nav', 'jellyfin-view'], fallback: 'jellyfin-view' }}
/>
</div>
);
};
@@ -1,11 +0,0 @@
import type { LayoutNode } from 'officerdev';
export const defaultLayout: LayoutNode = {
type: 'group',
id: 'jellyfin-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'jellyfin-nav', appType: 'jellyfin-nav' }, size: 18 },
{ node: { type: 'panel', id: 'jellyfin-view', appType: 'jellyfin-view' }, size: 82 },
],
};
@@ -1 +0,0 @@
export * from './JellyfinScreen';
@@ -14,7 +14,6 @@ export * from './Files';
export * from './Calendar';
export * from './Contacts';
export * from './CommandHQ';
export * from './Jellyfin';
export * from './Gitea';
export * from './Invoices';
export * from './Wallet';
@@ -23,7 +23,6 @@ const RULES: TitleRule[] = [
{ match: (p) => p.startsWith('/app-store'), title: 'App store' },
{ match: (p) => p.startsWith('/marketplace'), title: 'Marketplace' },
{ match: (p) => p.startsWith('/plugins'), title: 'Marketplace' },
{ match: (p) => p.startsWith('/jellyfin'), title: 'Video' },
{ match: (p) => p.startsWith('/command-hq'), title: 'Command HQ' },
{ match: (p) => p.startsWith('/gitea'), title: 'Gitea' },
{ match: (p) => p.startsWith('/invoices'), title: 'Invoices' },