Skip to content

Commit 65de1b7

Browse files
committed
fix pomodoro time management
1 parent bf9e640 commit 65de1b7

9 files changed

Lines changed: 342 additions & 138 deletions

File tree

server/lib/db.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ function ensureSchema() {
4343
CREATE TABLE IF NOT EXISTS pomodoro_sessions (
4444
start INTEGER NOT NULL,
4545
end INTEGER NOT NULL,
46-
breakEnd INTEGER
46+
breakEnd INTEGER,
47+
type TEXT
4748
);
4849
`);
4950

@@ -601,4 +602,11 @@ try {
601602
/* ignore - column already exists or other error */
602603
}
603604

605+
// Migration for pomodoro_sessions table - add missing type column
606+
try {
607+
db.prepare("ALTER TABLE pomodoro_sessions ADD COLUMN type TEXT").run();
608+
} catch (_) {
609+
/* ignore - column already exists or other error */
610+
}
611+
604612
export default db;

server/repositories/pomodoroSessionsRepository.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,41 @@ import db from "../lib/db.js";
33

44
export function loadPomodoroSessions(): PomodoroSession[] {
55
try {
6-
return db
7-
.prepare("SELECT start, end, breakEnd FROM pomodoro_sessions")
8-
.all();
6+
const rows = db
7+
.prepare("SELECT start, end, breakEnd, type FROM pomodoro_sessions")
8+
.all() as {
9+
start: number;
10+
end: number;
11+
breakEnd?: number;
12+
type?: string;
13+
}[];
14+
15+
const normalized: PomodoroSession[] = [];
16+
for (const r of rows) {
17+
// If it has a breakEnd (legacy), split it
18+
if (r.breakEnd) {
19+
// Work session
20+
normalized.push({
21+
start: r.start,
22+
end: r.end,
23+
type: "work",
24+
});
25+
// Break session
26+
normalized.push({
27+
start: r.end,
28+
end: r.breakEnd,
29+
type: "break",
30+
});
31+
} else {
32+
// Already normalized or just a work session without break or just manual
33+
normalized.push({
34+
start: r.start,
35+
end: r.end,
36+
type: (r.type as "work" | "break") || "work",
37+
});
38+
}
39+
}
40+
return normalized.sort((a, b) => a.start - b.start);
941
} catch {
1042
return [];
1143
}
@@ -14,10 +46,13 @@ export function loadPomodoroSessions(): PomodoroSession[] {
1446
export function savePomodoroSessions(sessions: PomodoroSession[]): void {
1547
const tx = db.transaction(() => {
1648
db.exec("DELETE FROM pomodoro_sessions");
49+
const stmt = db.prepare(
50+
"INSERT INTO pomodoro_sessions (start, end, type) VALUES (?, ?, ?)",
51+
);
1752
for (const s of sessions || []) {
18-
db.prepare(
19-
"INSERT INTO pomodoro_sessions (start, end, breakEnd) VALUES (?, ?, ?)",
20-
).run(s.start, s.end, s.breakEnd ?? null);
53+
// Ensure we only save 'work' or 'break' types
54+
const type = s.type === "break" ? "break" : "work";
55+
stmt.run(s.start, s.end, type);
2156
}
2257
});
2358
tx();

src/components/PomodoroTicker.tsx

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef } from "react";
1+
import { useEffect } from "react";
22
import { usePomodoroStore } from "@/stores/pomodoro";
33
import { usePomodoroHistory } from "@/hooks/usePomodoroHistory.tsx";
44

@@ -9,35 +9,29 @@ const PomodoroTicker = () => {
99
const mode = usePomodoroStore((state) => state.mode);
1010
const setStartTime = usePomodoroStore((state) => state.setStartTime);
1111
const startTime = usePomodoroStore((state) => state.startTime);
12-
const { addSession, endBreak } = usePomodoroHistory();
13-
const prevMode = useRef(mode);
12+
const { addSession } = usePomodoroHistory();
13+
14+
const finishedSession = usePomodoroStore((state) => state.finishedSession);
15+
const consumeFinishedSession = usePomodoroStore((state) => state.consumeFinishedSession);
1416

1517
useEffect(() => {
16-
if (lastTick) {
17-
const diff = Math.floor((Date.now() - lastTick) / 1000);
18-
if (diff > 1) {
19-
for (let i = 0; i < diff; i++) tick();
20-
}
21-
}
18+
// Tick handles drift internally now, so we just trigger it.
19+
// If we missed many ticks (bg tab), the next tick will catch up in one go.
2220
setLastTick(Date.now());
2321
const interval = setInterval(() => {
2422
tick();
25-
setLastTick(Date.now());
23+
// We don't strictly need setLastTick here as tick() does it,
24+
// but it doesn't hurt to keep local sync if needed for other things.
2625
}, 1000);
2726
return () => clearInterval(interval);
2827
}, [tick, setLastTick]);
2928

3029
useEffect(() => {
31-
if (prevMode.current === "work" && mode === "break" && startTime) {
32-
addSession(startTime, Date.now());
33-
setStartTime(undefined);
34-
}
35-
if (prevMode.current === "break" && mode === "work") {
36-
endBreak(Date.now());
37-
setStartTime(Date.now());
30+
if (finishedSession) {
31+
addSession(finishedSession.start, finishedSession.end, finishedSession.type);
32+
consumeFinishedSession();
3833
}
39-
prevMode.current = mode;
40-
}, [mode, startTime, addSession, endBreak, setStartTime]);
34+
}, [finishedSession, addSession, consumeFinishedSession]);
4135

4236
return null;
4337
};

src/components/PomodoroTimer.tsx

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
5353
startTime,
5454
} = usePomodoroStore();
5555
const { pomodoro, updatePomodoro, theme } = useSettings();
56-
const { addSession, endBreak } = usePomodoroHistory();
56+
const { addSession } = usePomodoroHistory();
5757
const { t } = useTranslation();
5858
const pipWindowRef = useRef<Window | null>(null);
5959
const [now, setNow] = useState(Date.now());
@@ -115,13 +115,7 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
115115
return () => clearInterval(i);
116116
}, [isPaused]);
117117

118-
useEffect(() => {
119-
if (!isPaused) return;
120-
const id = setInterval(() => {
121-
endBreak(Date.now());
122-
}, 1000);
123-
return () => clearInterval(id);
124-
}, [isPaused, endBreak]);
118+
125119

126120
const pauseDuration = pauseStart ? Math.floor((now - pauseStart) / 1000) : 0;
127121

@@ -194,45 +188,45 @@ const PomodoroTimer: React.FC<PomodoroTimerProps> = ({
194188
console.error("Failed to open floating window", err);
195189
}
196190
};
191+
// We rely on store.startTime for both work and break now.
192+
197193
const handlePause = () => {
198-
if (mode === "work" && startTime) {
199-
const now = Date.now();
200-
addSession(startTime, now);
201-
endBreak(now);
194+
if (startTime) {
195+
addSession(startTime, Date.now(), mode);
202196
setStartTime(undefined);
203197
}
204-
if (mode === "break") {
205-
endBreak(Date.now());
206-
}
207198
pause();
208199
};
209200

210201
const handleReset = () => {
211-
if (mode === "work" && startTime) {
212-
addSession(startTime, Date.now());
213-
}
214-
if (mode === "break") {
215-
endBreak(Date.now());
202+
if (startTime) {
203+
addSession(startTime, Date.now(), mode);
216204
}
205+
setStartTime(undefined);
217206
reset();
218207
};
219208

220209
const handleResume = () => {
221210
resume();
222-
endBreak(Date.now());
223-
if (mode === "work") {
224-
setStartTime(Date.now());
225-
}
211+
setStartTime(Date.now());
226212
};
227213

228214
const handleStartBreak = () => {
215+
// If we are currently working, save the work done so far
216+
if (mode === "work" && startTime) {
217+
addSession(startTime, Date.now(), "work");
218+
}
229219
startBreak();
220+
// Store sets startTime in startBreak()
230221
};
231222

232223
const handleSkipBreak = () => {
233-
endBreak(Date.now());
224+
// If we are currently in a break, save the break time taken so far
225+
if (mode === "break" && startTime) {
226+
addSession(startTime, Date.now(), "break");
227+
}
234228
skipBreak();
235-
setStartTime(Date.now());
229+
// Store sets startTime in skipBreak()
236230
};
237231

238232
useEffect(() => {

src/hooks/usePomodoroHistory.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,8 @@ const usePomodoroHistoryImpl = () => {
4848
save();
4949
}, [sessions, loaded]);
5050

51-
const addSession = (start: number, end: number) => {
52-
setSessions((prev) => [...prev, { start, end }]);
53-
};
54-
55-
const endBreak = (time: number) => {
56-
setSessions((prev) => {
57-
if (!prev.length) return prev;
58-
const last = { ...prev[prev.length - 1] };
59-
last.breakEnd = time;
60-
return [...prev.slice(0, -1), last];
61-
});
51+
const addSession = (start: number, end: number, type: "work" | "break") => {
52+
setSessions((prev) => [...prev, { start, end, type }]);
6253
};
6354

6455
const updateSession = (index: number, data: Partial<PomodoroSession>) => {
@@ -71,7 +62,7 @@ const usePomodoroHistoryImpl = () => {
7162
setSessions((prev) => prev.filter((_, i) => i !== index));
7263
};
7364

74-
return { sessions, addSession, endBreak, updateSession, deleteSession };
65+
return { sessions, addSession, updateSession, deleteSession };
7566
};
7667

7768
type Store = ReturnType<typeof usePomodoroHistoryImpl>;

src/hooks/usePomodoroStats.ts

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ export const usePomodoroStats = (): PomodoroStats => {
1010
const minutes = (start: number, end: number) =>
1111
Math.round((end - start) / 60000);
1212
const locale = i18n.language === "de" ? "de-DE" : "en-US";
13-
const totalWorkMinutes = sessions.reduce(
13+
14+
const workSessions = sessions.filter(s => s.type === 'work');
15+
const breakSessions = sessions.filter(s => s.type === 'break');
16+
17+
const totalWorkMinutes = workSessions.reduce(
1418
(sum, s) => sum + minutes(s.start, s.end),
1519
0,
1620
);
17-
const totalBreakMinutes = sessions.reduce(
18-
(sum, s) => sum + minutes(s.end, s.breakEnd ?? s.end),
21+
const totalBreakMinutes = breakSessions.reduce(
22+
(sum, s) => sum + minutes(s.start, s.end),
1923
0,
2024
);
21-
const totalCycles = sessions.length;
25+
const totalCycles = workSessions.length;
2226

2327
const todayStart = new Date();
2428
todayStart.setHours(0, 0, 0, 0);
@@ -32,21 +36,27 @@ export const usePomodoroStats = (): PomodoroStats => {
3236
yearStart.setMonth(0, 1);
3337
yearStart.setHours(0, 0, 0, 0);
3438

35-
const today = sessions.filter((s) => s.start >= todayStart.getTime());
36-
const week = sessions.filter((s) => s.start >= weekStart.getTime());
37-
const month = sessions.filter((s) => s.start >= monthStart.getTime());
38-
const year = sessions.filter((s) => s.start >= yearStart.getTime());
39+
const filterByDate = (arr: typeof sessions, date: Date) => arr.filter(s => s.start >= date.getTime());
40+
41+
const todayWork = filterByDate(workSessions, todayStart);
42+
const todayBreak = filterByDate(breakSessions, todayStart);
43+
44+
// For lists, we just use all sessions but formatted
45+
const todayAll = filterByDate(sessions, todayStart);
46+
const weekAll = filterByDate(sessions, weekStart);
47+
const monthAll = filterByDate(sessions, monthStart);
48+
const yearAll = filterByDate(sessions, yearStart);
3949

4050
const fmt = (t: number) =>
4151
new Date(t).toLocaleTimeString(locale, {
4252
hour: "2-digit",
4353
minute: "2-digit",
4454
});
4555

46-
const todayData = today.map((s) => ({
56+
const todayData = todayAll.map((s) => ({
4757
time: `${fmt(s.start)}-${fmt(s.end)}`,
48-
work: minutes(s.start, s.end),
49-
break: minutes(s.end, s.breakEnd ?? s.end),
58+
work: s.type === 'work' ? minutes(s.start, s.end) : 0,
59+
break: s.type === 'break' ? minutes(s.start, s.end) : 0,
5060
}));
5161

5262
const aggregateBy = (
@@ -67,40 +77,38 @@ export const usePomodoroStats = (): PomodoroStats => {
6777
const d = new Date(s.start);
6878
const key = fmt(d);
6979
if (key in data) {
70-
data[key].work += minutes(s.start, s.end);
71-
data[key].break += minutes(s.end, s.breakEnd ?? s.end);
80+
const m = minutes(s.start, s.end);
81+
if (s.type === 'work') data[key].work += m;
82+
else data[key].break += m;
7283
}
7384
});
7485
return Object.keys(data).map((key) => ({ date: key, ...data[key] }));
7586
};
7687

7788
const weekData = aggregateBy(
78-
week,
89+
weekAll,
7990
(d) => d.toLocaleDateString(locale, { weekday: "short" }),
8091
7,
8192
"day",
8293
);
8394
const daysInMonth = new Date().getDate();
8495
const monthData = aggregateBy(
85-
month,
96+
monthAll,
8697
(d) => d.getDate().toString(),
8798
daysInMonth,
8899
"day",
89100
);
90101
const yearData = aggregateBy(
91-
year,
102+
yearAll,
92103
(d) => d.toLocaleDateString(locale, { month: "short" }),
93104
12,
94105
"month",
95-
);
106+
).map(d => ({ month: d.date, work: d.work, break: d.break }));
96107

97108
const todayTotals = {
98-
workMinutes: today.reduce((sum, s) => sum + minutes(s.start, s.end), 0),
99-
breakMinutes: today.reduce(
100-
(sum, s) => sum + minutes(s.end, s.breakEnd ?? s.end),
101-
0,
102-
),
103-
cycles: today.length,
109+
workMinutes: todayWork.reduce((sum, s) => sum + minutes(s.start, s.end), 0),
110+
breakMinutes: todayBreak.reduce((sum, s) => sum + minutes(s.start, s.end), 0),
111+
cycles: todayWork.length,
104112
};
105113

106114
const timeOfDay = {
@@ -110,7 +118,9 @@ export const usePomodoroStats = (): PomodoroStats => {
110118
night: 0,
111119
};
112120
sessions.forEach((s) => {
113-
const start = new Date(s.start);
121+
// Only count work for time of day distribution? Or both? Usually focus is on work.
122+
// Let's count both for "activity"
123+
const start = new Date(s.start);
114124
const h = start.getHours();
115125
const m = minutes(s.start, s.end);
116126
if (h >= 6 && h < 12) timeOfDay.morning += m;

0 commit comments

Comments
 (0)