Files
invoices/PLUGIN.md
T
officer 6c70eb8eb3 invoices, extracted from the platform as a plugin
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.
2026-08-19 03:47:44 +00:00

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, not warning, because it renders in the same cell as UNPAID and flattening both to warning would destroy a live distinction.
  • EXPIRED → warning while REJECTED → 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.