Andet trin mod iOS-appen. Appen henter sin flade fra capacitor://localhost og
taler med den server, ejeren har skrevet ind, så hver anmodning er cross-origin.
- CORS på /api/v1 for appens origin og ingen andres. Ingen
Allow-Credentials: appen bærer sin session som bearer, aldrig som cookie.
Alle andre origins får ingen headere, præcis som før.
- POST /ws-ticket giver en engangsbillet, der lever 30 sekunder, og
/ws?ticket=… åbner socket'en med den. En browser kan ikke sætte en header
på et WebSocket-håndtryk, og tokenet selv i URL'en ville ligge i hver
proxy-log i månedsvis. En socket med billet springer origin-tjekket over,
fordi den ikke bærer nogen cookie, som en fremmed side kunne låne.
- Et håndtryk fra appens origin uden billet afvises. Prøven fandt hvorfor:
biblioteket sammenligner kun værtsnavne, og capacitor://localhost har samme
værtsnavn som en instans på http://localhost.
- /ping svarer med product, edition og api, så appen kan kende en Verdande
fra en routers login-side, før nogen er logget ind. Ingen version: /version
er bag et login med vilje.
Fem plantede fejl, hver fanget af sin egen prøve: CORS til enhver origin,
genbrugelig billet, billet uden udløb, app-origin på en cookie, og
Allow-Credentials.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The socket carried task events and nothing else, so a project or a label
created anywhere — another tab, another device, Claude through the
connector — did not appear until a reload. Tasks updated live and the
sidebar did not, which reads as the sidebar being broken rather than as a
gap in what is published.
Projects and labels go to the *user* rather than to a project. A project
that has just been created has no subscribers, because nobody was
watching a thing that did not exist a moment ago.
Labels are announced as "something changed" and re-read, rather than sent
as a payload. The sidebar wants each label's task count, which no single
event carries and which moves whenever a task gains or loses a label —
including through quick add, where "@regnskab" invents the label inside a
task write and no label event is sent at all. Comparing the label lists
on a task change is what catches that, without re-reading on every ticked
checkbox.
And projects now go through an upsert, because the same row arrives twice
— once as the response to the request that created it, once over the
socket. Appending both puts two ids into a keyed {#each}, which throws
and stops the sidebar rendering. The tasks already had that guard; adding
the project event without it reintroduced the identical bug, and the
smoke test caught it on the first run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the MVP surface: project and section CRUD, task CRUD with
sub-trees, drag-and-drop ordering, the quick-add endpoint over the
natural-language parser, Today and Upcoming, search, sharing with roles,
an activity log and a WebSocket for live updates.
Permission checks are route middleware rather than a first line in each
handler, so an endpoint is guarded by where it sits instead of by the
author remembering. Anything the caller may not see answers 404, never
403 — a 403 confirms the thing exists. Visibility is enforced inside the
list query, so another user's project cannot be reached by filtering or
searching at it.
Ordering is a float, so a drop writes one row instead of renumbering the
list. When repeated drops into one gap exhaust the precision, the
section is spread out again and the move retried — in a separate
transaction, since the attempt that discovered the problem rolls back.
The connection pool is not 1: a single connection makes the pool a lock,
and a query run while an earlier result set is still open then waits for
a connection it is itself holding. The store now closes its rows before
querying again, and the pool leaves that mistake slow rather than fatal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>