mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-23 19:06:33 +00:00
9ce52a8116
* feat(web): unparented filtering UI and persistence (TASK-2099) Phase 2 of PLAN-2095: consumes Phase 1's local-first `is_unparented` projection with a FilterBar chip, local filtering across list/board/table, URL + saved-view round-trip, parent-filter mutual exclusivity, restricted clearing (DR-2), and an explicit public-share skip of the reserved `$unparented` saved-view discriminator (DR-5). Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): close mutex/URL-collision/sync gaps in unparented filter (TASK-2099) Round-1 Codex findings: reserve the URL round-trip under the $unparented key (not a plain "unparented" param) so a grandfathered schema field literally named "unparented" can't collide; extend the parent/unparented mutex to the legacy "phase" alias and enforce it on whole-state URL/ saved-view application, not just the interactive handlers; keep the URL's $unparented param in sync with the chip's effective state in both directions, including when a pre-hydration default-view apply's intent only becomes effective once projection metadata resolves afterward. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): close pending-resync and optimistic-row gaps in unparented filter (TASK-2099) Round-2 Codex findings: gate unparented-metadata availability on localIndex.pendingResyncFor so a warm-cache boot's possibly-stale persisted capability bit can't expose the chip/filter before the follow-up reconcile confirms it (P1); force a URL re-sync whenever a restricted caller's stuck intent is cleared, not just on an effective- value transition, so a leftover $unparented URL param can't suppress the user's default saved view; assign activeFilters when a URL carries only the unparented pseudo-param (no field filters), closing a stale- parent-survives-the-mutex gap; and treat is_unparented=undefined as optimistically passing the filter so a just-created loose item doesn't flash out of view before its authoritative projection bit lands. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): narrow the pending-resync gate to confirmed restriction only (TASK-2099) Round-3 Codex findings: the round-2 pending-resync gate on unparentedMetadataAvailable was too blunt — it destroyed a legitimate URL/saved-view intent the instant it loaded mid-resync (even one about to confirm unrestricted a moment later), and could wedge the chip hidden forever after a live permission upgrade since pendingResync is cleared only by bootstrap()'s own reconcile loop, never by a deltaSync()-triggered resync. Split the concern: ordinary rendering/filtering availability no longer depends on pendingResync (consistent with the rest of the local-first model, which doesn't block on it either); a new, narrower unparentedConfirmedRestricted (metadata confirmed false AND no resync in flight) gates only the destructive "clear a stuck intent" action. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): let deltaSync's own reconcile loop clear pendingResync (TASK-2099) Round-4 Codex finding: pendingResync had no owner for a resync triggered mid-session by the collection page's SSE/periodic-sync-driven deltaSync (as opposed to bootstrap()'s own reconcile loop). Left stuck true forever, it meant a caller downgraded and later re-upgraded within the same session would never see the "confirmed restricted" transition fire, letting a stuck pre-downgrade unparented-filter intent silently reactivate on the upgrade instead of staying cleared. Add localIndex.markCaughtUp so deltaSync's own catch-up branches clear it, mirroring exactly what bootstrap()'s internal loop already does for the path it owns. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): close cross-navigation URL race and epoch-guard markCaughtUp (TASK-2099) Round-5 Codex findings: gate the unparented URL-sync effect on !metaLoading (not just indexReady) so navigating between collections in an already-hydrated workspace can't rebuild the destination URL using the previous collection's stale filter state before loadUrlFilters() has run for the new route; reset the transition- detection memo on route change so a value carried over from a previous collection can't mask a genuine effective-state transition in the new one; and epoch-guard localIndex.markCaughtUp so one caller's catch-up confirmation can't clear pendingResync out from under a different, concurrently-landed resync (SSE/periodic-sync/ bootstrap can all trigger one). Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra * fix(web): replace ordering-fragile metaLoading gate with an explicit flag (TASK-2099) Round-6 Codex finding: the unparented URL-sync effect's !metaLoading guard implicitly assumed it always re-evaluates strictly after the load-triggering effect resets metaLoading for a new route -- not a documented cross-effect guarantee -- and metaLoading also flips back to false on a failed load, where loadUrlFilters() never ran. Replace it with an explicit urlFiltersLoaded flag that loadCollection sets true only once loadUrlFilters() has genuinely completed for the current route, reset by an effect declared before the sync effect so Svelte's same-flush declaration-order guarantee, not an incidental timing coincidence, is what keeps them correctly sequenced. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
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