"""Monitoring period bookkeeping per spec §5.2, §8.6, §9.8. A monitoring period is a span during which Fenris monitoring is enabled. Powered-off time stays inside a period; deliberately disabled time does not. Key contracts: - Run finding no open period opens one at the run moment, never backdated (§9.8) - Wall-clock outside periods excluded from numerator and denominator (§5.2) - End causes: user_disabled, migrated, unknown_gap """ import sqlite3 from datetime import datetime def ensure_period_open(conn: sqlite3.Connection, run_time: datetime) -> None: """Ensure a monitoring period is open. If none exists, open one at run_time. Spec §9.8: A collection run finding no open monitoring period opens one at the run moment, never backdated. """ if get_open_period(conn) is not None: return # Already open — no-op ts = run_time.isoformat() conn.execute( "INSERT INTO monitoring_periods (started_at) VALUES (?)", (ts,), ) conn.commit() def close_period( conn: sqlite3.Connection, closed_at: datetime, end_cause: str, ) -> None: """Close the current open monitoring period. Spec §8.6: Pause with an open period closes it user_disabled. If no period is open, this is a no-op (pause otherwise). """ open_period = get_open_period(conn) if open_period is None: return # No-op ts = closed_at.isoformat() conn.execute( "UPDATE monitoring_periods SET ended_at = ?, end_cause = ? WHERE id = ?", (ts, end_cause, open_period["id"]), ) conn.commit() def get_open_period(conn: sqlite3.Connection) -> dict | None: """Return the currently open monitoring period, or None.""" cursor = conn.execute( "SELECT id, started_at, ended_at, end_cause " "FROM monitoring_periods WHERE ended_at IS NULL LIMIT 1" ) row = cursor.fetchone() if row is None: return None return { "id": row[0], "started_at": row[1], "ended_at": row[2], "end_cause": row[3], } def is_inside_period(conn: sqlite3.Connection, ts: datetime) -> bool: """Check if a timestamp falls inside any monitoring period. Spec §5.2: Wall-clock outside periods is excluded from numerator/denominator. """ ts_str = ts.isoformat() cursor = conn.execute( "SELECT 1 FROM monitoring_periods " "WHERE started_at <= ? AND (ended_at IS NULL OR ended_at > ?) " "LIMIT 1", (ts_str, ts_str), ) return cursor.fetchone() is not None def wall_clock_in_periods( conn: sqlite3.Connection, start: datetime, end: datetime, ) -> int: """Compute total wall-clock seconds between start and end that fall inside any monitoring period. Used for denominator computation (§5.2). """ start_str = start.isoformat() end_str = end.isoformat() cursor = conn.execute( "SELECT started_at, ended_at FROM monitoring_periods " "WHERE ended_at IS NULL OR ended_at > ? " "ORDER BY started_at", (start_str,), ) total = 0 for row in cursor.fetchall(): period_start = row[0] period_end = row[1] # None if open # Clip period to [start, end] effective_start = max(period_start, start_str) if period_end is not None: effective_end = min(period_end, end_str) else: effective_end = end_str if effective_start < effective_end: # Parse for arithmetic s = datetime.fromisoformat(effective_start) e = datetime.fromisoformat(effective_end) total += int((e - s).total_seconds()) return total