The fifth plugin out, and the cheapest so far — offscale's shape rather than
music's or vault's. Nothing stayed behind and nothing had to be redesigned,
for three structural reasons recorded in PLUGIN.md:
• No reverse dependency. Nothing in src/ imported anything of photos'.
• Configuration was already right. The URL and API key live in photos_config,
a per-user registry encrypted at rest and driven from the app. Vault had to
move VAULTWARDEN_URL out of .env and invent a settings panel; here the
sidecar already read the database, and its own comment already said why
nothing reads process.env.
• The frontend was already panels. The screen was a WorkspaceView around
exactly photos-nav and photos-view, so web/panels.ts and web/layout.ts are
the screen's contents with the wrapper removed.
The router's prefix is now derived from mountPrefix rather than the literal
'/api/photos', which was correct only by coincidence — a third-party publisher
mounts at /api/p/<publisher>/photos and the literal would forward the wrong
path to the sidecar.
Palette debt paid down as the runbook requires: 6 classes (it says 47; someone
had already done most of it), all of them state colours wearing palette
numbers, now `success` and `warning` from the closed tone set so they follow
the theme.
Immich itself is still provisioned by the app store rather than by this plugin
— the `provisioned` half of its old catalogue entry has no home until Docker
provisioning for plugins exists. Point it at an instance you already run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.3 KiB
Photos — the extraction record
Extracted from the platform on 2026-08-18, the fifth plugin out (after offscale, music, email, vault).
plugins/EXTRACTING-A-PLUGIN.md is the runbook; this file is what it cost.
The clean case
Offscale's shape rather than music's or vault's: nothing stayed behind, and nothing had to be redesigned. Worth recording why, because it is the cheapest extraction so far and the reasons are structural rather than luck.
- No reverse dependency. Nothing in
src/imported anything of photos'. The whole platform surface was one commented router import, one barrel line, and the ordinary wiring (route, screen, registry, dock rule, catalogue entry). Compare vault, whose extraction had to invent an account-lifecycle hook because three core auth handlers called into it. - Configuration was already right. The URL and API key live in
photos_config— a per-user registry with one active row, encrypted at rest, driven from the app. Vault had to moveVAULTWARDEN_URLout of.envand invent a settings panel; here the sidecar already read the database and its own comment already said why nothing readsprocess.env: anIMMICH_API_KEYthere would sit in the environment of every pm2 process, including the one with the largest attack surface and no code to use it. - The frontend was already panels. Two of them,
photos-navandphotos-view, plus a layout. The screen was aWorkspaceViewaround exactly those, soweb/panels.tsandweb/layout.tsare the screen's own contents with the wrapper removed.
What moved
| Was | Is now |
|---|---|
src/servers/sidecar/photos/* (5 files) |
sidecar/* |
src/servers/api/photos/router.ts |
api/router.ts |
src/databases/officer_db/src/photos/* |
db/* |
src/workspaces/officerdev/src/apps/Photos/* (25 files) |
web/* |
src/apps/officer-web/Screens/Dashboard/Photos/* |
web/layout.ts |
The prefix in api/router.ts is now DERIVED from mountPrefix rather than the literal /api/photos,
which was right only by coincidence — see the comment there, and plugins/music/api/router.ts for the
same fix and the same reasoning.
What the platform lost
Deleted, not commented: the sidecar, the api router, the db directory, the Photos app, the screen, the
App.tsx routes, 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
comment, and the app-store/catalogue.ts entry.
The catalogue deletion is required, not 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, puts /photos into deniedRoutes and
withholds the dock tile, on a server where the plugin is installed and working.
Two things paid down on the way
Palette classes. The runbook says to pay these down as part of the extraction rather than exporting
colour debt into a repository nobody revisits, and it puts Photos at 47. It was 6 — someone had
already done most of it. All six were state colours wearing palette numbers (text-emerald-400 for the
app's own tile, text-emerald-500/text-amber-500 on a connection check) and are now success and
warning from the closed tone set, so they follow the theme.
Test fixtures. Photos was the canonical app-store fixture across four test files, chosen for being
existing|provisioned with members: 'accounts'. Repointed at jellyfin, which has the identical
shape, rather than deleted — the assertions are about the installer, not about photos.
Still open
- Immich is still provisioned by the app store, not by this plugin.
modes: ['existing', 'provisioned']and a compose template lived in the catalogue entry that had to be deleted, so theprovisionedhalf has no home right now: you point it at an instance you already run. Docker provisioning for plugins is a design still in progress, and this is one of the features waiting for it. - No
assets/icon.png. Falls back to theImagesglyph, which is what the catalogue entry used.