Wrap each DayChip in a container-query context so the chip layout
responds to the actual space available rather than the viewport width.
Breakpoints (per-chip width):
- < 72px (mobile, 7 chips in ~320px): compact — label + date + progress bar + badges only
- ≥ 72px (tablet+): show worked duration text below the date number; slightly larger padding/font
- ≥ 100px (desktop, 7 chips in ~750px wide main): larger date number, bigger worked label
The .chip-slot div in week/+page.svelte is the flex child (flex: 1,
min-width: 2.8rem) that feeds width into the container; .chip-wrap
inside DayChip carries container-type: inline-size and fills the slot.
All GET calls in client.ts now fall back to Dexie when a network error
(TypeError) is caught, so pages render from cached data when the server
is unreachable:
- entries.list() → db.entries filtered by day_key range
- days.list() → db.closed_days filtered by day_key range
- weeks.list() → db.closed_weeks filtered by week_key range
- weeks.balance() → computed locally from closed_weeks + balance_adjustments
- balance.list() → db.balance_adjustments ordered by effective_at DESC
- settings.current() → db.settings_history, latest row with effective_from <= today
- settings.history() → db.settings_history ordered by effective_from DESC
Day/week close and reopen remain online-only (they require server-side
computation).
Add isOnline store (navigator.onLine + window online/offline events) and
an amber 'Offline — showing cached data' banner in +layout.svelte shown
whenever the store is false.
- Migration 003: adds logged_at to sync_log for TTL pruning; migrates
settings_history to UUID TEXT PK with updated_at column
- SyncStore: Prune() deletes rows older than 30d and writes a '_pruned'
marker at the boundary version; Pull() calls Prune lazily and returns
ErrSyncStale (410) when the client's since_version is behind the marker
- sync_handler.go: GET /api/sync/pull?since=N; POST /api/sync/push with
last-updated_at-wins conflict resolution for entries, balance_adjustments,
settings_history; closed_days/closed_weeks skipped (server-only mutations)
- router.go: passes entryStore, adjustmentStore, settingsStore to SyncHandler
- settings_store.go: UUID PK, updated_at column, Upsert() for push path
- settings_service.go: generates UUID on create, sets updated_at on update
- settings_handler.go: ID params changed from int64 to string
- domain.go: Settings.ID string, Settings.UpdatedAt added
- client.ts: all mutation methods catch TypeError (offline) and fall back
to Dexie write + outbox enqueue; crypto.randomUUID() for offline creates;
Settings.id type changed to string
- db.ts: Dexie v3 — settings_history key path changed to string UUID;
upgrade handler clears table for repopulation via pull
- sync.ts: real pushOutbox to POST /api/sync/push; pullChanges uses GET
with ?since=N; 410 triggers coldStart() + retry; coldStart() wipes all
tables and resets last_version
- 4 new Go store tests covering normal pull, stale client, empty prune,
client-ahead-of-marker; all tests pass (store + service, 19 Vitest)
Backend:
- ClosedWeekStore.SumDelta: single SQL aggregate returning total delta_ms and
row count across all closed_weeks
- WeekService.Balance: thin passthrough returning BalanceResult{TotalDeltaMs, ClosedWeekCount}
- GET /api/weeks/balance handler; route registered alongside /weeks list/close/reopen
- Tests: store-level SumDelta (empty + populated), service-level Balance (empty + 2 weeks)
Frontend:
- weeks.balance() added to API client
- History page: balance card at top, fetched in parallel with existing data
- Loading state shows '—'; once loaded shows formatDelta value in green/red/gray
- Shows 'across N closed weeks' count alongside the value
Backend:
- SettingsStore: Add GetByID, Update, Delete, Count methods
- SettingsService: Add UpdateSettings (validates same rules as Upsert),
DeleteSettings (guards against deleting the last row → 409)
- New sentinels: ErrSettingsNotFound, ErrLastSettingsRow
- Handler: PUT /api/settings/history/{id} → 200 updated row
DELETE /api/settings/history/{id} → 204 / 404 / 409
Frontend:
- API client: settings.update(id, body) and settings.delete(id)
- Settings page: history table gains edit (pencil) and delete (×) buttons
- Inline edit form expands in place within the table row
- Delete button disabled and hint shown when only one row remains
- maskLabel() helper shows workday names instead of raw bitmask
- After save/delete: full reload to reflect changes in 'current' section
- Today nav link targets /week?week=<currentWeek>&day=<todayKey>
- Today tab active: route=/week AND ?day===todayKey() (not just pathname)
- Week tab active: route=/week AND today tab not active
- Root / redirect updated from /today to /week?week=…&day=…
- /today route hard-deleted (src/routes/today/ removed)
- +layout.svelte imports todayKey/currentWeekKey for link generation
- weekKey and selectedDay driven by ?week=&?day= query params
- Bare /week canonicalizes via replaceState (adds week+day params)
- Chip clicks: replaceState (no history push, no scroll jump)
- Week prev/next: goto with history push (back/forward works)
- Default day when week changes: today if in week, else Monday
- Keyboard navigation: ArrowLeft/Right cycles through chips
- Selected chip scrolls into view on selection change
- DayDetail always rendered for selectedDay (not just today)
- detailCaps reactive on closedDaysMap — updates immediately after
close/reopen/mark without extra load()
- Chips now wired: selected prop, onclick handler
- DayChip tabindex: 0 for selected chip, -1 for others (roving tabindex)
- New DayDetail.svelte: self-contained day panel with full capability
gating (canStartStop, canAddInterval, canEditEntries, canMarkKind,
canCloseDay, canReopenDay)
- Closed-day state: banner + read-only entry list with 'Reopen day to
edit' hint; mark-kind buttons still available
- Close Day button disabled (with tooltip) when running entry exists
- Timer only ticks when dayKey === todayKey() and entry is running;
properly cleaned up on dayKey change and component destroy
- oninvalidate() callback: called after every mutation so parent
(week view) can refetch and update the chip strip
- /today route: refactored to thin wrapper using DayDetail
- Week page: renders DayDetail below summary when today is in the
displayed week; oninvalidate triggers full week reload
- New DayChip.svelte component: weekday label, date number, progress
bar (worked/expected), kind badge (H/V/S), closed checkmark, today
highlight, selected state, accessible role=tab/aria-selected
- Week page: replace days-grid with horizontal scroll-snap chip strip
- Per-day workedMs: uses closed_days.worked_ms when closed, else sums
open entries for that day (so in-progress work shows immediately)
- dailyExpectedMs: evenly split hours_per_week across workdays; 0 for
weekends (no progress bar rendered for non-workdays)
- Progress bar turns amber when worked > expected (overtime)
- weekEntries stored in state (was discarded after computing set);
daysWithEntries now derived from weekEntries
- Install vitest + jsdom
- Add test/test:watch scripts to package.json
- Add test:web and test:all tasks to mise.toml
- Add dayCapabilities() to utils.ts — single source of truth for
what actions are permitted per day (future/today/past, open/closed)
- Add DayCapabilities interface to utils.ts
- 11 unit tests: dayCapabilities (5 cases), weekDayKeys (3 cases),
isWorkday (3 cases)
closedWeek.delta_ms is a snapshot taken at close time and goes stale
if entries are edited or days are re-closed afterward. The summary
rows above already use the live totalWorkedMs - expectedMs; the
closed-week banner now uses the same expression.
Fetch entries for the week alongside days/weeks in the week view.
canCloseWeek now mirrors the server rule: every past workday that
has at least one entry must have a closed_days record. Days with
no entries are still skipped (they count as 0h implicitly).
The frontend was blocking week close until every workday had a
closed_days record, which no longer matches the backend's rules
(untracked days are implicitly 0h). Replace the all-workdays-closed
guard with a simple check: week has started (Monday ≤ today) and
is not already closed. The server returns a clear error if a day
with entries still needs closing.
Also fixes a pre-existing TS type narrowing error on currentSettings
and removes the now-unused .hint CSS rule.