Files
pad/web
xarmian 5fb85534fd feat(web): collection page reads from localIndex (TASK-1357) (#504)
* 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.
2026-05-11 20:11:29 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

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