mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 19:32:10 +00:00
783e9ef0f8
Four review rounds running found the same bug class: a continuation
resuming after an await and writing state that belongs to a view the
user has already left. Each round fixed instances; the next found more.
The tripwire an earlier round set has been hit, so the invariant is
hoisted into one implementation instead of N call sites agreeing by
convention.
web/src/lib/attachments/viewFence.ts owns it now:
- viewIdentity(read) — the ONE place a component states what names its
view. Returns tokens carrying a SNAPSHOT of the parts, so a
continuation reads the workspace it was issued for off the token
rather than off the live prop. A missing part voids the whole key,
and a null key never matches — a half-identified view cannot pass a
fence.
- createFence(identity) — generation + identity. begin() coexists with
its siblings; restart() supersedes them; invalidate() ends them.
Used twice per surface, for fences 1 and 2.
- createPaintFence(identity) — fence 3, the paint-time entry check.
All three stay distinct: a prior round established that collapsing any
two loses either A→B suppression or same-item-Retry reconciliation.
Both consumers now build all three from a single identity declaration,
so no call site can restate a shorter one — which was the recurring
mistake (the workspace half kept going missing).
Two outstanding findings fixed alongside:
- StorageTab delete was not workspace-fenced: it used the live wsSlug
after its await, so an A→B switch mid-request let the success/404
handling toast and reload against B. It now takes the workspace off
the PAINTED identity (so the DELETE targets the row the user
actually clicked), refuses a click whose paint is already stale, and
fences the toast + reload. The broadcast stays ahead of the fence: a
global (workspace, id) side effect, not a write into this view.
- The strip's pendingUploads could resurrect externally deleted rows:
the buffer was retained indefinitely and no successful response ever
consumed it, so a deletion from another tab, followed by a load that
legitimately returned no row, merged the stale upload back in — and
kept doing so. A response is now treated as authoritative about the
entries the buffer already held when that request went OUT; entries
announced while it was in flight (the buffer's actual purpose) are
untouched.
No behaviour change from the refactor: all 592 existing tests pass
unmodified. 18 added — 14 unit tests on the module, 2 per fix. Every new
test mutation-tested.
Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
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
activity/ ActivityFeed
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