Phase 8a+b: the last REST routes to RPC, and the tasks table keeps only its own fields #14
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/phase-8a-routes-columns"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 8, steps a and b, of the DDD/CQRS refactor: the last REST routes move to RPC, and the
taskstable keeps only the task's own fields. Stacked on #13. Steps c (drizzle and better-sqlite3 →node:sqlite) and d (npm workspaces, a publishable SDK) follow as their own PRs.8a · The last REST routes
Moved to RPC, each a command or query, with events where state changes:
timecontext): the timer, time entries, booking a day.savedViews): saving, changing and removing a view.status), Reviews (setFinding), Timeline (removing a comment or an attachment).These state changes reach screens as
time.changed,views.changed,notification.readandstandup.created, projected from their events.What stays REST. Each route decodes its input with a Schema and runs one query or command, and each says who it is for.
/api/rpc.test/rest/electronRoutes.test.ts, and were compared with curl against a build of the base commit: identical, error bodies included, apart from random workflow node ids.LEGACY_API_ROUTESis now empty. Routes may no longer importserver/utils, drizzle, better-sqlite3 or zod. zod is gone from the server; the Claude Code plugin still uses it for the Claude SDK, so it stays inpackage.jsonuntil plugins get their own (8d).Behaviour changes
error.noFiles); before, it answered 200 with nothing attached.?limit=abcmeant 40 and a non-numeric id was a 404.8b · The tasks table keeps only the task's own fields
Each retired column now lives with what it describes. The repository composes them back under the old names and in the old field order (
server/database/taskRows.ts), so the views, the UI and Electron see exactly the same data.task_plans(new, theTaskPlansport)agent_sessions(SessionStore.forgetclears it on back-to-queue or a move to another project)task_workspaces(pathonly for a folder that isn't where the rule puts it)task_external_refstask_reviewsrounderror,started_atandfinished_atstay ontasks. They describe the task, not a session: the workflow and the board set them, and reports and the standup read them, so deriving them from sessions would change what you see.external_idandmr_descriptionstay too.Migrations
0017_task_planscreatestask_plansand fills it from the old columns.0018_task_partsmoves across what only the old columns knew. That includes a session row for each task whose sessions predate Phase 5, carrying its totals and resume id. It also adds a workspace row for every recorded workdir, and the missing issue references.0019_retire_task_columnsdrops the 16 columns. SQLite won't drop a foreign-key column, so the table is rebuilt: a new table, the rows copied with their ids, the old one dropped and the new one renamed. The id counter is kept, so deleted ids are never reused.Rebuilding
taskswith foreign keys on would cascade-delete every row that references it. So the migrator now turns foreign keys off while migrations are pending (and leaves the connection alone when nothing is pending). The rebuild refuses to run with them on, and fails if SQLite's foreign-key check finds more broken rows afterwards than before.Checks
/api/tasksand/api/tasks/[id]return before (base commit) and after (migrations 0017–0019) for all 19 tasks: 66,175 fields, 0 differences, byte-identical JSON. The copy covered plans, verdicts, sources, pre-Phase-5 sessions, costs and workdirs. Integrity and foreign-key checks were clean afterwards.Not checked yet
🤖 Generated with Claude Code
packages/{core,contracts,infra-sqlite,plugin-sdk} and plugins/* become workspaces (@kanbot/core, @kanbot/contracts, @kanbot/infra-sqlite, @kanbot/plugin-sdk, @kanbot/plugin-<id>), each with the dependencies it imports and nothing else: effect pinned at the repo's exact version, the workspace packages as "*" (npm links a workspace whose version satisfies the range; it has no workspace: protocol). Only the SDK is not private. The plugins take effect and the SDK as peers: the host supplies the one copy of each, which is also what a prebuilt plugin bundle leaves external. zod and the Claude Agent SDK leave the root for @kanbot/plugin-claude-code, the only code that imports them. A plugin's package version is its manifest's. Each package's exports point at its TypeScript sources: the app, Nitro, vitest and Storybook compile them as they did, through the workspace symlinks. Imports still use relative paths; the next commit switches them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Cookies are not scoped by port. The packaged app set its per-launch cookie (kanbot_local) on 127.0.0.1, and after the tray's "Open in browser" the system browser had it too: it then sent it to every server on 127.0.0.1, whatever the port, for instance a dev server an agent started from a cloned repository's code, which could log it (and drive Kanbot with it) or overwrite it with a Set-Cookie of its own. The shell now makes a name for this machine once per install, kanbot-<16 hex>.localhost (kept 0600 in the user-data folder), and the window, its cookie and the browser handoff all use it; the shell's own REST calls and boot poll stay on 127.0.0.1 with the Bearer header. The name goes to the server with the token over the message port (it is not secret, but must be known), and hostIsLoopback accepts exactly that name besides the loopback names; the server takes no name the shell could not have made (/^kanbot-[a-z0-9]{12,32}\.localhost$/). Nothing changes in how the server listens: it still binds 127.0.0.1. Browsers resolve *.localhost to this machine by themselves (Chromium, Firefox), and Safari asks macOS's resolver, which does too (getaddrinfo answers 127.0.0.1 and ::1 here), so the handoff needs no fallback to 127.0.0.1. Checked in headless Chrome against a server bound to 127.0.0.1: the name resolves, and a cookie set under it is not sent to 127.0.0.1 or localhost on another port. Remaining: the cookie still goes to the same name on another port, so a program that reads the name from the user-data folder and gets the browser to open it on its own port could still read the cookie; it is one more step than before, not a wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>vue-i18n reads @ as a linked message, so plugins.problem.noMetadata and undecorated failed to compile and took the dev page down with them. They are reworded rather than escaped as {'@'}, which the server's translate() would print verbatim. A test compiles every message, the plugins' included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Any code from Settings › Devices could pair a node, so a code offered for someone's phone could become a node the hub sends prompts to. An offer is now for a node or, by default, for a browser or a script (Devices.offerPairing takes an optional { kind }; no payload still works), and redeeming it as the other kind fails like a wrong code (it counts towards burning it; every failure is the same PairingRefused / 401). Settings › Devices' pairing card has a "For a node" switch: it asks for a node's code and shows the kanbot-node pair command to run there instead of the address and QR code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>A code offered for a desktop (Devices.offerPairing({ kind: 'desktop' })) pairs two devices in one transaction: the window's browser device (its token is the kanbot_device cookie, Secure over HTTPS) and the node it runs (its token in the answer: { deviceId, nodeDeviceId, nodeToken }). devices.companion_id (migration 0022) points each at the other. Revoking either revokes both and cuts both off. A code for another kind fails like a wrong one. Settings › Devices offers the three kinds and shows a desktop as one entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Over the utility process's parent port ({ type: 'kanbot-shell', action: 'open-connect' }), only from the desktop app's own window (a Local caller) and only when the shell started the server to talk over that port; anything else is ShellUnavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>