mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 01:53:33 +00:00
5fb85534fd
* feat(web): collection page reads from localIndex (TASK-1357)
Wire the collection page (web/src/routes/[username]/[workspace]/[collection]/+page.svelte) to the local-first read model.
- `items` is now `$derived` from `localIndex.getByCollection(ws, coll, { includeArchived: showArchived })`. The collection page no longer fires `/items-index` on every nav — bootstrap is idempotent and runs once per workspace per session.
- New `bootstrap` `$effect` calls `localIndex.bootstrap(ws, { userId })` when the workspace or signed-in user changes, picking up the warm-IDB / cold-/items-index flow from PLAN-1343 Phase 2.
- Mutations (`handleStatusChange`, `handleReorder`, `handleRestore`, `quickCreate`) call `localIndex.upsert(ws, item)` with the canonical post-API row; the derived `items` re-renders automatically.
- SSE handler now triggers `/items-changes` → `applyDelta` via a new `deltaSync` helper instead of refetching the whole collection. SSE merely says "something changed"; the local cursor pulls only the delta. TASK-1358 will refine this to per-event seq-stamped apply.
- `syncService.onSync` routes both incremental and full-refresh signals through the same `deltaSync` path. The legacy /changes payload is no longer threaded into the local store — the seq-cursor /items-changes is canonical.
- The plans cross-collection lookup for task relation labels reads directly from `localIndex.getByCollection(ws, 'plans')` — no extra request.
Parent: PLAN-1343. Depends on TASK-1355 + TASK-1356.
* fix(web): collection page loading + reactive plan labels (Codex round 1)
- [P2] deltaSync() now returns a boolean and the syncService.onSync
handler only calls markSynced() on a clean catch-up. A transient
/items-changes failure leaves the legacy cursor untouched so a
later tab-resume retries instead of pinning at "fresh".
- [P2] `loading` is now derived from BOTH the metadata fetch
(metaLoading) AND the localIndex bootstrap state. Without this,
non-empty collections briefly rendered the empty-state CTA while
items were still hydrating, and scroll-restore could be consumed
against an empty filteredItems list.
- [P3] `relationLabels` (plan-id → plan-title for task cards) is
now `$derived` over `localIndex.getByCollection(ws, 'plans')`
instead of a one-shot fetch in loadCollection. Plans flow into
the local store as they hydrate, so the badge stays correct
without a navigation refresh.
Parent: PLAN-1343.
* fix(web): always deltaSync + gate archive toast on success (round 2)
- [P2] syncService.onSync now runs deltaSync for ALL result types,
including 'caught_up'. SSE only delivers events, not delta data;
a previous incremental deltaSync failure won't recover without
a fresh fetch attempt. The localIndex cursor is independent of
syncService.lastSyncTime, and per-row seq guards make repeated
calls idempotent.
- [P3] handleBulkArchive now waits for deltaSync to succeed before
showing a definitive success toast. The server-side deletes are
already persisted; if the cache fetch fails, surface a softer
"queued / updating…" toast so the user knows the local view will
catch up. The deletes themselves are still real.
Parent: PLAN-1343.
* fix(web): optimistic local sort_order on reorder (Codex round 3)
[P2] handleReorder now upserts the row into the local index with
the new sort_order BEFORE awaiting the API. Otherwise ListView,
which calls onReorder without awaiting, resyncs its displayed groups
from the unchanged `items` prop the moment dragging ends and the
rows snap back to the old order until the network PATCH returns.
Clearing `seq: undefined` on the optimistic copy bypasses the
per-row seq guard so the real API response (with a higher seq)
wins on arrival without the guard rejecting it.
Parent: PLAN-1343.
* fix(web): deltaSync 401/403 + error state on bootstrap failure (round 4)
- [P1] deltaSync now resets the local index on 401/403, mirroring
the auth-error handling in localIndex.bootstrap. If workspace
access is revoked after the page is mounted, the cached rows
drop instead of staying visible until reload. Other errors stay
transient.
- [P2] localIndex.bootstrapState === 'error' is no longer
conflated with 'ready'. A new `indexError` derived gates a
dedicated error-state branch in the template with a Retry CTA;
the misleading "No items yet" empty state no longer fires on a
transient /items-index failure for a non-empty collection.
Parent: PLAN-1343.
* fix(web): deltaSync on page entry + error surface after revoke (round 5)
- [P1] The bootstrap effect now ALWAYS runs a deltaSync after the
bootstrap promise settles. Once localIndex is 'ready', bootstrap
itself no-ops — but an item the user created/updated elsewhere
(item detail page, dashboard, another tab) while this collection
was unmounted is still catchable via /items-changes. Without
this, returning to the collection page after creating an item
elsewhere could miss the new row until the next SSE event.
- [P2] After a 401/403 from /items-changes, deltaSync now sets a
`deltaSyncFailed` flag in addition to calling localIndex.reset.
The reset rolls bootstrapState back to 'cold' but the bootstrap
effect can't re-fire on the same wsSlug/userId, so without the
flag the page would pin at "Loading…" forever. The error-state
banner now triggers on EITHER indexError OR deltaSyncFailed and
the Retry CTA clears the flag before re-bootstrapping.
Parent: PLAN-1343.
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