"""Durable weekly impact metrics: how much the agent actually saved the user. Unlike the in-memory activity bus (``server/events.py``), which is an 800-entry ring buffer lost on restart, this records a small set of counters **per ISO week** to a JSON file, so the "This week" panel accumulates over real use and survives restarts. Counters per week: ``events_captured``, ``conflicts_caught``, ``minutes_saved``. A "capture" is the user *accepting* events by exporting them (``.ics`` download or Google Calendar push) — see ``ui/blocks.py``. ``minutes_saved`` is a deliberately conservative, fully configurable **estimate** (not a measurement): a fixed number of minutes per event captured plus per conflict caught. Persistence mirrors ``app.py``'s ``_append_feed``: an env-overridable JSON file under ``/tmp`` by default. No database — local-first by design. """ from __future__ import annotations import json import os import threading from datetime import datetime from pathlib import Path _lock = threading.Lock() _ZERO = {"events_captured": 0, "conflicts_caught": 0, "minutes_saved": 0} def _path() -> Path: return Path(os.environ.get("IMPACT_PATH", "/tmp/impact_weeks.json")) def _min_per_event() -> int: return int(os.environ.get("IMPACT_MIN_PER_EVENT", "8")) def _min_per_conflict() -> int: return int(os.environ.get("IMPACT_MIN_PER_CONFLICT", "15")) def _week_key(when: datetime | None = None) -> str: iso = (when or datetime.now()).isocalendar() return f"{iso.year}-W{iso.week:02d}" def _load() -> dict: try: return json.loads(_path().read_text()) except Exception: # noqa: BLE001 missing/corrupt file -> start fresh return {} def record_capture(events_captured: int, conflicts_caught: int = 0) -> dict: """Durably add to the current week's counters; return that week's record. Re-reads the file before incrementing so concurrent writers and restarts never drop prior counts (append/aggregate, never overwrite-from-memory). """ minutes = events_captured * _min_per_event() + conflicts_caught * _min_per_conflict() key = _week_key() with _lock: data = _load() wk = {**_ZERO, **data.get(key, {})} wk["events_captured"] += events_captured wk["conflicts_caught"] += conflicts_caught wk["minutes_saved"] += minutes data[key] = wk _path().write_text(json.dumps(data, indent=2)) return dict(wk) def this_week() -> dict: """Read-only current-week record (all zeros if nothing recorded yet). The durable, weekly analogue of ``events.metrics()``. """ return {**_ZERO, **_load().get(_week_key(), {})} def reset() -> None: """Drop all recorded impact (test helper).""" with _lock: try: _path().unlink() except FileNotFoundError: pass