focus-tracking
Implementation patterns for the pomodoro-dashboard timer engine, stats aggregation, and localStorage persistence layer. Use when building or modifying the core tracking logic.
Best use case
focus-tracking is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Implementation patterns for the pomodoro-dashboard timer engine, stats aggregation, and localStorage persistence layer. Use when building or modifying the core tracking logic.
Teams using focus-tracking should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/focus-tracking/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How focus-tracking Compares
| Feature / Agent | focus-tracking | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Implementation patterns for the pomodoro-dashboard timer engine, stats aggregation, and localStorage persistence layer. Use when building or modifying the core tracking logic.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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
```typescript
// src/lib/timer.ts
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
```typescript
// src/lib/storage.ts
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
```typescript
// Called when a pomodoro completes normally
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);
// Increment task completedPomodoros if linked
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
```typescript
// src/lib/stats.ts
import type { Session } from '../types/index.ts';
export interface StatsResult {
totalPomodoros: number;
totalFocusMinutes: number;
streak: number;
byDay: Record<string, number>; // ISO date -> count
}
function dateKey(iso: string): string {
return iso.slice(0, 10); // YYYY-MM-DD
}
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) {
// Allow missing today (session not yet completed)
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();
// Align to the start of the week grid
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
```typescript
// src/lib/audio.ts
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
```typescript
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 };
}
// break -> pomodoro
return { phase: 'pomodoro', newCount: pomodoroCount };
}
```
## Persisting timer state on each tick
```typescript
// In the React timer component, on every 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
```typescript
useEffect(() => {
const saved = storage.getTimerState();
if (!saved) return;
setPhase(saved.phase);
setPomodoroCount(saved.pomodoroCount);
setActiveTaskId(saved.activeTaskId);
if (saved.paused || saved.endTime === null) {
// Restore paused state
setRemainingMs(saved.remainingMs);
} else {
// Timer was running - check if it expired while away
const now = Date.now();
if (saved.endTime <= now) {
// Session would have completed; record as interrupted
recordSession(saved.phase === 'pomodoro' ? 'pomodoro' : 'short_break',
saved.activeTaskId, 0, new Date(saved.endTime - saved.remainingMs), true);
// Advance to next phase
const { phase: next } = nextPhase(saved.phase, saved.pomodoroCount, settings.longBreakInterval);
setPhase(next);
} else {
// Resume with correct remaining time
setRemainingMs(saved.endTime - now);
engine?.resume();
}
}
}, []);
```Related Skills
Skill: Uptime Monitoring
## Overview
Skill: Status Page
## Overview
Skill: unit-conversion
## Overview
Skill: recipe-scaler
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".
poll-builder
Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.
Skill: personal-finance
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose
Skill: Pastebin Core
## Purpose