photos, extracted into plugins/photos — the platform half
Fifth extraction, and the clean case: no reverse dependency, configuration already server-side, frontend already panels. Everything photos now lives at gitea.officer.dev/plugins/photos. Deleted, not commented: the sidecar, the api router, the db directory, the Photos app (25 files), the screen, the App.tsx route pair, the Dashboard barrel line, the usePageTitle rule, the AppRegistry wiring, the officerdev barrel re-exports, the permission registry entry, both database barrels, the hono.ts import comment, and the app-store catalogue entry. The catalogue deletion is required rather than tidying — the trap the runbook records as having bitten twice. permissionAvailability() derives from sidecar_installs, which a plugin never gets a row in, so a leftover entry makes the permission permanently 'unavailable' and withholds the route and dock tile on a server where the plugin is installed and working. ── Test fixtures, repointed rather than deleted ── Photos was the canonical app-store fixture across four test files, chosen for being existing|provisioned with members: 'accounts'. Jellyfin has the identical shape, so the assertions moved there — they are about the installer, not about photos, and deleting them would have quietly dropped coverage of the provisioned install path along with the plugin. One loss worth naming: Immich's `provisioned` install mode went with the catalogue entry. Docker provisioning for plugins does not exist yet, so photos is point-at-your-own-instance until it does. bunx tsgo clean. 1080 pass / 0 fail, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,8 +84,6 @@ export function App() {
|
||||
<Route path="/soulseek/:section" element={<Dashboard.SoulseekScreen />} />
|
||||
<Route path="/command-hq" element={<Dashboard.CommandHqScreen />} />
|
||||
<Route path="/command-hq/:sessionId" element={<Dashboard.CommandHqScreen />} />
|
||||
<Route path="/photos" element={<Dashboard.PhotosScreen />} />
|
||||
<Route path="/photos/:section" element={<Dashboard.PhotosScreen />} />
|
||||
<Route path="/app-store" element={<Dashboard.AppStoreScreen />} />
|
||||
<Route path="/marketplace" element={<Dashboard.PluginsScreen />} />
|
||||
{/* The screen was `/plugins` until marketplace.officer.dev existed. Redirected rather than
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Navigate, useParams } from 'react-router';
|
||||
import type { LayoutNode } from 'officerdev';
|
||||
import { WorkspaceView, DEFAULT_PHOTOS_SECTION, photosSectionPath, isPhotosSection } from 'officerdev';
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
// /photos uses the Workspace/Panel system: the section nav (photos-nav) on the left, the section view
|
||||
// (photos-view) on the right. Both talk to the officer-photos sidecar through the /api/photos auth proxy; the
|
||||
// Immich URL and API key live in the sidecar's environment and the browser never sees either.
|
||||
//
|
||||
// The open section is :section in the URL; deeper selection (which asset, album, person, search) is in the
|
||||
// query string. Nothing about "what is open" lives in a panel channel.
|
||||
|
||||
export const PhotosScreen = () => {
|
||||
const { section } = useParams();
|
||||
const workspace = useDashboardState<LayoutNode>('screens/photos', defaultLayout);
|
||||
|
||||
// Bare /photos, 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 (!isPhotosSection(section)) {
|
||||
return <Navigate to={photosSectionPath(DEFAULT_PHOTOS_SECTION)} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
locked
|
||||
appTypes={{ allowed: ['photos-nav', 'photos-view'], fallback: 'photos-view' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { LayoutNode } from 'officerdev';
|
||||
|
||||
export const defaultLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'photos-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'photos-nav', appType: 'photos-nav' }, size: 18 },
|
||||
{ node: { type: 'panel', id: 'photos-view', appType: 'photos-view' }, size: 82 },
|
||||
],
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './PhotosScreen';
|
||||
@@ -15,7 +15,6 @@ export * from './Calendar';
|
||||
export * from './Contacts';
|
||||
export * from './Soulseek';
|
||||
export * from './CommandHQ';
|
||||
export * from './Photos';
|
||||
export * from './Jellyfin';
|
||||
export * from './Transmission';
|
||||
export * from './Gitea';
|
||||
|
||||
@@ -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('/photos'), title: 'Photos' },
|
||||
{ match: (p) => p.startsWith('/jellyfin'), title: 'Video' },
|
||||
{ match: (p) => p.startsWith('/soulseek'), title: 'Soulseek' },
|
||||
{ match: (p) => p.startsWith('/command-hq'), title: 'Command HQ' },
|
||||
|
||||
Reference in New Issue
Block a user