# 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`, 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.**