Files
pad/web
xarmian f7bb735771 fix(web): state the cold rule positively; catch the epoch reset (TASK-2877)
Codex review round 5, two P1s, both about `localIndex.reset()` — the
sign-out / 403-purge / deleted-workspace path.

THE FLAG WAS THE WRONG WAY ROUND. `coldFailed` asked "did the last search
fail", and that was false in three states that are not answers at all:
before the first request, after a failure, and after a reset drops every
row while the query sits in the box. Each one read as "fine" and put a
create row on screen backed by nothing. Inverted to `coldAnswered` — set
in exactly one place, by the event that earns it, and cleared wherever the
answer stops describing what is in the box. A flag that must be cleared
everywhere is one that will be missed somewhere; this is the same defect
arriving twice (round 3 caught the failure case, round 5 the reset case)
because the polarity made silence indistinguishable from success.

THE EPOCH FENCE HAD TO BE TWO-SIDED. `upsert`'s own guard refuses a
captured epoch BELOW the current one, which catches a resync. But
`reset()` DELETES the workspace state and the next bootstrap starts a
fresh one at `scopeEpoch` 0 — so a captured 7 is not below 0, sails
through, and links a row minted under an identity that no longer holds.
`createRelationTarget` now requires equality. The residual is in the code
comment rather than papered over: a reset plus resyncs landing back on
exactly the captured number would compare equal, which an exposed reset
generation would catch and this does not.

Also dropped the `loading` term from `showCreate`. It and the per-query
`coldAnswered` reset were a redundant PAIR — each survived removal while
the other stood, which is one guard and one line that looks like a guard,
not defence in depth (this repo has a note about exactly that shape).
`coldAnswered` is the one kept: it states the rule (something
authoritative has answered FOR THIS QUERY) where `loading` is a UI state
that correlates with it.

Matrix: 24 mutants, all killed; baseline and restore both 85/85. Killing
the per-query reset needed `aria-expanded`, not the row's absence — with
`loading` still gating the MARKUP, `.picker-create` is missing either way
and asserting on it measures the branch instead of the rule. Third time
this suite has been fooled by that same separation.

Re-verified end to end in a real browser on this exact build: create row
offered for a non-matching query and keyboard-reachable; Enter created
COLO-6 "Chartreuse" in COLORS (colors 2 -> 3, cars unchanged) with
`status: approved` — the schema's declared default, which the "+ New"
`options[0]` heuristic would have gotten wrong; the car's field holds that
id; a second pass at the same text offers the existing row and no create;
Escape leaves the value untouched; no bare UUID anywhere on the page.
2026-09-03 22:14:47 +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
    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