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