mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-10 23:15:40 +00:00
ef7eabaddc
* fix(web): admin modal reactive loop on open (PLAN-1542 follow-up)
User reported the modal froze the page on open. Root cause was the
UserSettingsForm hydration \$effect added in TASK-1551 — it read prop
fields then reassigned editOverrides to a fresh object and mutated
per-key inside the same effect, while the template's
{#each overrideFields} block subscribed via bind:value to those exact
keys. The reassign+mutate pattern thrashed the bind subscriptions
which scheduled the effect's tracking owner, looping until
effect_update_depth_exceeded.
Fixes (defense in depth):
1. UserSettingsForm hydration \$effect now gates on user.id only
(a primitive read with no proxy churn) and wraps all writes in
untrack(). The objects are built locally then assigned ONCE per
state var — no "reassign then mutate" pattern.
Side effect: form state isn't nuked by the parent's
modalUser = { ...modalUser, ...updated } spread after every save.
The form's own state is authoritative for unsaved edits; re-
hydration only happens when the modal swaps to a different user.
2. UserModal now mounts only the active tab's body. The earlier
{#each TABS} + hidden pattern instantiated all four tab
components on every modal open, amplifying the UserSettingsForm
loop and firing three lazy-fetch effects in parallel for tabs
the admin may never click. Tabs are now mount-on-active —
lazy fetch happens when the admin selects the tab.
3. Lazy-fetch tabs (Overview/Workspaces/Activity) now claim
fetchedForUserId BEFORE the await, not after. A re-trigger of
the gating effect (parent passes a new modalUser object with
the same id) can no longer race in and fire a duplicate
concurrent fetch. On failure, the claim is cleared so a
retry-via-reactivation still works.
Verified via svelte-check (0 errors) and npm run build.
* fix: address Codex review on modal reactive-loop fix
1. Keep UserSettingsForm mounted across tab switches (hidden toggle
rather than {#if}). Mount-on-active for the Settings tab meant an
admin who tabbed away to Workspaces and back lost unsaved overrides
and the typed-email disable input. The form has no fetch — its
hydration $effect is now gated on user.id so the original loop
stays fixed, and keeping it alive is cheap. Lazy-fetch tabs
(Overview / Workspaces / Activity) still mount-on-active so their
network calls fire only when needed.
2. UserOverviewTab now clears fetchedForUserId when EITHER metrics or
recent-items branch fails (was: only when both failed). A partial
failure left the failed half stuck on the next activation until
the admin force-refreshed the page. The successful half gets
re-fetched on retry, which is cheap.
* fix: Overview tab partial-failure retry is now explicit, not auto
Clearing fetchedForUserId on failure while the tab is still active
re-triggered the gating \$effect immediately — under a persistent
outage that meant a continuous refetch loop and the successful
half's data getting wiped on every iteration. fetchedForUserId now
stays set on failure; the failed branch renders an inline Retry
button that calls loadAll() directly.
* fix: apply the same retry-claim fix to Workspaces + Activity tabs
Codex caught the same auto-retry-loop pattern in UserWorkspacesTab and
UserActivityTab: clearing fetchedForUserId on failure while the tab is
active re-triggers the gating $effect → immediate refetch → repeat.
Both already render explicit Retry buttons (calling loadDetail() /
loadInitial() directly), so the claim can stay set on failure.
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