OffGridSchedula / server /impact.py
ParetoOptimal's picture
Initial Commit
0366d65
Raw
History Blame Contribute Delete
2.86 kB
"""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