• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
ozanatgreenpt

ozanatgreenpt

thirdTimer

This version stores some data, so you can restart the app!
Remix of cricks_unmixed4u/thirdTimer
Public
Like
thirdTimer
Home
Code
11
.cursor
1
docs
2
src
3
.cursorrules
.vtignore
AGENTS.md
README.md
article.md
biome.json
deno.json
knowledge.md
Environment variables
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
docs
/
session-history.md
Code
/
docs
/
session-history.md
Search
…
session-history.md

Session History

Developer reference for how session history is written and calculated.

Overview

Session history records completed (or in-progress) work sessions so users can review past activity grouped by intent or time period.

Table schema

Defined in src/backend/database/migrations.ts:

CREATE TABLE session_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner_token TEXT NOT NULL, intent TEXT, -- nullable; the user's session intent string work_time REAL NOT NULL, -- total work minutes break_time REAL NOT NULL, -- total break minutes started_at TEXT NOT NULL, -- ISO-8601 timestamp ended_at TEXT NOT NULL, -- ISO-8601 timestamp break_fraction REAL NOT NULL -- the break fraction setting at write time );

Upsert key

UNIQUE INDEX idx_session_history_session ON session_history(owner_token, started_at)

(owner_token, started_at) uniquely identifies a session. The started_at value comes from TimerState.sessionStartedAt (set once on the first startWork from idle). Because sessionStartedAt is stable for the lifetime of a session, repeated writes update the same row via ON CONFLICT ... DO UPDATE.

Indexes

  • idx_session_history_owner(owner_token, ended_at DESC) — supports queries that list a user's history in reverse chronological order.

Three write paths

Session history is written from three different code paths. All three now include in-progress time (the current work or break block that hasn't been finalized into totalWorkTime/totalBreakTime yet).

1. Live upsert (backend)

Trigger: Every POST /api/state/:userId call — i.e., every debounced state save while the timer is running.

Location: src/backend/index.ts, inside the POST /api/state handler.

How times are calculated:

workTime  = state.totalWorkTime  + (now - state.workStartTime) / 60000     // if working
breakTime = state.totalBreakTime + min((now - state.breakStartTime) / 60000, state.savedBreakMinutes)  // if breaking

Conditions:

  • Only writes if ownerToken is present and state.sessionStartedAt != null
  • Only writes if workTime > 0

Data flow:

Frontend state change
  → debouncedSaveState()          [state-persistence.ts]
  → POST /api/state/:userId       [state-persistence.ts → backend]
  → saveSessionHistory()           [backend/index.ts → queries.ts]
  → UPSERT session_history         [queries.ts]

2. Reset session (frontend)

Trigger: User clicks "Reset Session".

Location: src/frontend/index.tsx, resetSession callback.

How times are calculated:

finalWorkTime  = state.totalWorkTime  + (now - state.workStartTime) / 60000     // if working
finalBreakTime = state.totalBreakTime + min((now - state.breakStartTime) / 60000, state.savedBreakMinutes)  // if breaking

The intent is captured from getSessionIntent() before the state is reset.

Conditions:

  • Only writes if finalWorkTime > 0

Data flow:

User clicks "Reset Session"
  → resetSession()                 [index.tsx]
  → archiveSession()               [history-api.ts]
  → POST /api/history              [history-api.ts → backend]
  → saveSessionHistory()           [backend/index.ts → queries.ts]
  → UPSERT session_history         [queries.ts]
  → TimerActions.resetSession()    [timer-actions.ts → state zeroed]

3. Day rollover (frontend)

Trigger: App initializes and detects that the stored timer_user_id is from a previous day.

Location: src/frontend/index.tsx, initialization useEffect.

How times are calculated:

The stale state is fetched from the backend using the old user_id. Then:

workTime  = staleState.totalWorkTime  + (now - staleState.workStartTime) / 60000     // if was working
breakTime = staleState.totalBreakTime + min((now - staleState.breakStartTime) / 60000, staleState.savedBreakMinutes)  // if was breaking

The intent is read from localStorage (getSessionIntent()), which still holds the value from the previous day.

Conditions:

  • Only triggers if the stored user ID fails isUserIdValidForToday()
  • Only writes if staleState.totalWorkTime > 0 or staleState.mode === "working"

Data flow:

App mounts
  → useEffect checks timer_user_id age     [index.tsx]
  → GET /api/state/:oldUserId               [fetch → backend]
  → archiveSession()                         [history-api.ts]
  → POST /api/history                        [history-api.ts → backend]
  → saveSessionHistory()                     [backend/index.ts → queries.ts]
  → UPSERT session_history                   [queries.ts]
  → localStorage.removeItem("timer_user_id") [index.tsx — forces new ID]
  → loadState() with fresh user_id           [state-persistence.ts]

In-progress time inclusion

All three write paths use the same pattern to include the current in-progress block:

let workTime = state.totalWorkTime; let breakTime = state.totalBreakTime; const now = Date.now(); if (state.mode === "working" && state.workStartTime) { workTime += (now - state.workStartTime) / 1000 / 60; } if (state.mode === "breaking" && state.breakStartTime) { breakTime += Math.min( (now - state.breakStartTime) / 1000 / 60, state.savedBreakMinutes, ); }

The break cap (min(elapsed, savedBreakMinutes)) ensures that break time beyond the earned allowance is not counted.

This pattern exists because totalWorkTime and totalBreakTime in TimerState only update at mode transitions. Without this addition, closing the browser mid-work or mid-break would lose the current block.

Data flow diagram

                         ┌──────────────────────┐
                         │   Frontend (browser)  │
                         └───────┬──────┬────────┘
                                 │      │
           ┌─────────────────────┘      └──────────────────────┐
           │                                                    │
    State changes                                    Reset / Day rollover
    (every ~500ms)                                             │
           │                                                    │
           ▼                                                    ▼
  POST /api/state/:userId                            POST /api/history
  {state, ownerToken,                                {ownerToken, intent,
   sessionIntent, ...}                                workTime, breakTime,
           │                                          startedAt, endedAt,
           │                                          breakFraction}
           ▼                                                    │
  ┌────────────────────┐                                        │
  │  Backend (Hono)    │◄───────────────────────────────────────┘
  │                    │
  │  Computes in-      │  (live upsert only; reset/rollover
  │  progress time     │   compute on the frontend)
  │                    │
  └────────┬───────────┘
           │
           ▼
  saveSessionHistory()
           │
           ▼
  ┌────────────────────┐
  │  SQLite            │
  │  session_history   │
  │  UPSERT on         │
  │  (owner_token,     │
  │   started_at)      │
  └────────────────────┘

Key source files

  • src/backend/database/migrations.ts — session_history table and index creation
  • src/backend/database/queries.ts — saveSessionHistory, getHistoryByIntent, getHistorySummary
  • src/backend/index.ts — POST /api/state (live upsert), POST /api/history (explicit archive)
  • src/frontend/index.tsx — resetSession callback, day-rollover useEffect
  • src/frontend/utils/history-api.ts — archiveSession (frontend → POST /api/history)
  • src/frontend/utils/state-persistence.ts — debouncedSaveState (frontend → POST /api/state)
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.