| 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('settings', s),
getTasks: (): Task[] => get<Task[]>('tasks', []),
setTasks: (tasks: Task[]): void => set('tasks', tasks),
getSessions: (): Session[] => get<Session[]>('sessions', []),
addSession: (session: Session): void => {
const sessions = storage.getSessions();
sessions.push(session);
set('sessions', sessions);
},
getTimerState: (): TimerState | null => get<TimerState | null>('state', null),
setTimerState: (state: TimerState): void => set('state', state),
clearTimerState: (): void => localStorage.removeItem(PREFIX + 'state'),
};
function defaultSettings(): Settings {
return {
pomodoroDuration: 25,
shortBreakDuration: 5,
longBreakDuration: 15,
longBreakInterval: 4,
autoStartBreaks: true,
autoStartPomodoros: false,
tickSound: false,
alarmSound: true,
alarmVolume: 0.7,
desktopNotifications: true,
theme: 'system',
};
}
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.type !== 'pomodoro' || s.interrupted) return false;
const key = dateKey(s.completedAt);
if (range === 'today') return key === todayKey;
if (range === 'week') {
const d = new Date(s.completedAt);
const diffDays = (now.getTime() - d.getTime()) / 86400000;
return diffDays < 7;
}
if (range === 'month') {
return s.completedAt.slice(0, 7) === todayKey.slice(0, 7);
}
return true;
});
const byDay: Record<string, number> = {};
let totalFocusMinutes = 0;
for (const s of filtered) {
const key = dateKey(s.completedAt);
byDay[key] = (byDay[key] ?? 0) + 1;
totalFocusMinutes += s.durationMinutes;
}
return {
totalPomodoros: filtered.length,
totalFocusMinutes,
streak: currentStreak(sessions),
byDay,
};
}
export function currentStreak(sessions: Session[]): number {
const completedDays = new Set(
sessions
.filter(s => s.type === 'pomodoro' && !s.interrupted)
.map(s => dateKey(s.completedAt)),
);
let streak = 0;
const today = new Date();
for (let i = 0; i < 366; i++) {
const d = new Date(today);
d.setDate(today.getDate() - i);
const key = d.toISOString().slice(0, 10);
if (completedDays.has(key)) {
streak++;
} else if (i > 0) {
break;
}
}
return streak;
}
export function buildHeatmap(sessions: Session[], weeks: number): number[][] {
const byDay: Record<string, number> = {};
for (const s of sessions) {
if (s.type === 'pomodoro' && !s.interrupted) {
const key = dateKey(s.completedAt);
byDay[key] = (byDay[key] ?? 0) + 1;
}
}
const today = new Date();
const grid: number[][] = [];
for (let w = weeks - 1; w >= 0; w--) {
const week: number[] = [];
for (let d = 0; d < 7; d++) {
const date = new Date(today);
date.setDate(today.getDate() - (w * 7) - (6 - d));
const key = date.toISOString().slice(0, 10);
week.push(byDay[key] ?? 0);
}
grid.push(week);
}
return 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);
}
export function alarm(volume: number): void {
const c = getCtx();
const times = [0, 0.3, 0.6];
for (const t of times) {
const osc = c.createOscillator();
const gain = c.createGain();
osc.connect(gain);
gain.connect(c.destination);
osc.frequency.value = 880;
osc.type = 'sine';
gain.gain.setValueAtTime(volume, c.currentTime + t);
gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + t + 0.25);
osc.start(c.currentTime + t);
osc.stop(c.currentTime + t + 0.25);
}
}
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);
} else {
setRemainingMs(saved.endTime - now);
engine?.resume();
}
}
}, []);