Files
TimeTracker/app/static/idle.js
Dries Peeters 99e6584c04 feat(ux): add command palette, bulk edit, idle detect, calendar
- Command palette (Ctrl/Cmd+K) with quick nav (g d/p/r/t), start/stop
  timer, theme toggle
  - Adds modal to base layout and global shortcuts
  - Files: app/templates/base.html, app/static/commands.js

- Bulk edit for time entries with multi-select and quick actions
  - Delete, set billable/non-billable from dashboard Recent Entries
  - API: POST /api/entries/bulk
  - Files: app/templates/main/dashboard.html, app/routes/api.py

- Idle detection and “resume/stop at” support
  - Detect inactivity and prompt to stop at last active time
  - API: POST /api/timer/stop_at, POST /api/timer/resume
  - Files: app/static/idle.js, app/templates/base.html, app/routes/api.py

- Calendar (day/week/month) with drag‑to‑create entries
  - Route: /timer/calendar
  - APIs: GET /api/calendar/events, POST /api/entries
  - Files: app/routes/timer.py, templates/timer/calendar.html, app/routes/api.py

- UX polish: improved flash container spacing; reused existing
  skeleton loaders, loading spinners, and toasts

No breaking changes.
2025-10-06 13:09:36 +02:00

65 lines
2.5 KiB
JavaScript

// Idle detection: when user is inactive, offer to stop timer at last active time
(function(){
if (window.__ttIdleLoaded) return; window.__ttIdleLoaded = true;
const IDLE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
const CHECK_INTERVAL_MS = 60 * 1000; // 1 minute
let lastActivity = Date.now();
let promptShown = false;
function markActive(){
lastActivity = Date.now();
promptShown = false;
}
['mousemove','keydown','scroll','click','touchstart','visibilitychange'].forEach(evt =>
document.addEventListener(evt, markActive, { passive: true })
);
async function getTimer(){
try {
const r = await fetch('/api/timer/status');
if (!r.ok) return null; const j = await r.json();
return j && j.active ? j.timer : null;
} catch(e){ return null; }
}
function formatTime(d){
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
async function stopAt(ts){
try {
const r = await fetch('/api/timer/stop_at', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stop_time: new Date(ts).toISOString() }) });
if (r.ok){ showToast('Timer stopped due to inactivity', 'warning'); location.reload(); }
} catch(e) {}
}
function showIdlePrompt(stopTs){
if (promptShown) return; promptShown = true;
// Create a lightweight inline prompt toast
const t = document.createElement('div');
t.className = 'toast align-items-center text-white bg-warning border-0 fade show';
t.innerHTML = `<div class="d-flex"><div class="toast-body">You seem inactive since ${formatTime(new Date(stopTs))}. Stop the timer at that time?</div><div class="d-flex gap-2 align-items-center me-2"><button class="btn btn-sm btn-light" data-act="stop">Stop</button><button class="btn btn-sm btn-outline-light" data-act="dismiss">Dismiss</button></div></div>`;
const container = document.getElementById('toast-container') || document.body;
container.appendChild(t);
t.querySelector('[data-act="stop"]').addEventListener('click', () => { t.remove(); stopAt(stopTs); });
t.querySelector('[data-act="dismiss"]').addEventListener('click', () => { t.remove(); });
setTimeout(() => { try { t.remove(); } catch(e){} }, 60_000);
}
async function tick(){
const active = await getTimer();
if (!active) return;
const idleFor = Date.now() - lastActivity;
if (idleFor >= IDLE_THRESHOLD_MS){
const stopTs = Date.now() - idleFor; // last active time
showIdlePrompt(stopTs);
}
}
setInterval(tick, CHECK_INTERVAL_MS);
})();