Files
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

94 lines
5.3 KiB
Markdown

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