diff --git a/deploy/2026.08.23.05.md b/deploy/2026.08.23.05.md new file mode 100644 index 00000000..d332fa06 --- /dev/null +++ b/deploy/2026.08.23.05.md @@ -0,0 +1,129 @@ +# 2026.08.23.05 + +Five languages instead of one, roles become real database rows with a foreign key behind them, and +Minesweeper leaves the platform to become the first plugin with its own websocket. + +**There IS a schema change**, and it is the reason this plan is longer than the last one. Two foreign keys +and one dropped CHECK, on `users` and `role_permissions`. **Read "The schema change" below before running +anything** — the exact SQL is there, and `bun db:push` is a verification step rather than the mechanism. + +**No dependency change.** `bun.lock` is untouched. **Do not run `bun install`.** + +## What is in it + +- **Five locales, complete and in step.** `en-GB`, `pt-PT`, `es-ES`, `de-DE`, `fr-FR` — 1,129 keys each. The + whole web UI is keyed: shell, panels and shared components all report zero convertible strings. +- **Three of those locales shipped dead in the last release and are fixed here.** `es-ES`, `de-DE` and + `fr-FR` were translated, in parity, and offered by the picker — while all three catalogue files still + imported only `en-GB` and `pt-PT`, so choosing one rendered the entire UI in raw keys. The parity tests + could not have caught it: they compare JSON files to each other, and a file nobody imports is as + parity-correct as one that is loaded. `lib/catalogues.test.ts` is now the thing that knows. +- **Roles are a table with a foreign key, not a TypeScript union.** `USER_ROLES` is gone, `UserRole` is + `string`, and the compile-time guard moves to a runtime check at every write path that takes a role from + outside. Renaming a role now moves its holders and grants atomically, as Postgres's job. +- **Minesweeper is a plugin** (`gitea.officer.dev/plugins/minesweeper`), not part of the platform. The + permission registry drops from 22 entries to 21 — `execution` goes 4 to 3. +- **Plugins can have websockets.** `/api/plugin-ws/` relays onto the plugin's own sidecar, which + serves it on the single port it announced at registration. Authorisation is the plugin's own permission, + so there is no second grant to forget. +- **A plugin can declare the architecture its packages need.** `multiarch: 'i386'` runs + `dpkg --add-architecture` and `apt-get update` before installing, on apt hosts, skipped when already + enabled. A closed enum rather than a command, because it runs as root. +- **Foreign keys are named explicitly across the fleet's plugins**, ending the `db:push` churn where + Postgres truncated an over-long generated name and drizzle-kit re-created the constraint every run. + +## The schema change + +Both foreign keys reference `roles.name`, so **every value in `users.role` and `role_permissions.role` must +already exist as a row in `roles`**, or the constraint cannot be created. + +**That was checked on every host before this tag was cut.** All five are on `deploy/2026.08.23.04`, which is +the release that created and seeded `roles`; `alpha`, `fofos`, `production-pertento-bridge` and +`deadpool-rezio-bridge` each report `roles = 4` and `0` orphans. **`macbookpro` was NOT verified** — it has +no `psql` client, so only its tag could be read. Check it there before running the SQL, by whatever means +that host offers. + +Run this **first**, before `bun db:push`. It is idempotent and safe to run twice. + +```sql +-- Refuse to proceed if any role in use is not a real row. Raises rather than half-applying. +DO $$ +DECLARE bad int; +BEGIN + SELECT count(*) INTO bad FROM ( + SELECT u.role FROM users u LEFT JOIN roles r ON r.name = u.role WHERE r.name IS NULL + UNION ALL + SELECT p.role FROM role_permissions p LEFT JOIN roles r ON r.name = p.role WHERE r.name IS NULL + ) x; + IF bad > 0 THEN + RAISE EXCEPTION 'STOP: % role value(s) have no matching roles row. Do not continue; report this.', bad; + END IF; +END $$; + +ALTER TABLE role_permissions DROP CONSTRAINT IF EXISTS ck_role_permissions_role; + +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_users_role') THEN + ALTER TABLE users ADD CONSTRAINT fk_users_role + FOREIGN KEY (role) REFERENCES roles(name) ON UPDATE CASCADE; + END IF; +END $$; + +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_role_permissions_role') THEN + ALTER TABLE role_permissions ADD CONSTRAINT fk_role_permissions_role + FOREIGN KEY (role) REFERENCES roles(name) ON UPDATE CASCADE; + END IF; +END $$; +``` + +`ck_role_permissions_not_owner` stays. It is a different constraint and this release does not touch it. + +Then verify **by querying**, not by reading an exit code: + +```sql +SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint +WHERE conname IN ('fk_users_role','fk_role_permissions_role', + 'ck_role_permissions_role','ck_role_permissions_not_owner'); +``` + +Both `fk_` rows must be present and say `ON UPDATE CASCADE` with no `ON DELETE`. +`ck_role_permissions_role` must be **absent**. `ck_role_permissions_not_owner` must be **present**. +Put that output in the report — for this release it is the evidence, and "schema ✓" without it is a guess. + +## Every host + +1. `git fetch origin --tags` then `git merge --ff-only deploy/2026.08.23.05` +2. **The SQL above**, then the verification query. Stop on the `RAISE EXCEPTION` and report it. +3. `bun db:push` — expect **no changes**. If it still wants to alter something after step 2, the SQL was + incomplete; stop and report rather than letting it improvise. +4. `bun run plugins:update` +5. `pm2 restart officer` + +`officer` only at step 5. Step 4 restarts each plugin's own sidecar, which belongs in the report. + +**`macbookpro`:** append `/opt/homebrew/bin` to PATH for the `bun` commands, run `pm2` separately. + +## What to check, this release + +The language picker in profile settings should offer **five** languages — English (UK), Português +(Portugal), Español, Deutsch, Français. Pick one that is not English and look at a **panel**, not just the +shell: the last release's defect was invisible in the shell and total in the panels. + +A host still offering two languages did not pick up the frontend bundle. + +Minesweeper should be **absent** from the dock and `/minesweeper` should 404. It is a plugin now and no host +has it installed. That is correct, and installing it is not part of this release. + +## Hosts + +- **`edge-pertento-bridge`** — skipped, per `docs/servers.md`. Name it and spend nothing on it. +- **`macbookpro`** — the one host whose roles table could not be verified before the tag. Check it, then + proceed; do not skip it on that account. +- All five others were on `deploy/2026.08.23.04` when this was cut, so each is exactly one release behind. + +## `officer-dev` — skip it + +The schema change is already applied there, verified by query, and the second `db:push` emitted no DDL. +Restarting `officer` on the host running the rollout ends the rollout. Name it as skipped and say it is +current. diff --git a/package.json b/package.json index 7d1c09b9..8dcf9ebf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "officer", - "version": "2026.08.23.04", + "version": "2026.08.23.05", "private": true, "type": "module", "workspaces": [