Time Tracker
The time tracker is no longer a mostly local Zustand feature. Its current design is split across three layers:
- persisted tracker entities in
@life-manager/db - runtime calculation and orchestration in
@life-manager/shared - web UI, browser notifications, and computed display state in
apps/web
Architecture Overview
Section titled “Architecture Overview”TimeTrackerManager (web) -> loads `timeTrackers` + projects from `@life-manager/db/queries` -> add/reorder actions from `@life-manager/db/actions` -> renders one `TimeTrackerInstance` per persisted tracker
TimeTrackerInstance (web) -> calls `useTimeTracker(timer)` -> renders big/small variants and row components
useTimeTracker(timer) (web wrapper) -> injects queries, actions, settings, and browser callbacks -> delegates to `useTimeTrackerEngine()` from `@life-manager/shared`
useTimeTrackerEngine() (shared) -> computes active time, Pomodoro transitions, title updates, submission flow -> invokes package actions to persist resultsSource Of Truth
Section titled “Source Of Truth”Persisted truth
Section titled “Persisted truth”Timers themselves are stored as TimeTracker entities in the shared DB layer and queried via useTimeTrackers().
That means tracker records survive reloads and offline sync because they are part of the synchronized application data.
Ephemeral truth
Section titled “Ephemeral truth”src/stores/timeTrackerManagerStore.ts only stores:
activeTimerDatarunningTimerCount
This store is for computed runtime snapshots such as formatted active time and money earned. It is not the authoritative timer store anymore.
Preferences
Section titled “Preferences”Pomodoro preferences and UI-related timer preferences come from settingsStore, while persisted user settings still come from shared queries like useSettings().
Core Components
Section titled “Core Components”| File | Purpose |
|---|---|
TimeTrackerManager.tsx | Loads trackers and projects, adds trackers, handles DnD reorder, keeps running count in sync, scrolls to active and new timers |
TimeTrackerInstance.tsx | Per-tracker wrapper between manager and rendered variants |
Big/TimeTrackerComponentBig*.tsx | Expanded and compact card variants |
Small/TimeTrackerComponentSmall.tsx | Minimal narrow-shell representation |
TimeTrackerButtons/* | Start, stop, minimize, and related actions |
TimeTrackerRow/* | Reusable rows for time, money, memo, and Pomodoro state |
Manager Responsibilities
Section titled “Manager Responsibilities”TimeTrackerManager.tsx is the high-level orchestrator for the feature.
It currently handles:
- loading projects and trackers from shared queries
- adding trackers through
useTimeTrackerActions().executeAdd - preventing duplicate trackers per project through action results
- sorting trackers by running state and
order_index - drag-and-drop reordering via
@dnd-kit - auto-selecting or auto-creating a tracker for the active work project
- syncing
runningTimerCountinto the local runtime store - scrolling the UI to running or newly-added timers
That makes the manager responsible for list behavior and shell UX, not timer business rules.
useTimeTracker() Wrapper
Section titled “useTimeTracker() Wrapper”src/hooks/timeTrackerHooks/useTimeTracker.ts is the web adapter around the shared timer engine.
It composes:
- queries:
useAppointments()useWorkProjects()useSettings()
- actions:
useWorkTimeEntryActions()useAppointmentActions()useTimeTrackerActions()
- local preferences from
settingsStore - browser callbacks for title updates and notifications
- runtime snapshot updates in
timeTrackerManagerStore
The actual timer algorithm then runs inside useTimeTrackerEngine() from @life-manager/shared/hooks.
Browser Callbacks Injected Into The Engine
Section titled “Browser Callbacks Injected Into The Engine”The web layer gives the shared engine browser-specific side effects:
onTitleUpdate: updatesdocument.titleonTimerDataUpdate: writes computed snapshot data intotimeTrackerManagerStoreonTimerStop: clears runtime snapshot data and resets the tab titleonPomodoroPhaseTransition: triggers toast, sound, and browser notificationsonLongBreakComplete: shows the long-break completion notification flow
This split is important: browser behavior lives in web, but timer math and workflow do not.
Data Flow When A Timer Stops
Section titled “Data Flow When A Timer Stops”At a high level, the stop and submit flow is now:
User stops/submits tracker -> shared engine computes effective session times and rounding -> web wrapper calls package actions -> work time entry is created in shared collections -> linked appointment may be updated -> time tracker state is stopped in persisted data -> runtime snapshot is removed from `timeTrackerManagerStore` -> PowerSync syncs the resulting dataSo the timer-to-work-entry conversion is no longer a local-only transformation.
Pomodoro Handling
Section titled “Pomodoro Handling”Pomodoro behavior is a joint feature across the shared engine and web notifications.
Web contributes:
- preferences from
settingsStore - toast, browser, and sound notification functions from
pomodoroNotifications.tsx - Pomodoro row UI and settings modal
Shared engine contributes:
- phase timing
- phase transitions
- auto-start and auto-continue decisions
- auto-submit behavior after long-break cycles
Why timeTrackerManagerStore Still Exists
Section titled “Why timeTrackerManagerStore Still Exists”Even though timers are now persisted elsewhere, the local store still matters because it caches fast-changing computed values that are expensive or awkward to keep in the synced entity itself:
- formatted active time
- rounded display time
- money earned
- current running count for shell badges and indicators
This keeps the persisted tracker entity clean while still letting the UI update frequently.
Working On This Feature
Section titled “Working On This Feature”If you are changing UI only
Section titled “If you are changing UI only”Stay in apps/web/src/components/TimeTracker/ and related web hooks and stores.
If you are changing timer behavior
Section titled “If you are changing timer behavior”Check the shared engine and package actions first:
@life-manager/shared/hooks@life-manager/db/actions@life-manager/db/types
If you are adding a persisted timer field
Section titled “If you are adding a persisted timer field”Do not only patch the web store. You will usually need coordinated changes in:
- shared DB types and schema
- collection, query, and action logic
- shared timer engine
- web rendering and runtime mapping
That is the main architectural shift compared with older versions of the feature.