Upkeep Freshness — one model for "when was this last done vs. how often it should be"
Status: engine SHIPPED 2026-07-26 (upkeep-freshness.ts + golden test). The unified
UI wiring across routines + maintenance is the follow-on. Requested 2026-07-26.
The idea
A single, universal model for anything with a recurring upkeep cadence — personal (brush teeth ~every 12h, shower ~daily) and household (bathroom cleaning ~weekly, furnace filter ~90d, dryer-vent ~yearly). Each item has:
- last done — a timestamp (from a check-in, a "mark done" tap, or a logged event).
- target cadence — "ideally every N hours/days/weeks/months."
From those two, we derive a freshness % and a green / yellow / red status that drives dots, rings, and "due soon / overdue" copy everywhere.
The engine (apps/backend/src/services/upkeep-freshness.ts)
Pure + deterministic (now is always passed in), so it's unit-tested and safe to mirror
on the client. Golden test: __tests__/upkeep-freshness.test.ts.
computeFreshness({ lastDoneAt, intervalMs, now }) → {
status: 'fresh' | 'due_soon' | 'overdue' | 'unknown',
color: 'green' | 'yellow' | 'red' | 'gray',
pct, // remaining life 0–100 (100 = just done, 0 = due/overdue)
ratio, // elapsed / interval (≥1 overdue; Infinity if never done)
overdue, elapsedMs, nextDueAt,
}
intervalToMs(n, 'hour'|'day'|'week'|'month')
humanSince(elapsedMs) // "3d ago"
humanDue(nextDueAt, now) // "due in 2d" / "3d overdue"
Thresholds: overdue (red) at ratio ≥ 1; due-soon (yellow) at ratio ≥ 0.75 by
default, per-item overridable — brushing might only go yellow in its last 10%
(dueSoonFraction: 0.9), a furnace filter yellow at 60%. Never done → gray/unknown,
treated as needing attention.
Where it plugs in
- Household maintenance hotspots — the heart of the House Map's
family_maintenance_items(FAMILY_HOUSE_MAP.md): storelast_done_at+interval_n/interval_unit; render the room/hotspot in its freshness color; sort the "what's due" list byratiodescending (most overdue first). - Family routines — brushing/showers already log check-ins
(
family_routine_checkins); the most-recent check-in islast_done_at, the routine's cadence maps to an interval. A freshness dot on each routine turns "did we do it lately enough" into a glance — richer than the current binary done-today. - Anything else with a cadence — pet flea meds, car oil, plant watering, medication refills. The engine doesn't care what the item is.
Data shape (when the unified tracker is built)
Extend family_maintenance_items (and optionally a light interval on routines):
last_done_at timestamptz null
interval_n integer -- "every N"
interval_unit text -- hour | day | week | month
due_soon_frac real null -- per-item yellow threshold (default 0.75)
Marking done sets last_done_at = now(); freshness is computed on read, never stored
(so it's always current). A "done" event can also drop a row in a small history/log for
trend lines later.
Follow-on build
- ✅ Engine + golden test (shipped).
- ✅ Household maintenance items (migration 586,
family-upkeep.ts+/dashboard/family/upkeep) — furnace filter, dryer vent, bathroom deep clean, etc., with an "every N hours/days/weeks/months" interval, rendered green/yellow/red + %fresh, most-overdue first, "Done" resets the clock. Starter catalog + custom add. - ✅ Routine freshness dot (cadence → interval; last check-in → last_done) on the family dashboard routines.
- Room attachment (
room_idreserved onfamily_maintenance_items) once the House Map rooms exist; render hotspots on the map by freshness color. - A single unified "Upkeep" view listing personal + household items together by how overdue they are, and putting freshness dots on the wall.
Guardrails
Inherits FAMILY_INTENT.md: a red dot is information, not a guilt trip. No streak-shaming; "overdue" states are calm and actionable (one tap to mark done or reschedule). Household ops, not a cleanliness scoreboard for a person.