File size: 2,861 Bytes
0366d65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""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