feat: Add Focus Tools (ADHD-friendly productivity features)
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Add Focus Tools dashboard with collapsible help section - Implement Quick Wins page for tasks under 15 minutes - Add Single Task Focus mode to reduce overwhelm - Create Body Doubling page for virtual co-working - Add visual timer, break reminders, and energy tracking - Implement hyperfocus protection alerts - Add ADHD settings panel with customizable options - Include full English and German translations - Fix larger touch targets CSS to not break button layouts - Add Playwright tests for Focus Tools features
This commit is contained in:
+349
-1
@@ -1871,6 +1871,354 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
console.error("Export Failed:", e);
|
||||
res.status(500).json({ error: "Failed to export data" });
|
||||
}
|
||||
}); // End of routes
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ADHD MODE ROUTES
|
||||
// ============================================
|
||||
|
||||
// Update ADHD settings
|
||||
app.patch("/api/user/adhd-settings", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { adhdMode, adhdSettings } = req.body;
|
||||
const updated = await storage.updateUser(userId, {
|
||||
adhdMode,
|
||||
adhdSettings,
|
||||
});
|
||||
|
||||
await storage.createAuditLog({
|
||||
userId,
|
||||
action: "UPDATE",
|
||||
entityType: "USER",
|
||||
entityId: userId,
|
||||
details: { action: "UPDATE_ADHD_SETTINGS", adhdMode },
|
||||
source: "USER"
|
||||
});
|
||||
|
||||
res.json(updated);
|
||||
} catch (e) {
|
||||
console.error("Update ADHD settings failed:", e);
|
||||
res.status(500).json({ error: "Failed to update ADHD settings" });
|
||||
}
|
||||
});
|
||||
|
||||
// Log a break
|
||||
app.post("/api/user/log-break", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { breakType, durationMinutes } = req.body;
|
||||
|
||||
// Log the break
|
||||
await storage.createBreakLog({
|
||||
userId,
|
||||
breakType,
|
||||
durationMinutes: durationMinutes || 5,
|
||||
});
|
||||
|
||||
// Update user's lastBreakAt
|
||||
await storage.updateUser(userId, {
|
||||
lastBreakAt: new Date(),
|
||||
});
|
||||
|
||||
// Award XP for taking a break (micro-XP)
|
||||
await gamificationService.awardXP(userId, 10, 'break_taken', undefined, { breakType });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error("Log break failed:", e);
|
||||
res.status(500).json({ error: "Failed to log break" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get break stats
|
||||
app.get("/api/user/break-stats", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const stats = await storage.getBreakStats(userId);
|
||||
res.json(stats);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to get break stats" });
|
||||
}
|
||||
});
|
||||
|
||||
// Log energy level
|
||||
app.post("/api/energy/log", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { energyLevel, notes } = req.body;
|
||||
|
||||
await storage.createEnergyLog({
|
||||
userId,
|
||||
energyLevel,
|
||||
notes,
|
||||
});
|
||||
|
||||
// Update user's current energy level
|
||||
await storage.updateUser(userId, {
|
||||
currentEnergyLevel: energyLevel,
|
||||
todayEnergyCheckedIn: true,
|
||||
});
|
||||
|
||||
// Award XP for checking in
|
||||
await gamificationService.awardXP(userId, 5, 'energy_checkin', undefined, { energyLevel });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error("Log energy failed:", e);
|
||||
res.status(500).json({ error: "Failed to log energy" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get energy history
|
||||
app.get("/api/energy/history", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const history = await storage.getEnergyHistory(userId);
|
||||
res.json(history);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to get energy history" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get energy-based task suggestions
|
||||
app.get("/api/tasks/energy-based", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const tasks = await storage.getTasksForUser(userId);
|
||||
const energyLevel = user.currentEnergyLevel || 'medium';
|
||||
|
||||
// Filter tasks based on energy level
|
||||
let recommended = tasks.filter(t => t.status !== 'done');
|
||||
|
||||
if (energyLevel === 'low') {
|
||||
// Prefer low-energy tasks for low energy
|
||||
recommended = recommended.filter(t =>
|
||||
t.energyLevel === 'low' ||
|
||||
(t.estimatedDuration && t.estimatedDuration <= 15) ||
|
||||
t.priority === 'low'
|
||||
);
|
||||
} else if (energyLevel === 'high') {
|
||||
// Prefer high-energy/challenging tasks
|
||||
recommended = recommended.filter(t =>
|
||||
t.energyLevel === 'high' ||
|
||||
t.priority === 'high' ||
|
||||
(t.estimatedDuration && t.estimatedDuration > 30)
|
||||
);
|
||||
}
|
||||
|
||||
res.json(recommended.slice(0, 10));
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to get energy-based tasks" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get quick wins
|
||||
app.get("/api/tasks/quick-wins", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const tasks = await storage.getTasksForUser(userId);
|
||||
const threshold = (user.adhdSettings as any)?.quickWinThreshold || 10;
|
||||
|
||||
const quickWins = tasks
|
||||
.filter(t => {
|
||||
if (t.status === 'done') return false;
|
||||
if (t.estimatedDuration && t.estimatedDuration > threshold * 1.5) return false;
|
||||
return true;
|
||||
})
|
||||
.slice(0, 10);
|
||||
|
||||
res.json(quickWins);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to get quick wins" });
|
||||
}
|
||||
});
|
||||
|
||||
// AI Task Breakdown
|
||||
app.post("/api/ai/breakdown-task", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { taskId, title, description, estimatedDuration } = req.body;
|
||||
|
||||
const breakdown = await aiService.breakdownTask({
|
||||
title,
|
||||
description,
|
||||
estimatedDuration,
|
||||
});
|
||||
|
||||
await storage.createAuditLog({
|
||||
userId,
|
||||
action: "AI_BREAKDOWN",
|
||||
entityType: "TASK",
|
||||
entityId: taskId,
|
||||
details: { title, subtaskCount: breakdown.subtasks?.length },
|
||||
source: "AI"
|
||||
});
|
||||
|
||||
res.json(breakdown);
|
||||
} catch (e) {
|
||||
console.error("AI breakdown failed:", e);
|
||||
res.status(500).json({ error: "Failed to break down task" });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// BODY DOUBLING SESSION ROUTES
|
||||
// ============================================
|
||||
|
||||
// Get all sessions
|
||||
app.get("/api/sessions", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const sessions = await storage.getBodyDoublingSessions(userId);
|
||||
res.json(sessions);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to get sessions" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create session
|
||||
app.post("/api/sessions", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const session = await storage.createBodyDoublingSession({
|
||||
hostId: userId,
|
||||
...req.body,
|
||||
});
|
||||
|
||||
// Auto-join as participant
|
||||
await storage.joinSession(session.id, userId);
|
||||
|
||||
res.json(session);
|
||||
} catch (e) {
|
||||
console.error("Create session failed:", e);
|
||||
res.status(500).json({ error: "Failed to create session" });
|
||||
}
|
||||
});
|
||||
|
||||
// Join session
|
||||
app.post("/api/sessions/:id/join", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
const sessionId = req.params.id;
|
||||
try {
|
||||
await storage.joinSession(sessionId, userId);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error("Join session failed:", e);
|
||||
res.status(500).json({ error: "Failed to join session" });
|
||||
}
|
||||
});
|
||||
|
||||
// Leave session
|
||||
app.post("/api/sessions/:id/leave", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
const sessionId = req.params.id;
|
||||
try {
|
||||
await storage.leaveSession(sessionId, userId);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error("Leave session failed:", e);
|
||||
res.status(500).json({ error: "Failed to leave session" });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// FOCUS SESSION ROUTES
|
||||
// ============================================
|
||||
|
||||
// Start focus session
|
||||
app.post("/api/focus-sessions", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const session = await storage.createFocusSession({
|
||||
userId,
|
||||
...req.body,
|
||||
});
|
||||
res.json(session);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to start focus session" });
|
||||
}
|
||||
});
|
||||
|
||||
// Complete focus session
|
||||
app.patch("/api/focus-sessions/:id/complete", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { actualMinutes, wasCompleted } = req.body;
|
||||
const session = await storage.completeFocusSession(req.params.id, actualMinutes, wasCompleted);
|
||||
|
||||
// Award XP based on completion
|
||||
const xpAmount = wasCompleted ? 15 : 5;
|
||||
await gamificationService.awardXP(userId, xpAmount, 'focus_session', undefined, {
|
||||
sessionType: session?.sessionType,
|
||||
actualMinutes,
|
||||
});
|
||||
|
||||
res.json(session);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to complete focus session" });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// DAILY CHALLENGES ROUTES
|
||||
// ============================================
|
||||
|
||||
// Get today's challenges
|
||||
app.get("/api/challenges/today", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
let challenges = await storage.getTodaysChallenges(userId);
|
||||
|
||||
// Generate new challenges if none exist for today
|
||||
if (challenges.length === 0) {
|
||||
challenges = await storage.generateDailyChallenges(userId);
|
||||
}
|
||||
|
||||
res.json(challenges);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to get challenges" });
|
||||
}
|
||||
});
|
||||
|
||||
// Update challenge progress
|
||||
app.post("/api/challenges/:id/progress", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { increment } = req.body;
|
||||
const challenge = await storage.updateChallengeProgress(req.params.id, increment || 1);
|
||||
|
||||
// Award XP if completed
|
||||
if (challenge?.completedAt && challenge.xpReward) {
|
||||
await gamificationService.awardXP(userId, challenge.xpReward, 'daily_challenge', undefined, {
|
||||
challengeType: challenge.challengeType,
|
||||
});
|
||||
}
|
||||
|
||||
res.json(challenge);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "Failed to update challenge progress" });
|
||||
}
|
||||
});
|
||||
|
||||
// End of routes
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user