From 5fb85534fd963442beb167c319c4b302d6fad7bf Mon Sep 17 00:00:00 2001 From: xarmian Date: Mon, 11 May 2026 20:11:29 -0400 Subject: [PATCH] feat(web): collection page reads from localIndex (TASK-1357) (#504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .../[workspace]/[collection]/+page.svelte | 383 ++++++++++++------ 1 file changed, 254 insertions(+), 129 deletions(-) diff --git a/web/src/routes/[username]/[workspace]/[collection]/+page.svelte b/web/src/routes/[username]/[workspace]/[collection]/+page.svelte index d7cac256..e8a05ed2 100644 --- a/web/src/routes/[username]/[workspace]/[collection]/+page.svelte +++ b/web/src/routes/[username]/[workspace]/[collection]/+page.svelte @@ -1,7 +1,7 @@