mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 22:10:25 +00:00
53c567dd4a
* fix(web): the sync cursor is only ever a server time taken before the reads it vouches for (BUG-3207) Three cursor writes used the client's wall clock: the initial value, a failed seed, and markSynced() after every successful full reload. A client running ahead of the server by D skipped every change committed in that window, on the hottest of the three paths. markSynced also stamped at the END of the reload, which skips a change committed between the reads and the stamp even with a perfect clock. - serverClock.ts estimates the server clock from response Date headers, projected by monotonic elapsed time, minus 1 s. It errs early only: an early cursor re-delivers (a duplicate), a late one skips (a miss). - stamp() is taken BEFORE a full reload's reads and passed to markSynced(stamp). With no response seen yet it asks /changes once. - A failed seed writes nothing. An unseeded cursor (0) answers full_refresh instead of asking since=0. - The seed's since uses the estimate, so a client running ahead still gets the changes between its page load and the seed. - ItemDetail no longer calls markSynced (lead ruling): a single-item reload cannot vouch for the workspace cursor. The layout is the only writer. Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ * test(web): re-sign the ItemDetail fence rows the markSynced removal moved, and state what the duplicate legs measure (BUG-3207) The AST gate hashes each async unit with its enclosing onMount, so removing the sync callback's markSynced call moved both the SSE and the sync row. Both were re-read against the new code: the SSE callback is untouched, and the sync change removes one synchronous call after the existing fence, adding no await and no commit. Both rows hold, and their hashes are updated. The reconcile-duplicate legs now say what mutants measured: the same-row duplicate is idempotent by content (no guard mutant can redden it), and the late older copy is held by the cursor-floor and existing-seq guards together. Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ * fix(web): the server-clock estimate is a whole millisecond, so /changes accepts the cursor it stamps (BUG-3207) performance.now() is fractional, and handleGetChanges parses `since` with ParseInt. A stamp taken from the estimate became a fractional cursor, every /changes asked from it answered 400, and each sync degraded to a full reload for the rest of the tab. Measured on the e2e trail: 11 of 11 first-attempt failures of bug-2539-sync-window sent a fractional since after the archive, 0 of 49 passes did. Floor rather than round, so the estimate still never runs ahead. The cursor model's /changes mock now refuses a non-integer since the way the server does. Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ * fix(web): a sync signal that beats the seed waits for it instead of reloading the workspace (BUG-3207) An unseeded cursor answers full_refresh, and the SSE connect and the first events routinely arrive while the seed request is still out. Each of those signals cost a whole-workspace reconcile where main asked one incremental /changes: 77 of 160 instrumented page loads under load. A signal reaching an unseeded cursor now awaits the in-flight seed and asks from its server_time, which is taken before the seed's reads, so the pass covers everything the seed's own delta did not. A tab whose seed failed still answers full_refresh. bug-2539-sync-window: the oracle now awaits every /changes issued after the archive before reading the record. One issued after the archive but not yet answered at check time read as "nothing carried it". Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ * fix(web): an early sync signal waits for the seed at most SEED_WAIT_MS, and a failed seed still ends in full_refresh (BUG-3207) The API client has no request timeout, so an unbounded wait on a seed that never answers held the signal, and the sync lock with it, forever. The wait is bounded at 5 s against a measured seed latency of p50 160 ms / max 874 ms at 8 workers. Two legs pin the lead's question: a FAILED seed and a HUNG seed each leave the awaited signal answering full_refresh, never dropped. bug-2539-sync-window: a non-seed /changes that carried the deletion passes whenever the client sent it. Client send time was the wrong test: a response that carries the deletion was read after the archive committed. Negative control: with ItemsModifiedSince comparing deleted_at with > instead of >=, 20 of 20 same-second runs fail. Claude-Session: https://claude.ai/code/session_01UcaMsmoCt8u7VpJhCNxiJJ
Pad Web UI
SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.
Development
npm install
npm run dev # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build # Production build to build/
npm run check # Type checking with svelte-check
When developing, run the Go backend separately with make dev from the project root.
Building for Production
Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.
Stack
- Svelte 5 with runes (
$state,$derived,$effect) - SvelteKit 2 with
adapter-static(SPA mode) - Tiptap block editor with markdown round-trip
- svelte-dnd-action for drag-and-drop in board/list views
- SSE for real-time updates
- TypeScript throughout
Structure
src/
routes/ SvelteKit pages
+layout.svelte App shell (sidebar + main)
+page.svelte Landing/redirect
[workspace]/
+page.svelte Dashboard (collections, phases, activity)
+layout.svelte SSE connection per workspace
[collection]/
+page.svelte Collection view (board/list)
[collection]/[item]/
+page.svelte Item detail + editor
conventions/ Purpose-built conventions page
playbooks/ Purpose-built playbooks page
settings/ Workspace settings
lib/
api/client.ts HTTP API client
components/
layout/ Sidebar, navigation
editor/ Tiptap editor, raw markdown editor
fields/ FieldEditor, relation picker
items/ ItemCard, ItemDetail
collections/ BoardView, ListView
common/ StatusBadge, badges, modals
search/ CommandPalette
stores/ Svelte 5 reactive stores
workspace.svelte.ts Workspace state
collections.svelte.ts Collection + item state
ui.svelte.ts Sidebar, mobile state
types/index.ts TypeScript types and constants
app.css Global styles and design tokens