Debounced Autosave Pattern

For Agents

Eliminated explicit Save buttons across the standup ritual surfaces. Text fields now persist on idle (default 600ms) via useDebouncedAutosave. Phase-completion buttons (Complete morning, Close day) just stamp the phase flag — they don’t write the data themselves. Used on both mobile sheets and the desktop standup page.

The Hook

useDebouncedAutosave<T>(value, save, delay = 600) in web/src/lib/hooks/useDebouncedAutosave.ts.

type AutosaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'

Behavior:

  • Skips the initial mount (lastSavedRef seeded from first value) — loading existing data does NOT trigger a write
  • On value change → status 'pending' → after delay ms idle → status 'saving' → on success 'saved' → after a moment back to 'idle'
  • Save errors set status 'error' and surface via the status pill

Status Indicators

Small uppercase pill rendered above each form/field:

StatusPill text
pending(no pill, or subtle)
savingSaving…
savedSaved
errorSave failed

For multi-field forms, the parent computes the aggregate (worst-of) status across all field statuses and renders a single pill in the sheet header.

Field-Level Usage (mobile sheets)

Pattern: a small ChecklistField / MorningField subcomponent owns its own local value + autosave instance.

SheetSubcomponent
EveningChecklistSheet (mobile)ChecklistField
EveningChecklistCard (desktop)ChecklistField
MorningCheckinSheet (mobile)MorningField
EveningReflectionSheet (mobile)inline summary textarea

Each subcomponent calls useUpsertDatapoint directly. Idempotent on the (standup, config) upsert key — repeated writes update the same row, no duplicate rows.

Parent-Orchestrator Usage (desktop standup page)

pages/standup/index.tsx:

  • Morning intake — per-config Map<configId, NodeJS.Timeout> of timers; the page-level effect debounces each prompt’s value separately
  • Evening summary — single timer
  • Status indicators rendered above each form

Phase-Completion Button Behavior

Buttons stamp, don't save

“Complete morning” / “Close day” no longer write the prompt/summary data. Their job is to:

  1. Flush any pending autosave timers (so nothing in-flight is lost)
  2. Stamp the phase flag (morning_completed_at, evening_completed_at)

Data is already persisted by the autosave loop. The button is just the seal.

Idempotency

useUpsertDatapoint upserts on (standup, config) — repeated writes for the same prompt update one row, no duplicates regardless of how many keystrokes triggered saves. This is the foundation that makes debouncing safe to use on every keystroke.

Commits

  • f718425 — Hook + EveningChecklistSheet/Card refactor
  • 7d18acb — MorningCheckinSheet (mobile), EveningReflectionSheet summary, desktop standup page parent-level orchestration