Files
pad/web
xarmian 00a91dfcf4 feat(push): session targeting — target_session_id + delivered_sessions (TASK-2588) (#1108)
* watchevents: add session-targeted push delivery predicate

PLAN-2558 S5 (TASK-2588). Notification gains TargetSessionID,
evaluated in the existing per-connection KindPush predicate in
watchNotificationVisible alongside TargetUserID — one delivery path,
targeted is broadcast-with-a-predicate, no bus changes. Empty
TargetSessionID (the pre-S5 shape) still matches every one of the
target user's sessions.

* server: accept target_session_id on push, report delivered_sessions

PLAN-2558 S5 (TASK-2588). POST .../items/{slug}/push accepts an
optional target_session_id (an id from GET /api/v1/sessions) and the
response gains delivered_sessions — a prediction read from the S1
presence registry at push time, scoped to the caller's own
ListForUser(userID) so a vanished id and one belonging to a different
user are both an honest 200/0 with no existence oracle across users.
Omitting the field keeps the exact pre-S5 request/response shape.

* web: session picker in the push composer, targeted-miss handling

PLAN-2558 S5 (TASK-2588). PushToAgentDialog gains a target picker
(broadcast default + one option per live session), reusing the
presence read already fetched for the count — no second GET
/api/v1/sessions. Selecting a session passes target_session_id;
leaving it untouched keeps the exact pre-S5 3-argument push() call.
A targeted miss (delivered_sessions === 0) toasts "that session is
gone — refresh the list", drops the selection back to broadcast, and
re-polls presence instead of closing — zero delivery means nothing
was sent, so nothing is duplicated by resending.

* server: bound target_session_id, skip publish on a targeted miss

Codex round 1 fixes for TASK-2588:

- Cap target_session_id at 256 runes (400 over-cap) so an authenticated
  caller can't park arbitrary garbage in the bus's shared replay buffer;
  a registry-issued id (36 runes) can never hit this bound.
- Snapshot presence BEFORE publish instead of counting after: the old
  order raced a target disconnecting between publish and count, which
  could report delivered_sessions=0 on a push that had already landed
  once. A targeted push now skips the publish entirely when its id
  isn't in the pre-publish snapshot — session ids are per-connection
  and never reused, so a target absent now can never be matched later,
  making the 0 a guarantee rather than a race. Broadcast is unaffected
  (still publish-always, pre-publish count).

Strengthened the targeted-miss and cross-user tests to assert the bus
does not grow (not just that the notification fails to arrive
downstream) — verified this fails if the skip-on-miss guard is
reverted.

* push targeting: document the pushed ruling, fix stale picker selection, guard mixed-version responses

Codex round 2 dispositions for TASK-2588:

- pushed:true on a skipped publish is RULED, not a bug (dispatcher):
  moved the ruling from a test comment onto the contract itself —
  pushResponse.Pushed's own doc comment in Go, mirrored in the TS
  ItemPushResult doc comment.
- Fixed a real sharp edge: when a presence refresh drops the selected
  session, a <select> can visually fall back to "All connected
  sessions" while the bound value stays the stale id, so the wire
  would carry a dead target the UI no longer shows as selected.
  Added reconcileSelectedSession(), called at every point `sessions`
  is reassigned outside the fresh-open reset (a live poll, a failed
  read, and the staleness-expiry path).
- Guarded the mixed-version hazard with a cheap check, not capability
  negotiation (the deployment shape — web assets embedded in the
  server binary — bounds this to a transient stale tab, argument
  recorded in the comment): delivered_sessions is now optional on the
  wire type, and a targeted send whose response omits it entirely is
  treated as UNKNOWN (info toast, dismiss like a normal success) —
  never inferred as a confirmed miss.

Verified all three new/changed legs actually catch their regression
by temporarily reverting each fix and confirming the corresponding
test fails, then restoring.

* push targeting: fix stale publish-guarantee comments (codex round 3)

Two doc-comment remnants of round 2's skip-on-miss fix, both claiming
push unconditionally publishes:

- watchevents.KindPush's doc comment ("publishes exactly one of
  these") now notes handlePushToItem decides whether to publish at
  all, and points at TargetSessionID / pushResponse.DeliveredSessions
  for why.
- api.items.push()'s JSDoc in client.ts no longer claims a resolved
  promise means "published to the bus" unconditionally — a targeted
  miss resolves with delivered_sessions: 0 and nothing published.

Comment-only; no behavior change.
2026-08-15 14:52:25 -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

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