No account? Create one

Have an account? Sign in

Dashboard

0:00
deep work today
Target: 4h 0m
This Week 0h
Today's Sessions 0 sessions

No focus sessions yet today

Distractions Today 0

No distractions logged β€” nice! 🎯

Tap to begin a deep work session

Goals

Set a goal to connect your work to what matters.

$0

Projects

Distractions

🎯

No distractions logged yet

Settings

Daily Targets
Daily focus target
min
Default session goal
min
Max sessions per day
Focus Science
🧠 90-min ultradian rhythm
Your brain cycles through 90-120 min focus peaks. Sessions of 60-90 min align with natural cognitive performance.
⏱️ 2-hour ceiling
For 95% of knowledge work, 2 hours is the sustainable max per session before quality drops.
📊 2-3 sessions daily
Huberman recommends a max of 2-3 deep work bouts per day. Even elite performers rarely exceed 4 hours total.
🚶 Breaks are essential
Physical movement between sessions resets your cognitive system. Stand, stretch, walk β€” your next session depends on it.
Reports
Default view
AI coaching tips
Email Monitor
Monitor during focus
Status Not configured
Account
β€” free
Sign out

Reports

Today
β€”
Focus Time
β€”
Sessions
β€”
Distractions
β€”
Earned
Time by Project

Loading…

Distractions

Loading…

Goal Progress

Loading…

✨ AI Coaching

What interrupted you?

