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 (
lastSavedRefseeded from first value) — loading existing data does NOT trigger a write - On
valuechange → status'pending'→ afterdelayms 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:
| Status | Pill text |
|---|---|
| pending | (no pill, or subtle) |
| saving | Saving… |
| saved | Saved |
| error | Save 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.
| Sheet | Subcomponent |
|---|---|
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:
- Flush any pending autosave timers (so nothing in-flight is lost)
- 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 refactor7d18acb— MorningCheckinSheet (mobile), EveningReflectionSheet summary, desktop standup page parent-level orchestration
Related
- evening-checklist — Primary consumer (4 free-text prompts)
- mobile-day-rituals — Sheets using this pattern
- mobile-native-feel — Surrounding mobile architecture