diff --git a/css/index.css b/css/index.css
index 28fbfd3..abcffb5 100644
--- a/css/index.css
+++ b/css/index.css
@@ -1336,6 +1336,44 @@ body {
gap: 24px;
}
+.focus-stats-panel {
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.focus-stat-card {
+ background: var(--color-background-secondary);
+ border: 1px solid var(--color-border-secondary);
+ border-radius: var(--border-radius-md);
+ padding: 18px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+
+.focus-stat-label {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--color-text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.focus-stat-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--color-text-primary);
+}
+
+@media (max-width: 820px) {
+ .focus-stats-panel {
+ grid-template-columns: 1fr;
+ }
+}
+
.timer-container {
position: relative;
width: 240px;
@@ -4161,6 +4199,19 @@ body {
.tasks-section { flex: 1; overflow-y: auto; padding: 0 24px 24px; scroll-behavior: smooth; }
.tasks-section::-webkit-scrollbar { width: 6px; }
.tasks-section::-webkit-scrollbar-thumb { background: var(--color-border-secondary); border-radius: 10px; }
+.profile-section { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 24px; }
+.profile-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; }
+.profile-page-title { font-size: 24px; font-weight: 700; color: var(--color-text-primary); }
+.profile-page-subtitle { margin: 8px 0 0; color: var(--color-text-secondary); max-width: 640px; line-height: 1.6; }
+.profile-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
+.profile-card { background: var(--color-background-primary); border: 1px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); padding: 24px; box-shadow: var(--shadow-sm); }
+.profile-card h2 { margin: 0 0 16px; font-size: 16px; font-weight: 700; color: var(--color-text-primary); }
+.profile-field { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 14px; font-size: 14px; color: var(--color-text-secondary); }
+.profile-field-label { color: var(--color-text-tertiary); font-weight: 600; }
+.profile-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
+.profile-stat-value { display: block; font-size: 22px; font-weight: 700; color: var(--color-text-primary); margin-bottom: 4px; }
+.profile-summary-card p { margin: 0; color: var(--color-text-secondary); line-height: 1.8; }
+@media (max-width: 900px) { .profile-grid { grid-template-columns: 1fr; } }
.tasks-actions-bar {
margin-top: 18px;
display: flex;
@@ -5018,7 +5069,6 @@ body {
font-size: 14px;
transition: opacity 0.2s ease;
}
-
.subject-sidebar-item:hover .delete-subject-btn {
opacity: 1;
}
diff --git a/database.js b/database.js
index 33a1b17..311bf7f 100644
--- a/database.js
+++ b/database.js
@@ -33,6 +33,15 @@ function initDb() {
FOREIGN KEY (subject_id) REFERENCES subjects(id)
)`);
+ // Focus Sessions Table
+ db.run(`CREATE TABLE IF NOT EXISTS focus_sessions (
+ id TEXT PRIMARY KEY,
+ task_id TEXT,
+ duration_seconds INTEGER NOT NULL,
+ completed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (task_id) REFERENCES tasks(id)
+ )`);
+
db.all("PRAGMA table_info(tasks)", (err, rows) => {
if (err) return;
diff --git a/index.html b/index.html
index 543be76..0bfbb85 100644
--- a/index.html
+++ b/index.html
@@ -62,7 +62,7 @@
StudyPlan
@@ -191,6 +191,21 @@ StudyPlan
+
+
+
+
Sessions completed
+
0
+
+
+
Total focus time
+
0m
+
+
+
Current streak
+
0 days
+
+
@@ -207,6 +222,61 @@
StudyPlan
+
+
+
+
+
+
+ Account details
+
+ Username
+ StudyPlan User
+
+
+ Email
+ user@studyplan.app
+
+
+ Member since
+ June 2026
+
+
+
+
+ Study statistics
+
+
+ 0
+ Completed
+
+
+ 0
+ Pending
+
+
+ 0
+ Archived
+
+
+ 0
+ Subjects
+
+
+
+
+
+
+ Account overview
+ Your profile information and study statistics will update automatically as you use StudyPlan.
+
+
+
@@ -471,7 +541,9 @@ {
- if (e.target.checked) {
+ const isCompact = e.target.checked;
+ localStorage.setItem('studyplan_compact_view', isCompact);
+ if (isCompact) {
document.body.classList.add('compact-view');
} else {
document.body.classList.remove('compact-view');
diff --git a/js/app.js b/js/app.js
index 2bc3bce..b2c53ae 100644
--- a/js/app.js
+++ b/js/app.js
@@ -99,6 +99,10 @@ const downloadBtn = document.getElementById('download-btn');
const calendarDownloadBtn = document.getElementById('calendar-download-btn');
const newTaskBtn = document.getElementById('add-task-btn');
const labelFilterSelect = document.getElementById('label-filter');
+const focusStatsPanel = document.getElementById('focus-stats-panel');
+const focusTotalSessions = document.getElementById('focus-total-sessions');
+const focusTotalTime = document.getElementById('focus-total-time');
+const focusCurrentStreak = document.getElementById('focus-current-streak');
if (labelFilterSelect) {
labelFilterSelect.addEventListener('change', (e) => {
@@ -323,16 +327,16 @@ function loadTimerState() {
saveTimerState();
if (timeLeft <= 0) {
- clearInterval(timerInterval);
- timerInterval = null;
- localStorage.removeItem('focusTimerState');
+ clearInterval(timerInterval);
+ timerInterval = null;
+ localStorage.removeItem('focusTimerState');
- playCompletionSound();
- showBrowserNotification();
- Toast.show('Focus session complete!', 'success');
+ playCompletionSound();
+ showBrowserNotification();
+ Toast.show('Focus session complete!', 'success');
- resetTimer();
-}
+ resetTimer();
+ }
}, 250);
timerPauseBtn.classList.remove('hidden');
@@ -343,6 +347,7 @@ function loadTimerState() {
console.error('Failed to load timer state', err);
}
}
+
function getTimerColor(timeLeft, totalTime) {
const fraction = timeLeft / totalTime;
if (fraction <= 0.1) return '#ef4444';
@@ -393,6 +398,53 @@ function showBrowserNotification() {
}
}
+async function recordCompletedFocusSession(durationSeconds) {
+ await store.addFocusSession({
+ task_id: activeFocusTaskId || null,
+ duration_seconds: durationSeconds,
+ completed_at: new Date().toISOString()
+ });
+}
+
+function formatSessionDuration(seconds) {
+ const mins = Math.floor(seconds / 60);
+ const hrs = Math.floor(mins / 60);
+ const remainder = mins % 60;
+ if (hrs > 0) {
+ return `${hrs}h ${remainder}m`;
+ }
+ return `${remainder}m`;
+}
+
+function calculateFocusStreak(sessions) {
+ const daySet = new Set(
+ (sessions || []).map(s => new Date(s.completed_at).toISOString().substring(0, 10))
+ );
+ let streak = 0;
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ let cursor = new Date(today);
+
+ while (daySet.has(cursor.toISOString().substring(0, 10))) {
+ streak += 1;
+ cursor.setDate(cursor.getDate() - 1);
+ }
+
+ return streak;
+}
+
+function renderFocusStats() {
+ if (!focusStatsPanel) return;
+ const sessions = store.focusSessions || [];
+ const totalSessions = sessions.length;
+ const totalSeconds = sessions.reduce((acc, session) => acc + Number(session.duration_seconds || 0), 0);
+ const streak = calculateFocusStreak(sessions);
+
+ focusTotalSessions.textContent = totalSessions;
+ focusTotalTime.textContent = formatSessionDuration(totalSeconds);
+ focusCurrentStreak.textContent = `${streak} day${streak === 1 ? '' : 's'}`;
+}
+
function startTimer() {
if (timerInterval) return;
@@ -405,7 +457,7 @@ function startTimer() {
startTime = Date.now() - (timePassed * 1000);
saveTimerState();
- timerInterval = setInterval(() => {
+ timerInterval = setInterval(() => {
timePassed = Math.floor((Date.now() - startTime) / 1000);
timeLeft = TIME_LIMIT - timePassed;
@@ -421,6 +473,7 @@ function startTimer() {
playCompletionSound();
showBrowserNotification();
Toast.show('Focus session complete!', 'success');
+ recordCompletedFocusSession(TIME_LIMIT).then(() => renderFocusStats());
resetTimer();
}
}, 1000);
@@ -578,6 +631,89 @@ function renderFocusTasks() {
}
}
+function renderProfileSection() {
+ if (!profileSection) return;
+
+ const tasks = store.tasks || [];
+ const subjects = store.subjects || [];
+ const completedCount = tasks.filter(t => t.status === 'Done').length;
+ const pendingCount = tasks.filter(t => t.status !== 'Done' && !t.archived).length;
+ const archivedCount = tasks.filter(t => t.archived).length;
+ const subjectsCount = subjects.length;
+ const username = localStorage.getItem('studyplan_username') || 'StudyPlan User';
+ const email = localStorage.getItem('studyplan_email') || 'user@studyplan.app';
+ const joinedDate = localStorage.getItem('studyplan_joined') || 'June 2026';
+
+ profileSection.innerHTML = `
+
+
+
+
+ Account details
+
+ Username
+ ${escapeHtml(username)}
+
+
+ Email
+ ${escapeHtml(email)}
+
+
+ Member since
+ ${escapeHtml(joinedDate)}
+
+
+
+
+ Study statistics
+
+
+ ${completedCount}
+ Completed
+
+
+ ${pendingCount}
+ Pending
+
+
+ ${archivedCount}
+ Archived
+
+
+ ${subjectsCount}
+ Subjects
+
+
+
+
+
+
+ Account overview
+ Your profile information and study statistics will update automatically as you use StudyPlan.
+
+ `;
+}
+
+function showProfileSection() {
+ currentView = 'profile';
+ document.querySelector('.cal-section')?.classList.add('hidden');
+ document.getElementById('tasks-section')?.classList.add('hidden');
+ document.getElementById('focus-section')?.classList.add('hidden');
+ profileSection?.classList.remove('hidden');
+ topbar?.classList.add('hidden');
+ renderProfileSection();
+}
+
+function hideProfileSection() {
+ profileSection?.classList.add('hidden');
+ topbar?.classList.remove('hidden');
+}
+
function formatDate(dateStr) {
if (!dateStr) return 'No Date';
const d = new Date(dateStr);
@@ -1219,6 +1355,8 @@ store.subscribe(renderTasks);
store.subscribe(renderExtraction);
store.subscribe(renderCalendar);
store.subscribe(renderFocusTasks);
+store.subscribe(renderFocusStats);
+store.subscribe(renderProfileSection);
store.subscribe(renderSidebarSubjects);
document.addEventListener('DOMContentLoaded', () => {
@@ -1277,8 +1415,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
store.fetchInitialData();
+ store.fetchFocusSessions();
loadTimerState();
-
+
const calendarBtn = document.getElementById('calendar-btn');
const allTasksBtn = document.getElementById('all-tasks-btn');
const archivedTasksBtn = document.getElementById('archived-tasks-btn');
@@ -1291,6 +1430,7 @@ document.addEventListener('DOMContentLoaded', () => {
calendarBtn.addEventListener('click', () => {
currentView = 'calendar';
+ hideProfileSection();
document.querySelector('.cal-section').classList.remove('hidden');
document.getElementById('tasks-section').classList.remove('hidden');
document.getElementById('focus-section').classList.add('hidden');
@@ -1300,6 +1440,7 @@ document.addEventListener('DOMContentLoaded', () => {
allTasksBtn.addEventListener('click', () => {
currentView = 'all-tasks';
+ hideProfileSection();
document.querySelector('.cal-section').classList.add('hidden');
document.getElementById('tasks-section').classList.remove('hidden');
document.getElementById('focus-section').classList.add('hidden');
@@ -1309,6 +1450,7 @@ document.addEventListener('DOMContentLoaded', () => {
archivedTasksBtn.addEventListener('click', () => {
currentView = 'archived';
+ hideProfileSection();
document.querySelector('.cal-section').classList.add('hidden');
document.getElementById('tasks-section').classList.remove('hidden');
document.getElementById('focus-section').classList.add('hidden');
@@ -1316,9 +1458,10 @@ document.addEventListener('DOMContentLoaded', () => {
renderTasks();
});
- if(focusModeBtn) {
+ if (focusModeBtn) {
focusModeBtn.addEventListener('click', () => {
currentView = 'focus';
+ hideProfileSection();
document.querySelector('.cal-section').classList.add('hidden');
document.getElementById('tasks-section').classList.add('hidden');
document.getElementById('focus-section').classList.remove('hidden');
@@ -1327,121 +1470,130 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
+ if (profileBtn) {
+ profileBtn.addEventListener('click', () => {
+ showProfileSection();
+ });
+ }
+
+ document.getElementById('cal-prev').addEventListener('click', () => {
+ currentMonthDate.setMonth(currentMonthDate.getMonth() - 1);
+ renderCalendar();
+ });
+
document.getElementById('cal-next').addEventListener('click', () => {
currentMonthDate.setMonth(currentMonthDate.getMonth() + 1);
renderCalendar();
});
+ // New Task addition event listeners
+ newTaskBtn.addEventListener('click', () => {
+ if (!store.subjects || store.subjects.length === 0) {
+ Toast.show('Subjects are still loading. Please try again in a moment.', 'warning');
+ return;
+ }
-//NEw Task addition event listeners
-newTaskBtn.addEventListener('click', () => {
-
- if (!store.subjects || store.subjects.length === 0) {
- Toast.show('Subjects are still loading. Please try again in a moment.', 'warning');
- return;
- }
-
- newTaskSubject.innerHTML = store.subjects
- .map(s => ``)
- .join('');
-
-
- if (selectedDate) {
- const d = new Date(selectedDate);
- d.setHours(18, 0, 0, 0);
- newTaskDate.value = d.toISOString().substring(0, 16);
- } else {
- newTaskDate.value = '';
- }
+ newTaskSubject.innerHTML = store.subjects
+ .map(s => ``)
+ .join('');
- newTaskTitle.value = '';
- newTaskNotes.value = '';
- if (newTaskEstimatedDuration) newTaskEstimatedDuration.value = '';
- setNewTaskDurationUnit('minutes');
+ if (selectedDate) {
+ const d = new Date(selectedDate);
+ d.setHours(18, 0, 0, 0);
+ newTaskDate.value = d.toISOString().substring(0, 16);
+ } else {
+ newTaskDate.value = '';
+ }
- newTaskModal.style.display = 'flex';
-});
+ newTaskTitle.value = '';
+ newTaskNotes.value = '';
+ if (newTaskEstimatedDuration) newTaskEstimatedDuration.value = '';
+ setNewTaskDurationUnit('minutes');
-newTaskCancel.addEventListener('click', () => {
- newTaskModal.style.display = 'none';
-});
+ newTaskModal.style.display = 'flex';
+ });
-newTaskModal.addEventListener('click', (e) => {
- if (e.target === newTaskModal) {
+ newTaskCancel.addEventListener('click', () => {
newTaskModal.style.display = 'none';
- }
-});
+ });
-function setNewTaskDurationUnit(unit) {
- selectedTaskDurationUnit = unit;
- newTaskDurationSwitch?.setAttribute('data-unit', unit);
- newTaskDurationMin?.classList.toggle('active', unit === 'minutes');
- newTaskDurationHr?.classList.toggle('active', unit === 'hours');
-}
+ newTaskModal.addEventListener('click', (e) => {
+ if (e.target === newTaskModal) {
+ newTaskModal.style.display = 'none';
+ }
+ });
-newTaskDurationMin?.addEventListener('click', () => setNewTaskDurationUnit('minutes'));
-newTaskDurationHr?.addEventListener('click', () => setNewTaskDurationUnit('hours'));
-
-newTaskSave.addEventListener('click', async () => {
- const rawTitle = newTaskTitle.value.trim();
- const subject_id = newTaskSubject.value;
- const notes = newTaskNotes.value.trim();
- const dateVal = newTaskDate.value;
- const durationValue = newTaskEstimatedDuration ? Number(newTaskEstimatedDuration.value) : 0;
- const estimated_duration = durationValue > 0
- ? Math.round(selectedTaskDurationUnit === 'hours' ? durationValue * 60 : durationValue)
- : null;
-
- if (!rawTitle) {
- alert('Please enter a task name');
- return;
+ function setNewTaskDurationUnit(unit) {
+ selectedTaskDurationUnit = unit;
+ newTaskDurationSwitch?.setAttribute('data-unit', unit);
+ newTaskDurationMin?.classList.toggle('active', unit === 'minutes');
+ newTaskDurationHr?.classList.toggle('active', unit === 'hours');
}
- if (!dateVal) {
- alert('Please enter a deadline');
- return;
-}
+ newTaskDurationMin?.addEventListener('click', () => setNewTaskDurationUnit('minutes'));
+ newTaskDurationHr?.addEventListener('click', () => setNewTaskDurationUnit('hours'));
+
+ newTaskSave.addEventListener('click', async () => {
+ const rawTitle = newTaskTitle.value.trim();
+ const subject_id = newTaskSubject.value;
+ const notes = newTaskNotes.value.trim();
+ const dateVal = newTaskDate.value;
+ const durationValue = newTaskEstimatedDuration ? Number(newTaskEstimatedDuration.value) : 0;
+ const estimated_duration = durationValue > 0
+ ? Math.round(selectedTaskDurationUnit === 'hours' ? durationValue * 60 : durationValue)
+ : null;
+
+ if (!rawTitle) {
+ alert('Please enter a task name');
+ return;
+ }
-if (!subject_id) {
- alert('Please select a subject');
- return;
-}
- const { cleanTitle, labels } = extractLabels(rawTitle);
- const due_at = dateVal ? new Date(dateVal).toISOString() : '';
-
- const newTask = {
- title: cleanTitle || rawTitle,
- subject_id,
- due_at,
- notes,
- priority: 'medium',
- status: 'Not Started',
- archived: 0,
- estimated_duration,
- is_estimated_duration_min: selectedTaskDurationUnit === 'minutes' ? 1 : 0,
- labels
- };
+ if (!dateVal) {
+ alert('Please enter a deadline');
+ return;
+ }
- await store.addTasks([newTask]);
- newTaskModal.style.display = 'none';
-});
+ if (!subject_id) {
+ alert('Please select a subject');
+ return;
+ }
-addItemsBtn.addEventListener('click', () => {
- if (store.currentPaste) {
- const pasteWithLabels = store.currentPaste.map(t => {
- const { cleanTitle, labels } = extractLabels(t.title);
- return { ...t, title: cleanTitle || t.title, labels };
- });
- store.addTasks(pasteWithLabels);
- store.clearExtracted();
- pasteInput.value = '';
- }
-});
+ const { cleanTitle, labels } = extractLabels(rawTitle);
+ const due_at = dateVal ? new Date(dateVal).toISOString() : '';
+
+ const newTask = {
+ title: cleanTitle || rawTitle,
+ subject_id,
+ due_at,
+ notes,
+ priority: 'medium',
+ status: 'Not Started',
+ archived: 0,
+ estimated_duration,
+ is_estimated_duration_min: selectedTaskDurationUnit === 'minutes' ? 1 : 0,
+ labels
+ };
+
+ await store.addTasks([newTask]);
+ newTaskModal.style.display = 'none';
+ });
+
+ addItemsBtn.addEventListener('click', () => {
+ if (store.currentPaste) {
+ const pasteWithLabels = store.currentPaste.map(t => {
+ const { cleanTitle, labels } = extractLabels(t.title);
+ return { ...t, title: cleanTitle || t.title, labels };
+ });
+ store.addTasks(pasteWithLabels);
+ store.clearExtracted();
+ pasteInput.value = '';
+ }
+ });
});
// Ensures the button is hidden on initial page load if the textarea is empty
if (pasteInput.value.trim() === "") {
- clearBtn.style.display = 'none';
+ clearBtn.style.display = 'none';
}
extractBtn.addEventListener('click', async () => {
@@ -1461,19 +1613,19 @@ extractBtn.addEventListener('click', async () => {
// Wipes the text, clears the store, hides the button, and refocuses the cursor
clearBtn.addEventListener('click', () => {
- pasteInput.value = '';
- store.clearExtracted();
- clearBtn.style.display = 'none'; // Hides the clear button instantly
- pasteInput.focus(); // Puts the typing cursor back in the box
+ pasteInput.value = '';
+ store.clearExtracted();
+ clearBtn.style.display = 'none';
+ pasteInput.focus();
});
// Listens to typing/pasting to show or hide the button dynamically
pasteInput.addEventListener('input', () => {
- if (pasteInput.value.trim().length > 0) {
- clearBtn.style.display = 'block';
- } else {
- clearBtn.style.display = 'none';
- }
+ if (pasteInput.value.trim().length > 0) {
+ clearBtn.style.display = 'block';
+ } else {
+ clearBtn.style.display = 'none';
+ }
});
downloadBtn.addEventListener('click', () => {
diff --git a/js/store.js b/js/store.js
index b78dd42..b5e0ef1 100644
--- a/js/store.js
+++ b/js/store.js
@@ -5,6 +5,7 @@ export const store = {
subjects: [],
tasks: [],
currentPaste: null,
+ focusSessions: [],
listeners: [],
isSameCalendarDate(dateA, dateB) {
@@ -358,6 +359,36 @@ export const store = {
}
},
+ async fetchFocusSessions() {
+ try {
+ const res = await fetch('/api/focus-sessions');
+ if (!res.ok) throw new Error('Failed to fetch focus sessions');
+ this.focusSessions = await res.json();
+ this.notify();
+ } catch (e) {
+ console.error('Failed to load focus sessions', e);
+ }
+ },
+
+ async addFocusSession(sessionData) {
+ try {
+ const res = await fetch('/api/focus-sessions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(sessionData)
+ });
+
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error(body.error || 'Failed to record focus session');
+ }
+
+ await this.fetchFocusSessions();
+ } catch (e) {
+ console.error('Failed to save focus session', e);
+ }
+ },
+
clearExtracted() {
this.currentPaste = null;
this.notify();
diff --git a/server.js b/server.js
index 68dadff..e7b2fe8 100644
--- a/server.js
+++ b/server.js
@@ -356,6 +356,45 @@ app.get('/api/tasks', (req, res) => {
});
});
+app.get('/api/focus-sessions', (req, res) => {
+ const query = `
+ SELECT fs.*, t.title AS task_title
+ FROM focus_sessions fs
+ LEFT JOIN tasks t ON t.id = fs.task_id
+ ORDER BY datetime(fs.completed_at) DESC
+ `;
+ db.all(query, (err, rows) => {
+ if (err) return res.status(500).json({ error: err.message });
+ res.json(rows);
+ });
+});
+
+app.get('/api/focus-stats', (req, res) => {
+ db.all('SELECT duration_seconds, completed_at FROM focus_sessions ORDER BY datetime(completed_at) ASC', (err, rows) => {
+ if (err) return res.status(500).json({ error: err.message });
+
+ const totalSessions = rows.length;
+ const totalSeconds = rows.reduce((sum, row) => sum + Number(row.duration_seconds || 0), 0);
+ const daySet = new Set(rows.map(row => new Date(row.completed_at).toISOString().substring(0, 10)));
+
+ let streak = 0;
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ let cursor = new Date(today);
+
+ while (daySet.has(cursor.toISOString().substring(0, 10))) {
+ streak += 1;
+ cursor.setDate(cursor.getDate() - 1);
+ }
+
+ res.json({
+ totalSessions,
+ totalSeconds,
+ currentStreakDays: streak,
+ });
+ });
+});
+
// ================= ADD TASKS =================
app.post('/api/tasks', (req, res) => {
try {
@@ -471,6 +510,25 @@ app.post('/api/tasks', (req, res) => {
}
});
+app.post('/api/focus-sessions', (req, res) => {
+ const { task_id, duration_seconds, completed_at } = req.body;
+ if (!Number.isFinite(Number(duration_seconds)) || Number(duration_seconds) <= 0) {
+ return res.status(400).json({ error: 'duration_seconds must be a positive number' });
+ }
+
+ const id = `focus_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+ const timestamp = completed_at ? completed_at : new Date().toISOString();
+
+ db.run(
+ 'INSERT INTO focus_sessions (id, task_id, duration_seconds, completed_at) VALUES (?, ?, ?, ?)',
+ [id, task_id || null, Number(duration_seconds), timestamp],
+ function (err) {
+ if (err) return res.status(500).json({ error: err.message });
+ res.status(201).json({ success: true, id, task_id: task_id || null, duration_seconds: Number(duration_seconds), completed_at: timestamp });
+ }
+ );
+});
+
// ================= UPDATE =================
app.put('/api/tasks/:id', (req, res) => {