Ninth plugin out, third resurrection: switched off since 2026-08-13 with the permission reduced to api: [], so the server booted clean and every panel request 404'd. invoiceshelf/schema was also commented out of the drizzle barrel. Named `invoices`, not `invoiceshelf`. The appName drives the API prefix, the frontend route, the table prefix and the install key all at once, and before extraction those had drifted: the API was /api/invoiceshelf while the screen was /invoices. The URL is what a person sees and should name what the plugin shows rather than the daemon behind it — the permission key and label were already `invoices`, so the API prefix was the odd one out. Cost: three call sites, and renaming invoiceshelf_accounts to invoices_accounts to satisfy the table-prefix rule. That rename was free ONLY because the table existed on no server, which was checked before deciding. With rows in it, keeping the vendor name would have been the better trade: db:push sees a rename as a drop plus a create and this project has no migration mechanism. The sidecar keeps its Invoiceshelf-named functions deliberately — they name the upstream API it actually speaks. 91 palette classes converted, 52 of them in STATUS_TONE, which was already the single source of status colour but held raw palette strings. SENT and VIEWED now share `info`: the closed six-tone set has no second shade of "in flight, not yet actioned". Recorded in PLUGIN.md as a real loss.
5.3 KiB
Invoices — the extraction record
Extracted from the platform on 2026-08-19, the ninth plugin out, after jellyfin.
plugins/EXTRACTING-A-PLUGIN.md is the runbook.
Dead on arrival, like the two before it
hono.ts carried the router import and its PROTECTED_MOUNTS entry commented out
(// plugin — switched off 2026-08-13) and the permission had been reduced to api: [], so
assertPermissionTotality was satisfied, the server booted clean, and every panel request 404'd. Same
data hazard too: invoiceshelf/schema was commented out of the drizzle barrel.
The naming decision, which is the interesting part
Before extraction the API was /api/invoiceshelf and the screen was /invoices. Those had
drifted apart harmlessly while they were separate hardcoded strings. A plugin cannot hold both:
appName is THE id — API prefix, frontend route, table prefix, sidecar suffix and install key all
derive from it.
So one of them had to move, and the choice was:
| appName | URL | API | Tables |
|---|---|---|---|
invoiceshelf |
/invoiceshelf ❌ |
/api/invoiceshelf ✓ |
invoiceshelf_accounts ✓ |
invoices |
/invoices ✓ |
/api/invoices (3 sites) |
rename to invoices_accounts |
invoices won, for two reasons. The URL is the part a person sees, and it should name what the
thing shows rather than the vendor behind it — the permission key and the label were already
invoices, so the API prefix was the odd one out rather than the route. And the cost was small:
three call sites, all in useInvoiceShelfData.ts, plus a table rename.
The rename was free only because the table existed on no server — checked before deciding, not
after. invoiceshelf_accounts did not exist in the live database, so there were no rows to migrate.
Had there been any, keeping the vendor name and living with /invoiceshelf would have been the better
trade: bun db:push sees a rename as a drop plus a create, so a table with data would have needed a
hand-written ALTER TABLE … RENAME TO before the push, and that is a migration this project has no
mechanism for.
What moved
| Was | Is now |
|---|---|
src/servers/sidecar/invoiceshelf/* (4 files) |
sidecar/* |
src/servers/api/invoiceshelf/router.ts |
api/router.ts |
src/databases/officer_db/src/invoiceshelf/* (3 files) |
db/* |
src/workspaces/officerdev/src/apps/Invoices/* (25 files) |
web/* |
src/apps/officer-web/Screens/Dashboard/Invoices/* |
web/layout.ts |
The invoiceshelf* symbols inside db/ were renamed to invoices* with the table, so the schema
file and the SQL agree. The SIDECAR keeps its Invoiceshelf-named functions — those name the upstream
API it speaks, which really is InvoiceShelf, and renaming them would have made the sidecar lie about
what it talks to.
Self-contained
invoices_accounts is a registry this plugin owns outright — label, URL, encrypted credential, one
active — like photos and jellyfin, and for the same reason: one person really does keep more than one
company's books. No core table, no platform union to extend.
Fixtures
invoiceshelf was a fixture in members.test.ts — re-anchored an hour earlier when jellyfin left, and
broken again here. It now names caldav and memos, both services with no extraction planned,
which is the actual fix for a test that has been re-pointed four times.
Not carried over
composeTemplate and modes: ['existing', 'provisioned'] died with the catalogue entry. Like every
plugin so far, this is point-at-your-own-instance only until plugin Docker provisioning exists.
One status distinction the tone set could not keep
STATUS_TONE in web/shared.ts was already the single source for status colour, but as raw palette
classes — 52 of this plugin's 91 palette occurrences were in that one map. It is now
Record<string, Tone>, read only by StatusBadge.
SENT and VIEWED both resolve to info, and upstream had them as distinct blues. They are real,
ordered states, and the closed six-tone set has no second shade of "in flight, not yet actioned" —
busy would misrepresent viewed as in progress. The badge text still distinguishes them; only the
colour is shared. Recorded here because it is a genuine loss rather than a tidy-up, and because the
right fix, if it matters, is a seventh tone rather than a local exception.
Two judgement calls in the same map, both reversible one-liners:
PARTIALLY_PAID→busy, notwarning, because it renders in the same cell asUNPAIDand flattening both to warning would destroy a live distinction.EXPIRED→warningwhileREJECTED→danger: an estimate that lapsed can be re-issued, one that was declined is over.
The dashboard chart's three hard-coded hexes moved with the legend dots that label them, to
var(--info) / var(--success) / var(--destructive). Those resolve from :root in every theme
(only 3 of 14 themes override --info/--success; all 14 define --destructive). Not verified in a
browser.