const token = localStorage.getItem('df_token'); const res = await fetch('/api' + path, { headers: token ? { 'Authorization': 'Bearer ' + token } : {}, credentials: 'include' }); if (!res.ok) throw new Error('API error ' + res.status); return res.json(); } /* ── Formatters ── */ function fmtHM(seconds) { const h = Math.floor(seconds / 3600), m = Math.floor((seconds % 3600) / 60); if (h > 0 && m > 0) return h + 'h ' + m + 'm'; if (h > 0) return h + 'h'; return m + 'm'; } function fmtDate(iso) { return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } const DIST_META = { email: { icon: 'πŸ“§', label: 'Email', color: '#4B7BF5' }, text: { icon: 'πŸ’¬', label: 'Text', color: '#34D399' }, phone: { icon: 'πŸ“ž', label: 'Call', color: '#F97316' }, colleague: { icon: 'πŸ—£οΈ', label: 'Colleague', color: '#A855F7' }, family: { icon: 'πŸ‘¨β€πŸ‘©β€πŸ‘§', label: 'Family', color: '#EC4899' }, social_media: { icon: 'πŸ“±', label: 'Social', color: '#EF4444' }, self: { icon: '🧠', label: 'Self', color: '#F59E0B' }, other: { icon: '❓', label: 'Other', color: '#14B8A6' }, }; let currentPeriod = 'daily'; /* ── Date helpers ── */ function getRange(period) { const now = new Date(); const ymd = d => d.toISOString().slice(0, 10); const today = ymd(now); if (period === 'daily') return { from: today, to: today, label: 'Today' }; if (period === 'weekly') { const mon = new Date(now); mon.setDate(now.getDate() - ((now.getDay() + 6) % 7)); const sun = new Date(mon); sun.setDate(mon.getDate() + 6); return { from: ymd(mon), to: ymd(sun), label: fmtDate(ymd(mon)) + ' – ' + fmtDate(ymd(sun)) }; } const first = new Date(now.getFullYear(), now.getMonth(), 1); const last = new Date(now.getFullYear(), now.getMonth() + 1, 0); return { from: ymd(first), to: ymd(last), label: now.toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) }; } /* ── Render helpers ── */ function setEl(id, html) { const el = document.getElementById(id); if (el) el.innerHTML = html; } function setText(id, txt) { const el = document.getElementById(id); if (el) el.textContent = txt; } function renderBars(containerId, items, valueFn, labelFn, colorFn, formatFn, emptyMsg) { const el = document.getElementById(containerId); if (!el) return; if (!items.length) { el.innerHTML = '

' + emptyMsg + '

'; return; } const max = Math.max(...items.map(valueFn), 1); el.innerHTML = items.map(item => `
${labelFn(item)}
${formatFn(item)}
`).join(''); } /* ── Main render ── */ async function renderReport(period) { currentPeriod = period; const range = getRange(period); setText('report-period-label', range.label); // Reset stats to loading state ['rpt-focus-time','rpt-sessions','rpt-distractions','rpt-earnings'].forEach(id => setText(id, '…')); try { if (period === 'daily') { await renderDaily(); } else if (period === 'weekly') { await renderWeekly(range); } else { await renderMonthly(range); } } catch (e) { console.error('Reports render error', e); } // Always load goals and AI loadGoalsSection(); if (document.getElementById('set-show-ai-insights')?.checked !== false) { fetchInsights(period); } else { document.getElementById('rpt-insights-card').style.display = 'none'; } } /* ── DAILY ── uses /api/dashboard/today which has full session + distraction data ── */ async function renderDaily() { const [todayData, projectsData] = await Promise.all([ rptApi('/dashboard/today'), rptApi('/projects') ]); const sessions = (todayData.sessions || []).filter(s => s.status === 'completed'); const deepSec = todayData.deep_work_seconds || 0; const projects = {}; (projectsData.projects || []).forEach(p => { projects[p.id] = p; }); // Stats setText('rpt-focus-time', fmtHM(deepSec)); setText('rpt-sessions', sessions.length); setText('rpt-distractions', todayData.total_distractions || 0); // Earnings from billable sessions let earnings = 0; sessions.forEach(s => { const p = projects[s.project_id]; if (p && p.is_billable && p.hourly_rate > 0) { earnings += (s.active_seconds || 0) / 3600 * p.hourly_rate; } }); setText('rpt-earnings', earnings > 0 ? '$' + Math.round(earnings) : '–'); // Project breakdown from today's sessions const projSecs = {}; sessions.forEach(s => { const k = s.project_id || '__none__'; projSecs[k] = (projSecs[k] || 0) + (s.active_seconds || 0); }); const projItems = Object.entries(projSecs) .sort((a,b) => b[1] - a[1]) .map(([id, secs]) => { const p = projects[id]; return { name: p ? p.name : (id === '__none__' ? 'No Project' : 'Unknown'), color: p ? p.color : '#6B7280', secs }; }); setText('rpt-projects-total', fmtHM(deepSec)); renderBars('rpt-projects-chart', projItems, i => i.secs, i => i.name, i => i.color, i => fmtHM(i.secs), 'No sessions today'); // Distraction breakdown (today data has category counts) document.getElementById('rpt-trend-card').style.display = 'none'; const distItems = (todayData.distractions || []) .sort((a,b) => b.count - a.count) .map(d => ({ ...d, ...(DIST_META[d.category] || { icon:'❓', label: d.category, color:'#6B7280' }) })); setText('rpt-distraction-count', todayData.total_distractions || 0); renderBars('rpt-distraction-chart', distItems, i => i.count, i => i.icon + ' ' + i.label, i => i.color, i => i.count + 'Γ—', 'No distractions β€” nice! 🎯'); } /* ── WEEKLY ── uses /api/dashboard/week for the bar chart ── */ async function renderWeekly(range) { const [weekData, projectsData] = await Promise.all([ rptApi('/dashboard/week'), rptApi('/projects') ]); const days = weekData.days || []; const totalSec = weekData.total_seconds || 0; const projects = {}; (projectsData.projects || []).forEach(p => { projects[p.id] = p; }); setText('rpt-focus-time', fmtHM(totalSec)); setText('rpt-sessions', '–'); setText('rpt-distractions', '–'); setText('rpt-earnings', '–'); setText('rpt-projects-total', fmtHM(totalSec)); // Trend bars using week data document.getElementById('rpt-trend-card').style.display = ''; setText('rpt-trend-title', 'Daily Breakdown'); const maxSec = Math.max(...days.map(d => d.total_seconds), 1); document.getElementById('rpt-trend-chart').innerHTML = days.map(d => `
${d.total_seconds > 0 ? (d.total_seconds/3600).toFixed(1) + 'h' : ''}
${d.day_name ? d.day_name.slice(0,3) : ''}
`).join(''); // No per-project breakdown available at week level without extra API setEl('rpt-projects-chart', '

Project breakdown available in Daily view

'); setEl('rpt-distraction-chart', '

Distraction detail available in Daily view

'); setText('rpt-distraction-count', ''); } /* ── MONTHLY ── similar to weekly, uses week data as proxy ── */ async function renderMonthly(range) { // Reuse week data as best available summary await renderWeekly(range); setText('rpt-trend-title', 'This Week (Monthly detail coming soon)'); } /* ── Goal progress ── */ async function loadGoalsSection() { try { const data = await rptApi('/goals?include_achieved=1'); const active = (data.goals || []).filter(g => !g.achieved); if (!active.length) { setEl('rpt-goals-list', '

No active goals

'); return; } document.getElementById('rpt-goals-list').innerHTML = active.map(g => { const cur = g.current_amount || 0; const tgt = g.target_amount || 1; const pct = Math.min(100, Math.round(cur / tgt * 100)); const fin = g.goal_type === 'financial'; const cL = fin ? '$' + Math.round(cur).toLocaleString() : cur.toFixed(1) + 'h'; const tL = fin ? '$' + Math.round(tgt).toLocaleString() : tgt + 'h'; const dl = g.deadline ? ' Β· Due ' + fmtDate(g.deadline) : ''; return `
${g.icon || '🎯'} ${g.name || 'Goal'} ${pct}%
${cL} of ${tL}${dl}
`; }).join(''); } catch(e) { setEl('rpt-goals-list', '

Could not load goals

'); } } /* ── AI Coaching ── */ async function fetchInsights(period) { document.getElementById('rpt-insights-card').style.display = ''; setEl('rpt-insights-body', `
`); // Gather context from already-rendered stats const focusTime = document.getElementById('rpt-focus-time')?.textContent || '–'; const sessions = document.getElementById('rpt-sessions')?.textContent || '–'; const distracts = document.getElementById('rpt-distractions')?.textContent || '–'; const pLabel = { daily:'today', weekly:'this week', monthly:'this month' }[period]; const prompt = `You are a friendly, encouraging time management coach. A Tempr Focus user's ${pLabel} stats: - Focus time: ${focusTime} - Sessions completed: ${sessions} - Distractions logged: ${distracts} Give exactly 3 short coaching insights as a JSON array. Each: {"icon":"emoji","title":"3-5 word headline","text":"1-2 sentences referencing the numbers, specific and actionable"}. Be encouraging but concrete. Return ONLY the JSON array, no markdown.`; try { const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model:'claude-sonnet-4-6', max_tokens:800, messages:[{ role:'user', content: prompt }] }) }); const data = await res.json(); const raw = data?.content?.[0]?.text || '[]'; const insights = JSON.parse(raw.replace(/```json|```/g,'').trim()); if (!Array.isArray(insights) || !insights.length) throw new Error(); document.getElementById('rpt-insights-body').innerHTML = insights.map(i => `
${i.icon}
${i.title}
${i.text}
`).join(''); } catch { document.getElementById('rpt-insights-body').innerHTML = `
πŸ’‘
Keep building momentum
Every session you log adds to your picture. Consistency is the real superpower.
`; } } /* ── Wire up period tabs ── */ function initReports() { document.querySelectorAll('.period-tab').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.period-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); renderReport(btn.dataset.period); }); }); document.getElementById('rpt-refresh-insights')?.addEventListener('click', () => fetchInsights(currentPeriod)); // Load saved setting try { const saved = JSON.parse(localStorage.getItem('tempr_report_settings') || '{}'); if (saved.period) document.getElementById('set-default-report-period').value = saved.period; if (saved.aiInsights === false) document.getElementById('set-show-ai-insights').checked = false; } catch {} // Save settings on change document.getElementById('set-default-report-period')?.addEventListener('change', e => { saveReportSettings(); // Sync tabs const p = e.target.value; document.querySelectorAll('.period-tab').forEach(b => b.classList.toggle('active', b.dataset.period === p)); }); document.getElementById('set-show-ai-insights')?.addEventListener('change', saveReportSettings); /* ── Key fix: hook into nav click for Reports tab. app.js calls showScreen('reports') which handles show/hide via CSS class. We just need to trigger our data load after that happens. ── */ document.querySelectorAll('.nav-item[data-screen="reports"]').forEach(btn => { btn.addEventListener('click', () => { // Let app.js showScreen() run first, then load data setTimeout(() => { const activePeriod = document.querySelector('.period-tab.active')?.dataset?.period || currentPeriod; renderReport(activePeriod); }, 50); }); }); } function saveReportSettings() { try { localStorage.setItem('tempr_report_settings', JSON.stringify({ period: document.getElementById('set-default-report-period')?.value, aiInsights: document.getElementById('set-show-ai-insights')?.checked, })); } catch {} } // Boot after app.js has initialised if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initReports, 300)); } else { setTimeout(initReports, 300); } })();