| name | focus-tracking |
| description | Implementation patterns for the pomodoro-dashboard timer engine, stats aggregation, and localStorage persistence layer. Use when building or modifying the core tracking logic. |
| triggers | ["timer engine","focus tracking","pomodoro session storage","stats aggregation","heatmap data","streak calculation"] |
focus-tracking Skill
When to use
Use this skill when implementing or modifying:
- The
TimerEngine class in src/lib/timer.ts
- The
storage.ts localStorage wrappers
- Session recording and stats aggregation in
src/lib/stats.ts
- The heatmap data builder
- The streak counter
TimerEngine implementation
export type Phase = 'pomodoro' | 'short_break' | 'long_break' | 'idle';
interface TimerEngineOptions {
onTick: (remainingMs: number) => void;
onComplete: () => void;
}
export class TimerEngine {
private intervalId: ReturnType<typeof setInterval> | null = null;
private endTime: number | null = null;
private remainingMs: number;
private paused = false;
constructor(
private durationMs: number,
private options: TimerEngineOptions,
) {
this.remainingMs = durationMs;
}
start(): void {
if (this.intervalId !== null) return;
this.endTime = Date.now() + this.remainingMs;
this.paused = false;
this.intervalId = setInterval(() => this.tick(), 250);
}
pause(): void {
if (this.intervalId === null || this.endTime === null) return;
this.remainingMs = Math.max(0, this.endTime - Date.now());
clearInterval(this.intervalId);
this.intervalId = null;
this.paused = true;
}
resume(): void {
this.start();
}
reset(): void {
this.pause();
this.remainingMs = this.durationMs;
this.paused = false;
this.endTime = null;
this.options.onTick(this.remainingMs);
}
skip(): void {
this.pause();
this.options.onComplete();
}
setDuration(ms: number): void {
this.durationMs = ms;
this.reset();
}
getState(): { remainingMs: number; paused: boolean; endTime: number | null } {
const remaining = this.endTime !== null && !this.paused
? Math.max(0, this.endTime - Date.now())
: this.remainingMs;
return { remainingMs: remaining, paused: this.paused, endTime: this.endTime };
}
private tick(): void {
if (this.endTime === null) return;
const remaining = this.endTime - Date.now();
if (remaining <= 0) {
clearInterval(this.intervalId!);
this.intervalId = null;
this.remainingMs = 0;
this.options.onTick(0);
this.options.onComplete();
} else {
this.options.onTick(remaining);
}
}
}
Storage helpers
import type { Settings, Task, Session, TimerState } from '../types/index.ts';
const PREFIX = 'pomo_';
function get<T>(key: string, fallback: T): T {
try {
const raw = localStorage.getItem(PREFIX + key);
return raw !== null ? (JSON.parse(raw) as T) : fallback;
} catch {
return fallback;
}
}
function set<T>(key: string, value: T): void {
localStorage.setItem(PREFIX + key, JSON.stringify(value));
}
export const storage = {
getSettings: (): Settings => get<Settings>('settings', defaultSettings()),
setSettings: (s: Settings): void => set(, s),
: (): [] => get<[]>(, []),
: (: []): (, tasks),
: (): [] => get<[]>(, []),
: (: ): {
sessions = storage.();
sessions.(session);
(, sessions);
},
: (): | get< | >(, ),
: (: ): (, state),
: (): .( + ),
};
(): {
{
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
};
}
Session recording
function recordSession(
type: Session['type'],
activeTaskId: string | null,
durationMinutes: number,
startedAt: Date,
interrupted: boolean,
): void {
const session: Session = {
id: crypto.randomUUID(),
type,
taskId: activeTaskId,
startedAt: startedAt.toISOString(),
completedAt: new Date().toISOString(),
durationMinutes,
interrupted,
};
storage.addSession(session);
if (type === 'pomodoro' && activeTaskId && !interrupted) {
const tasks = storage.getTasks();
const idx = tasks.findIndex(t => t.id === activeTaskId);
if (idx !== -1) {
tasks[idx].completedPomodoros += 1;
storage.setTasks(tasks);
}
}
}
Stats aggregation
import type { Session } from '../types/index.ts';
export interface StatsResult {
totalPomodoros: number;
totalFocusMinutes: number;
streak: number;
byDay: Record<string, number>;
}
function dateKey(iso: string): string {
return iso.slice(0, 10);
}
export function aggregateSessions(
sessions: Session[],
range: 'today' | 'week' | 'month' | 'all',
): StatsResult {
const now = new Date();
const todayKey = dateKey(now.toISOString());
const filtered = sessions.filter(s => {
if (s. !== || s.) ;
key = (s.);
(range === ) key === todayKey;
(range === ) {
d = (s.);
diffDays = (now.() - d.()) / ;
diffDays < ;
}
(range === ) {
s..(, ) === todayKey.(, );
}
;
});
: <, > = {};
totalFocusMinutes = ;
( s filtered) {
key = (s.);
byDay[key] = (byDay[key] ?? ) + ;
totalFocusMinutes += s.;
}
{
: filtered.,
totalFocusMinutes,
: (sessions),
byDay,
};
}
(): {
completedDays = (
sessions
.( s. === && !s.)
.( (s.)),
);
streak = ;
today = ();
( i = ; i < ; i++) {
d = (today);
d.(today.() - i);
key = d.().(, );
(completedDays.(key)) {
streak++;
} (i > ) {
;
}
}
streak;
}
(): [][] {
: <, > = {};
( s sessions) {
(s. === && !s.) {
key = (s.);
byDay[key] = (byDay[key] ?? ) + ;
}
}
today = ();
: [][] = [];
( w = weeks - ; w >= ; w--) {
: [] = [];
( d = ; d < ; d++) {
date = (today);
date.(today.() - (w * ) - ( - d));
key = date.().(, );
week.(byDay[key] ?? );
}
grid.(week);
}
grid;
}
Audio implementation
let ctx: AudioContext | null = null;
function getCtx(): AudioContext {
if (!ctx) ctx = new AudioContext();
if (ctx.state === 'suspended') void ctx.resume();
return ctx;
}
export function tick(volume: number): void {
const c = getCtx();
const osc = c.createOscillator();
const gain = c.createGain();
osc.connect(gain);
gain.connect(c.destination);
osc.frequency.value = 1000;
gain.gain.setValueAtTime(volume * 0.1, c.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.08);
osc.start(c.currentTime);
osc.stop(c.currentTime + 0.08);
}
(): {
c = ();
times = [, , ];
( t times) {
osc = c.();
gain = c.();
osc.(gain);
gain.(c.);
osc.. = ;
osc. = ;
gain..(volume, c. + t);
gain..(, c. + t + );
osc.(c. + t);
osc.(c. + t + );
}
}
Phase transition logic
function nextPhase(
currentPhase: Phase,
pomodoroCount: number,
longBreakInterval: number,
): { phase: Phase; newCount: number } {
if (currentPhase === 'pomodoro') {
const newCount = pomodoroCount + 1;
if (newCount % longBreakInterval === 0) {
return { phase: 'long_break', newCount };
}
return { phase: 'short_break', newCount };
}
return { phase: 'pomodoro', newCount: pomodoroCount };
}
Persisting timer state on each tick
useEffect(() => {
if (!engine) return;
const state: TimerState = {
phase,
endTime: engine.getState().endTime,
remainingMs: engine.getState().remainingMs,
paused: engine.getState().paused,
pomodoroCount,
activeTaskId,
};
storage.setTimerState(state);
}, [remainingMs]);
Restoring timer state on mount
useEffect(() => {
const saved = storage.getTimerState();
if (!saved) return;
setPhase(saved.phase);
setPomodoroCount(saved.pomodoroCount);
setActiveTaskId(saved.activeTaskId);
if (saved.paused || saved.endTime === null) {
setRemainingMs(saved.remainingMs);
} else {
const now = Date.now();
if (saved.endTime <= now) {
recordSession(saved.phase === 'pomodoro' ? 'pomodoro' : 'short_break',
saved.activeTaskId, 0, new Date(saved.endTime - saved.remainingMs), true);
const { phase: next } = nextPhase(saved.phase, saved.pomodoroCount, settings.longBreakInterval);
setPhase(next);
} {
(saved. - now);
engine?.();
}
}
}, []);