from __future__ import annotations import json import sqlite3 import time from datetime import datetime, timezone from pathlib import Path from urllib.error import URLError from urllib.request import Request, urlopen ROOT = Path(__file__).resolve().parents[1] DB_PATH = ROOT / ".data_local" / "mvp_cc.db" OUT_DIR = ROOT / ".artifacts" / "track12-monitor" OUT_DIR.mkdir(parents=True, exist_ok=True) OUT_PATH = OUT_DIR / "latest.log" BASE_URL = "http://127.0.0.1:8080" AUTH_BASE_URL = "http://127.0.0.1:8001" USERNAME = "operator" PASSWORD = "op12345" ROLE = "operator" WINDOW_SECONDS = 180 POLL_SECONDS = 1.0 def iso_now() -> str: return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds") def write_line(handle, label: str, payload) -> None: handle.write(f"[{iso_now()}] {label} {json.dumps(payload, ensure_ascii=False, sort_keys=True)}\n") handle.flush() def login_headers() -> dict[str, str]: body = json.dumps( { "username": USERNAME, "password": PASSWORD, "role": ROLE, } ).encode("utf-8") req = Request( f"{AUTH_BASE_URL}/auth/login", headers={"Content-Type": "application/json"}, data=body, method="POST", ) with urlopen(req, timeout=10) as response: payload = json.loads(response.read().decode("utf-8", errors="replace")) token = str(payload.get("access_token") or "").strip() if not token: raise RuntimeError("auth/login did not return access_token") return {"Authorization": f"Bearer {token}"} def fetch_json(path: str, headers: dict[str, str]): req = Request( f"{BASE_URL}{path}", headers=headers, method="GET", ) with urlopen(req, timeout=10) as response: body = response.read().decode("utf-8", errors="replace") return json.loads(body) def latest_events(conn: sqlite3.Connection, limit: int = 6) -> list[dict]: rows = conn.execute( """ select event_id, event_type, call_id, interaction_id, payload_json, created_at from voice_events order by id desc limit ? """, (limit,), ).fetchall() result = [] for row in rows: payload = row["payload_json"] try: payload = json.loads(payload) except Exception: pass result.append( { "event_id": row["event_id"], "event_type": row["event_type"], "call_id": row["call_id"], "interaction_id": row["interaction_id"], "payload": payload, "created_at": row["created_at"], } ) return result def latest_links(conn: sqlite3.Connection, limit: int = 4) -> list[dict]: rows = conn.execute( """ select call_id, interaction_id, status, telephony_status, claimed_by_user, operator_extension, started_at, connected_at, ended_at from asterisk_call_links order by id desc limit ? """, (limit,), ).fetchall() return [dict(row) for row in rows] def main() -> int: with OUT_PATH.open("w", encoding="utf-8") as handle: write_line(handle, "monitor", {"status": "started", "window_seconds": WINDOW_SECONDS}) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row headers = login_headers() write_line(handle, "auth", {"status": "ok", "user": USERNAME, "role": ROLE}) end_at = time.time() + WINDOW_SECONDS seen = { "live": None, "recent": None, "events": None, "links": None, } try: while time.time() < end_at: try: live = fetch_json("/proxy/asterisk-bridge/asterisk/live-calls", headers) if live != seen["live"]: seen["live"] = live write_line(handle, "live-calls", live) except URLError as exc: write_line(handle, "live-calls-error", {"error": str(exc)}) try: recent = fetch_json("/proxy/asterisk-bridge/asterisk/recent-calls", headers) if recent != seen["recent"]: seen["recent"] = recent write_line(handle, "recent-calls", recent) except URLError as exc: write_line(handle, "recent-calls-error", {"error": str(exc)}) events = latest_events(conn) if events != seen["events"]: seen["events"] = events write_line(handle, "voice-events", events) links = latest_links(conn) if links != seen["links"]: seen["links"] = links write_line(handle, "call-links", links) time.sleep(POLL_SECONDS) finally: conn.close() write_line(handle, "monitor", {"status": "finished"}) return 0 if __name__ == "__main__": raise SystemExit(main())