Skip to content

Time Tracker

The time tracker is no longer a mostly local Zustand feature. Its current design is split across three layers:

  1. persisted tracker entities in @life-manager/db
  2. runtime calculation and orchestration in @life-manager/shared
  3. web UI, browser notifications, and computed display state in apps/web
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 results

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.

src/stores/timeTrackerManagerStore.ts only stores:

  • activeTimerData
  • runningTimerCount

This store is for computed runtime snapshots such as formatted active time and money earned. It is not the authoritative timer store anymore.

Pomodoro preferences and UI-related timer preferences come from settingsStore, while persisted user settings still come from shared queries like useSettings().

FilePurpose
TimeTrackerManager.tsxLoads trackers and projects, adds trackers, handles DnD reorder, keeps running count in sync, scrolls to active and new timers
TimeTrackerInstance.tsxPer-tracker wrapper between manager and rendered variants
Big/TimeTrackerComponentBig*.tsxExpanded and compact card variants
Small/TimeTrackerComponentSmall.tsxMinimal narrow-shell representation
TimeTrackerButtons/*Start, stop, minimize, and related actions
TimeTrackerRow/*Reusable rows for time, money, memo, and Pomodoro state

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 runningTimerCount into 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.

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: updates document.title
  • onTimerDataUpdate: writes computed snapshot data into timeTrackerManagerStore
  • onTimerStop: clears runtime snapshot data and resets the tab title
  • onPomodoroPhaseTransition: triggers toast, sound, and browser notifications
  • onLongBreakComplete: shows the long-break completion notification flow

This split is important: browser behavior lives in web, but timer math and workflow do not.

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 data

So the timer-to-work-entry conversion is no longer a local-only transformation.

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

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.

Stay in apps/web/src/components/TimeTracker/ and related web hooks and stores.

Check the shared engine and package actions first:

  • @life-manager/shared/hooks
  • @life-manager/db/actions
  • @life-manager/db/types

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.