Feature: Dashboard + Today Timeline (iOS)
Version: 1.3.1 Last Reviewed: 2026-02-18 Status: Approved
User Story
As a caregiver, I open the app and immediately see when the last feed/diaper/sleep was and can log a new one in 2 taps. I can also see today's full event history in the Today tab.
MVP Scope
Tab 1: Dashboard
- "Since last" elapsed time counters: feeding (with last side indicator), diaper, sleep
- Today's summary counts: total feedings, pee count, poop count, total sleep hours
- Quick-log cards: Feed, Diaper, Sleep, Note (tappable → opens bottom sheet)
- Active sleep indicator when timer is running
- Auto-refresh every 30 seconds + on foreground
Tab 2: Today Timeline
- Chronological list of today's events (newest first) across all types
- Each entry shows: time, type icon, key details
- Tapping an entry opens edit sheet
- Pull-to-refresh
-
- button in toolbar for logging from this tab (via
.menu— native popover, not confirmationDialog)
- button in toolbar for logging from this tab (via
NOT in MVP
- Charts or daily summary
- Widgets
- Sync status indicator (post-MVP #4)
- Grouping by time period (morning/afternoon/evening)
Dashboard Layout
┌──────────────────────────────┐
│ Baby Basics [⚙️] │ ← Gear icon → Settings
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ 🍼 Feed │ │ 🧷 Diaper │ │
│ │ 2h 15m ago │ │ 45m ago │ │
│ │ L │ │ │ │ ← "L" = last side was Left
│ └────────────┘ └────────────┘ │
│ ┌────────────┐ ┌────────────┐ │
│ │ 😴 Sleep │ │ 📝 Note │ │
│ │ 💤 1h 20m │ │ │ │ ← Note card: entry point only
│ └────────────┘ └────────────┘ │
│ │
│ ─── Today ─── │
│ 🍼 6 feeds │
│ 🧷 4 pee · 2 poop │
│ 😴 8h 30m total sleep │
│ 📝 3 notes │
│ │
├───────────┬──────────┬────────┤
│ Dashboard │ Today │Settings│
└───────────┴──────────┴────────┘
Dashboard Cards
Each card is a Button that opens a .sheet():
| Card | Icon | Primary Info | Secondary Info | Tap Action |
|---|---|---|---|---|
| Feed | 🍼 (drop.fill) | "2h 15m ago" | Side indicator (see below) | Opens feeding sheet |
| Diaper | 🧷 (arrow.triangle.2.circlepath) | "45m ago" | -- | Opens diaper sheet |
| Sleep | 😴 (moon.zzz.fill) | "3h ago" or "💤 1h 20m" (active) | -- | Opens sleep sheet |
| Note | 📝 (note.text) | "Note" | -- | Opens note sheet |
Feed Card Side Indicator
Derived from last_feeding.started_side and last_feeding.both_sides:
started_side=left, both_sides=false→ "L"started_side=right, both_sides=false→ "R"started_side=left, both_sides=true→ "L+R"started_side=right, both_sides=true→ "R+L"type=bottle→ no side indicator shown
Elapsed Time Display
- Format:
Xm ago(under 1h),Xh Ym ago(1-24h),Xd Xh ago(24h+) - Updates every 60 seconds via SwiftUI Timer
--when no events of that type exist yet
Active Sleep State
- Dashboard API returns
active_sleep: {id, start_time}(separate fromlast_sleep) - When
active_sleepis not null, the Sleep card shows:- Pulsing 💤 icon (subtle animation)
- "Sleeping: 1h 20m" (elapsed since
active_sleep.start_time, updating live) - Tapping opens the sleep sheet with "Wake Up" as primary action
- When
active_sleepis null, the Sleep card shows "Since last sleep" fromlast_sleep.end_time
Today's Summary
Below the cards, show aggregated counts:
- Total feedings count
- Pee count + poop count (separate)
- Total sleep in hours and minutes
- Total notes count
- "Today" defined by user's
day_start_timepreference
Suggested Side for Feeding Sheet
- Dashboard API returns
suggested_side: "left" | "right" | null - The feeding sheet uses this as the primary quick-tap button label
- Server computes this as opposite of last breast feeding's
started_side - null when no breast feedings exist (show both L and R as equal options)
Quick-Log Behavior
When a user taps a quick-action button (e.g., "Left" on feeding, "Poop" on diaper):
- Optimistic dismiss: Sheet dismisses immediately (no loading spinner in the sheet)
- Success banner: A banner appears at the bottom of the dashboard:
"Logged left breast"with an Edit button, for 10 seconds- Tapping "Edit" re-opens the sheet in edit mode (post-MVP, separate ticket)
- Banner auto-dismisses after 10 seconds
- Only one banner visible at a time (new action replaces previous)
- Network request fires in background after sheet dismisses
- On success: Snackbar remains until timeout (no change), dashboard refreshes
- On failure: Snackbar is replaced with a toast:
"Couldn't save · Will retry"(amber/warning color)- Request is added to the in-memory retry queue (see ios-app-shell.md)
- Toast auto-dismisses after 10 seconds
- Retried requests that eventually succeed trigger a silent dashboard refresh
- Retried requests that permanently fail (5 attempts exhausted) show a persistent toast:
"Failed to save [type] · Tap to retry"(red/danger color, does not auto-dismiss)
Success banner content per type:
- Feeding (quick):
"Logged left breast"/"Logged right breast"/"Logged both (L+R)" - Diaper:
"Logged pee"/"Logged poop"/"Logged pee + poop" - Sleep (start):
"Started sleep" - Sleep (wake):
"Ended sleep (Xh Ym)"
Empty States
When no events of a type exist yet:
Dashboard cards:
- Feed card: icon + "Feed" label +
"--"(no elapsed time, no side indicator) - Diaper card: icon + "Diaper" label +
"--" - Sleep card: icon + "Sleep" label +
"--" - Note card: icon + "Note" label (always shows just the label, no elapsed time)
Today's summary (no events today):
"0 feeds""0 pee · 0 poop""0h 0m total sleep"
Today timeline (no events today):
- Centered illustration-free message:
"No events yet today"with subtext"Tap a card on the Dashboard to log your first entry"
Brand new baby (first-ever launch after onboarding):
- All dashboard cards show
"--" - All summary counts show 0
- Timeline shows empty state message
- This is the expected state — no special "welcome" or tutorial overlay
Data Loading
- Single API call:
GET /children/:childId/dashboard - Auto-refresh every 30 seconds (SwiftUI
.taskwith Timer) - Refresh on
scenePhase == .active(app returns to foreground) - Loading state: skeleton/shimmer on cards
- Error state (first load, no data): retry button with message
"Couldn't load dashboard"+"Tap to retry" - Stale data state (refresh fails but ViewModel has data): keep showing existing data with subtle banner
"Offline · Showing last known data"
Today Timeline Layout
┌──────────────────────────────┐
│ Today [+] │ ← + opens .menu
│ │
│ 2:30 PM │
│ 🍼 Left breast · 15 min │
│ │
│ 1:45 PM │
│ 🧷 Poop │
│ │
│ 12:00 PM │
│ 😴 Nap · 2h 15m │
│ │
│ 10:30 AM │
│ 📝 Medication · Vitamin D │
│ │
│ (pull to refresh) │
├───────────┬──────────┬────────┤
│ Dashboard │ Today │Settings│
└───────────┴──────────┴────────┘
Timeline Entry Details
Each entry shows:
- Time: formatted in user's locale (2:30 PM or 14:30)
- Icon: type-specific SF Symbol
- Detail line: varies by type
- Feeding (breast, one side): "Left breast · 15 min"
- Feeding (breast, both sides): "Both (L→R) · 15 min"
- Feeding (bottle): "Bottle · 4 oz"
- Diaper: "Pee", "Poop", or "Pee + Poop"
- Sleep: "2h 15m" (duration) or "Started 8:00 PM" (active, no end)
- Note: "Category · First 30 chars of text..."
Timeline Interactions
- Tap entry: Opens edit sheet for that event (pre-populated)
- Pull to refresh: Reloads timeline data
- + button (toolbar, top-right): Opens
.menuwith Feed/Diaper/Sleep/Note (menus are for creation; confirmationDialogs are for destructive actions) - Data from
GET /children/:childId/timeline?date=today
Amount Display (Feeding)
- API returns
amount_ml(Float) - iOS converts to oz for display:
amount_ml / 29.5735, rounded to nearest 0.5 oz - Display as "4 oz" or "2.5 oz"
- Post-MVP: user preference for mL vs oz
Acceptance Criteria
- Dashboard loads in <2 seconds
- "Since last feeding: Xh Ym ago" is accurate and updates live (every 60s)
- "Since last diaper: Xh Ym ago" is accurate and updates live
- "Since last sleep: Xh Ym ago" is accurate and updates live
- Feeding card shows last breast side indicator (L, R, L+R, R+L)
- Active sleep shows "Sleeping: Xh Ym" with pulsing indicator
- Today's counts are accurate
- Tapping "Feed" card opens feeding bottom sheet
- Tapping "Diaper" card opens diaper bottom sheet
- Tapping "Sleep" card opens sleep bottom sheet
- Tapping "Note" card opens note bottom sheet
- Quick-tap action dismisses sheet immediately and shows success banner
- Success banner shows for 10 seconds with correct message per type and "Edit" button
- Tapping "Edit" re-opens sheet in edit mode (post-MVP)
- Network failure after quick-tap shows warning toast "Couldn't save · Will retry"
- Permanently failed requests show persistent error toast with "Tap to retry"
- Data from other caregivers appears within ~30 seconds (auto-refresh)
- Today tab shows chronological list of all events (newest first)
- Can tap a timeline entry to edit it
- + button on Today tab opens menu with 4 options (Feed/Diaper/Sleep/Note)
- Pull-to-refresh works on Today tab
- Dashboard refreshes when app returns to foreground
- Light and dark mode render correctly
- "--" shows when no events exist for a category
- Empty dashboard shows "--" on all cards and 0 counts
- Empty timeline shows "No events yet today" message
Test Cases
Dashboard
- Empty dashboard: New baby, no events → all cards show "--", counts show 0
- All data present: Events for all types → cards show correct elapsed times
- Live update: Wait 60 seconds → elapsed times update without manual refresh
- Active sleep: Start sleep timer → sleep card shows pulsing elapsed time
- Auto-refresh: Log from another device → data appears within 30 seconds
- Foreground refresh: Background app, return → data refreshes immediately
- Feeding side indicator (single): started_side=left, both_sides=false → card shows "L"
- Feeding side indicator (both): started_side=left, both_sides=true → card shows "L+R"
- Bottle feeding: Last feed was bottle → no side indicator shown
- Suggested side: Dashboard API suggested_side="right" → feeding sheet primary button is "Right"
- Today counts: Log 3 feedings, 2 pee diapers, 1 "both" diaper → shows "3 feeds · 3 pee · 1 poop"
Quick-Log & Banner
- Quick-tap dismiss: Tap "Left" on feeding → sheet dismisses immediately, banner shows "Logged left breast" with "Edit" button
- Edit action: Tap "Edit" within 10 seconds → re-opens sheet in edit mode (post-MVP)
- Banner timeout: Don't tap Edit → banner disappears after 10 seconds, entry persists
- Rapid logging: Quick-tap diaper, then quick-tap feeding → first banner replaced by second
- Background save: Server responds 201 while banner is showing → no visible change, dashboard refreshes
- Network failure toast: Quick-tap while offline → banner replaced with "Couldn't save · Will retry" (amber)
- Permanent failure toast: Retry exhausted (5 attempts) → persistent red toast "Failed to save feeding · Tap to retry"
- Retry success: Offline quick-tap → toast shown → connection restored → silent retry succeeds → dashboard refreshes
Empty States
- Brand new baby: Complete onboarding → dashboard shows "--" on all cards, 0 counts, timeline shows "No events yet today"
- Partial data: Only feedings logged → Feed card shows elapsed, other cards show "--"
- Empty timeline message: No events today → "No events yet today" with helpful subtext
Timeline
- Timeline sorting: Multiple events → newest at top
- Timeline tap to edit: Tap a feeding → edit sheet opens with current values
- Timeline + button: Tap + → menu → tap Feed → feeding sheet opens
- Pull to refresh: Pull down on timeline → spinner → data reloads
- oz conversion: API returns 120 mL → displays "4 oz"
Visual
- Dark mode: All cards, snackbars, and toasts readable in dark mode
- Error state: Network error on initial load → shows error view with "Couldn't load dashboard" + retry button
Boundaries
- No charts or trend analysis
- No grouping of timeline events by time period
- No infinite scroll / pagination on timeline (single day)
- No persistent offline cache (post-MVP #1 - SwiftData). In-memory stale data from the current session is shown with "Offline" banner on refresh failure.
- Amounts always displayed in oz for MVP (no unit toggle)
- Side indicator only shows for breast feedings, not bottle