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:
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user