- Add LogClosedDayDelete and LogClosedWeekDelete to SyncStore
- Inject syncStore into EntryService; log Start, Stop, StopByID,
Update, CreateInterval, Delete, AutoStopStalledEntries
- Inject syncStore into DayService; log CloseDay, MarkDay, ReopenDay,
and the recomputeWeek closed-week upsert
- Inject syncStore into SettingsService; log Upsert, UpdateSettings,
DeleteSettings
- Add LogClosedWeek/LogClosedWeekDelete calls in WeekService.CloseWeek
and ReopenWeek
- Update main.go and all service test helpers for new constructor signatures
- All Go tests and 19 Vitest tests pass
- 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
- Add ErrFutureDay sentinel error
- CreateInterval: rejects if startDayKey > todayKey (400)
- Update: rejects if new start_time moves entry to a future day (400)
- Handler maps ErrFutureDay → 400 Bad Request for both endpoints
- Add TestCreateIntervalRejectsFutureDay
- Add TestUpdateRejectsMoveToFutureDay
- UI already gates this via dayCapabilities (canAddInterval=false,
canEditEntries=false for future days), but server now enforces it too
Settings configured mid-week (e.g. Thursday) have an effective_from
of that date. CloseWeek was looking up settings as-of Monday, which
predates the new settings row and fell back to the old default.
Now uses today's date for the settings lookup, so any settings change
made before closing the week is correctly reflected in expected_ms.
The naive formula used (jan4.Weekday() - time.Monday) produces -1
when Jan 4 is a Sunday (Weekday()==0), shifting the computed Monday
one week forward. 2026 is affected: Jan 4 is a Sunday, so every
week key in 2026 was mapped to the wrong 7-day range, causing
CloseWeek to look for closed_days on the wrong dates and finding
nothing — resulting in worked_ms=0 and a full -Nh delta.
Fix: use (weekday+6)%7 to get days-since-Monday (Mon=0…Sun=6),
which is always non-negative.
Adds table-driven TestWeekDayKeys covering 2024 (Thu), 2026 (Sun),
and 2023 (Wed) to prevent regression.
When a day is closed, re-closed, or reopened, DayService now
recomputes worked_ms and delta_ms on the closed week containing
that day (if the week is already closed). This prevents stale
delta values after editing entries and re-closing a day.
- DayService.recomputeWeek: sums worked_ms from all closed_days
in the week, updates closed_weeks row preserving expected_ms
- NewDayService now takes ClosedWeekStore
- WeekKeyForDayKey exported helper (used by DayService)
- TestWeekSnapshotUpdatesWhenDayReopened regression test
Previously any past workday without a closed_days record blocked week
close. Now only days that actually have entries require an explicit
close. Empty workdays count as 0h worked, which is reflected in the
weekly delta automatically.
- WeekService.CloseWeek: after finding no closed_days record, check
whether the day has any entries; only error if it does
- NewWeekService: takes EntryStore to support the above check
- Updated TestCloseWeekMissingDayFails to reflect the new semantic
(test now creates entries on Friday but leaves it unclosed)
CloseWeek was requiring every workday in the ISO week to have a
closed_days record, including days in the future. Now only workdays
up to and including today are checked; future workdays are skipped.
Adds TestCloseWeekMidWeek regression test.