Files
Hankan/server
MechaCat02 c2e1fc23fe feat(server): the sync endpoints, on Node 22 with no build step
Hono and pg, run under --experimental-strip-types, so the deployed thing
is the source. GET /api/sync?cursor=N pages rows above the cursor;
POST /api/sync upserts last-write-wins. Bearer token on everything under
/api; /health is open, for the container healthcheck.

Rows are stored generically — primary key as text, body as JSONB —
because the server never reads inside a row. It stores and orders them and
the client interprets them, which keeps the two schemas from having to
move in lockstep.

change_seq is bumped by a BEFORE UPDATE trigger rather than by the write
path. A row edited after a client last pulled would otherwise keep its old
sequence, sit below that client's cursor, and never be delivered; putting
it in the database means no future write path can forget.

The last-write-wins comparison is in the ON CONFLICT clause itself, so a
losing row is not written at all and does not bump change_seq — a
conflict does not become traffic for every other device.

test/sync/roundtrip.test.ts runs two clients against a real Postgres and
asserts what actually goes wrong in sync: that a fresh client's seeded rows
cannot overwrite the server's history (the artifact's bug, as an executable
test), that a delete propagates, and that dict.loadedBands never crosses
the wire. It skips without HANKAN_TEST_SERVER, so npm test still runs
anywhere.

POST /api/test/reset exists only when HANKAN_TEST_MODE=1, so it cannot be
reached on the Pi even if the token leaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:57:44 +02:00
..
2026-09-08 19:13:53 +02:00

server/ — not in this pass

PORT.md steps 46 land here: the sync API, the tutor SSE endpoint, and the Docker Compose deployment beside the Pi's existing Postgres and Caddy.

Nothing here yet, on purpose. Steps 13 give a fully working offline app with no server at all, and that is the version that gets used first.

What is already shaped for it:

  • every syncable table carries updated_at, and seeded rows are pinned to 0 (see app/src/db/writes.ts) — the artifact's clobbering bug cannot be expressed;
  • change_seq is deliberately absent: it is server-assigned and arrives with the sync layer;
  • the dictionary is fetched by URL from app/src/domain/dictionary.ts, so per-band deltas can come from the Pi instead of the bundle by changing a base URL;
  • the tutor goes through one Sample function (app/src/domain/stub-tutor.ts). The real endpoint implements the same contract — onText receives cumulative text, and an aborted turn stops billing — so only that file changes.

When the SSE endpoint lands, Caddy needs flush_interval -1 on the proxy or the stream buffers and the tutor appears to hang.