"""The timer endpoints. Thin: every decision lives in `services.pomodoro`.""" from __future__ import annotations from datetime import date from fastapi import APIRouter, HTTPException, Query from app.api.deps import DbSession, Settings from app.api.schemas import ( AbandonRequest, AttachTaskRequest, CompleteTimerRequest, ExtendRequest, StartTimerRequest, ) from app.services import pomodoro, report, stats from app.services.notify import count_sent, notify, record_sent from app.timeutil import local_date router = APIRouter(prefix="/api/timer", tags=["timer"]) @router.get("/state") def get_state(session: DbSession, settings: Settings) -> dict: return pomodoro.state(session, settings) @router.post("/start") def start(body: StartTimerRequest, session: DbSession, settings: Settings) -> dict: try: pomodoro.start( session, settings, kind=body.kind, minutes=body.minutes, task_id=body.task_id, note=body.note, ) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.post("/pause") def pause(session: DbSession, settings: Settings) -> dict: try: pomodoro.pause(session, settings) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.post("/resume") def resume(session: DbSession, settings: Settings) -> dict: try: pomodoro.resume(session) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.post("/complete") def complete(body: CompleteTimerRequest, session: DbSession, settings: Settings) -> dict: try: finished, follow_on = pomodoro.complete( session, settings, auto_next=body.auto_next, expect_id=body.session_id, ) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc # `finished` is None when the named interval was no longer the running one: # nothing happened, so there is nothing to celebrate and nothing to announce. celebration = _maybe_celebrate(session, settings) if finished and finished.is_work else None state = pomodoro.state(session, settings) state["finished"] = pomodoro.session_dict(finished) state["auto_started"] = pomodoro.session_dict(follow_on) state["celebration"] = celebration return state @router.post("/abandon") def abandon(body: AbandonRequest, session: DbSession, settings: Settings) -> dict: try: pomodoro.abandon(session, settings, reason=body.reason) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.post("/interrupt") def interrupt(session: DbSession, settings: Settings) -> dict: try: pomodoro.add_interruption(session) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.post("/extend") def extend(body: ExtendRequest, session: DbSession, settings: Settings) -> dict: try: pomodoro.extend(session, body.minutes) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.post("/task") def attach_task(body: AttachTaskRequest, session: DbSession, settings: Settings) -> dict: try: pomodoro.attach_task(session, body.task_id) except pomodoro.TimerError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return pomodoro.state(session, settings) @router.get("/sessions") def sessions( session: DbSession, settings: Settings, day: date | None = Query(default=None, description="Local date; defaults to today."), ) -> dict: target = day or local_date(settings.timezone) rows = pomodoro.sessions_for_day(session, settings, target) return { "date": target.isoformat(), "sessions": [pomodoro.session_dict(row) for row in rows], "metrics": stats.day_metrics(session, settings, target), } def _maybe_celebrate(session, settings) -> str | None: """Cheer in Telegram every N pomodoros — the cheapest dopamine there is.""" every = settings.notifications.telegram.celebrate_every_n_pomodoros if not (settings.notifications.telegram.enabled and every): return None done = pomodoro.completed_work_today(session, settings) if done == 0 or done % every != 0: return None today = local_date(settings.timezone) kind = f"celebrate:{done}" if count_sent(session, kind, today): return None ru = settings.notifications.report.language == "ru" goal = settings.pomodoro.daily_goal_pomodoros bar = report.progress_bar(done, goal) text = ( f"🎉 {done} помидорок готово!\n{bar} {done}/{goal}\nДержи темп." if ru else f"🎉 {done} pomodoros done!\n{bar} {done}/{goal}\nKeep the rhythm." ) deliveries = notify(settings, text) if any(delivery.ok for delivery in deliveries): record_sent(session, kind, today, {"pomodoros": done}) return text