| """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: |
| 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 |
|
|