#!/usr/bin/env python3 """maintenance_check.py — Router mattutino manutenzione vault. Gira via cron (09:00 ogni mattina): 1. Legge msg-e2a di Elon con risposte a manutenzione-notturna e checklist-settimanale 2. Adrian fa da gate: filtra e porta nel cockpit solo l'actionable per Mauro 3. Ack i msg-e2a letti 4. Se msg-a2e pendenti da >48h senza ack → Telegram a Mauro (freeze/assenza Elon) 5. Todo check: scaduti/in scadenza oggi → cockpit; completati recenti → consapevolezza reattività """ import json import os import sqlite3 from datetime import datetime, timezone from pathlib import Path import httpx from dotenv import load_dotenv load_dotenv(Path(__file__).parent.parent / ".env") VAULT_DB = Path("/mnt/ssd/data/adrian-ops/adrian.db") TG_TOKEN = os.getenv("ADRIAN_BOT_TOKEN", "") TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "") ACK_TIMEOUT_HOURS = 48 MANUTENZIONE_SUBJECTS = {"manutenzione-notturna", "checklist-settimanale"} def tg_notify(text: str): if not TG_TOKEN or not TG_CHAT_ID: return try: httpx.post( f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage", json={"chat_id": TG_CHAT_ID, "text": text}, timeout=10, ) except Exception: pass def db_write_cockpit(text: str, conn: sqlite3.Connection | None = None): own = conn is None if own: conn = sqlite3.connect(VAULT_DB) conn.execute("UPDATE items SET body=? WHERE id='da-adrian-a-mauro'", (text,)) if own: conn.commit() conn.close() def ack_msg(conn: sqlite3.Connection, msg_id: str): ack_id = "ack-" + msg_id[4:] ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") meta = json.dumps({"tipo": "agent_memory", "acks": msg_id, "by": "adrian", "ts": ts}) conn.execute( "INSERT OR IGNORE INTO items(id, type, body, file, meta) VALUES (?,?,?,NULL,?)", (ack_id, "os", "", meta), ) def is_actionable(body: str) -> bool: """Gate: la risposta di Elon contiene qualcosa che vale la pena portare a Mauro? Filtra via le risposte con solo 'nessuno' in tutte le sezioni.""" body_low = body.lower() # Se sia AUTONOMO-FATTO che SEGNALAZIONI dicono nessuno → non portare a Mauro tutto_nessuno = ( "autonomo-fatto:" in body_low and "segnalazioni:" in body_low and body_low.count("nessuno") >= 2 ) return not tutto_nessuno def format_for_cockpit(responses: list[dict], subject: str) -> str: label = "Checklist settimanale" if subject == "checklist-settimanale" else "Manutenzione notturna" lines = [f"📋 {label} — risposta Elon:"] for r in responses: lines.append("") lines.append(r["body"].strip()) return "\n".join(lines) def check_pending_acks(conn: sqlite3.Connection, now: datetime) -> list[tuple]: """Ritorna msg-a2e di manutenzione senza ack da >ACK_TIMEOUT_HOURS.""" rows = conn.execute(""" SELECT i.id, i.meta FROM items i WHERE i.id LIKE 'msg-a2e-%' AND json_extract(i.meta, '$.subject') IN ('manutenzione-notturna', 'checklist-settimanale') AND NOT EXISTS ( SELECT 1 FROM items a WHERE a.id = 'ack-' || substr(i.id, 5) ) """).fetchall() scaduti = [] for row in rows: try: meta = json.loads(row[1] or "{}") ts_str = meta.get("ts", "") ts = datetime.strptime(ts_str[:15], "%Y%m%dT%H%M%S").replace(tzinfo=timezone.utc) age_h = (now - ts).total_seconds() / 3600 if age_h > ACK_TIMEOUT_HOURS: scaduti.append((row[0], round(age_h), meta.get("subject", ""))) except Exception: pass return scaduti def todo_check(conn: sqlite3.Connection, today: str, yesterday: str) -> str | None: """Controlla todos scaduti/in scadenza + completati di recente. Ritorna testo cockpit o None.""" # Scaduti (due_date < oggi, ancora pending) scaduti = conn.execute(""" SELECT json_extract(meta,'$.title'), json_extract(meta,'$.due_date') FROM items WHERE type='todo' AND json_extract(meta,'$.status')='pending' AND json_extract(meta,'$.due_date') IS NOT NULL AND json_extract(meta,'$.due_date') < ? ORDER BY json_extract(meta,'$.due_date') ASC """, (today,)).fetchall() # In scadenza oggi (due_date = oggi, ancora pending) oggi = conn.execute(""" SELECT json_extract(meta,'$.title') FROM items WHERE type='todo' AND json_extract(meta,'$.status')='pending' AND json_extract(meta,'$.due_date') = ? ORDER BY id ASC """, (today,)).fetchall() # Completati nell'ultima giornata (aggiornati dopo yesterday, status=done) completati = conn.execute(""" SELECT json_extract(meta,'$.title'), json_extract(meta,'$.updated_at') FROM items WHERE type='todo' AND json_extract(meta,'$.status')='done' AND json_extract(meta,'$.updated_at') >= ? ORDER BY json_extract(meta,'$.updated_at') DESC LIMIT 10 """, (yesterday + "T00:00:00",)).fetchall() if not scaduti and not oggi and not completati: return None lines = ["📋 Todo check:"] if scaduti: lines.append("") lines.append("⚠️ Scaduti (non completati):") for title, due in scaduti: lines.append(f" • {title} [scaduto {due}]") if oggi: lines.append("") lines.append("📌 In scadenza oggi:") for (title,) in oggi: lines.append(f" • {title}") if completati: lines.append("") lines.append("✅ Completati di recente:") for title, upd in completati: when = (upd or "")[:10] lines.append(f" • {title} [{when}]") return "\n".join(lines) def main(): now = datetime.now(timezone.utc) conn = sqlite3.connect(VAULT_DB) # 1. Leggi risposte di Elon non ancora lette (msg-e2a senza ack-e2a) # relative a trigger di manutenzione (per subject del msg-a2e corrispondente) elon_responses = conn.execute(""" SELECT e.id, e.body, json_extract(e.meta,'$.ts') as ts, json_extract(a2e.meta,'$.subject') as subject FROM items e LEFT JOIN items a2e ON a2e.id = json_extract(e.meta,'$.acks_msg') WHERE e.id LIKE 'msg-e2a-%' AND NOT EXISTS ( SELECT 1 FROM items ack WHERE ack.id = 'ack-' || substr(e.id, 5) ) AND ( json_extract(e.meta,'$.subject') IN ('manutenzione-notturna','checklist-settimanale') OR json_extract(e.meta,'$.in_reply_to_subject') IN ('manutenzione-notturna','checklist-settimanale') ) ORDER BY ts ASC """).fetchall() # Fallback: se Elon non mette subject nel meta, prendo tutti i msg-e2a non letti # e controllo se c'è un msg-a2e di manutenzione pendente correlato per timestamp if not elon_responses: elon_responses = conn.execute(""" SELECT e.id, e.body, json_extract(e.meta,'$.ts') as ts, 'manutenzione-notturna' as subject FROM items e WHERE e.id LIKE 'msg-e2a-%' AND NOT EXISTS ( SELECT 1 FROM items ack WHERE ack.id = 'ack-' || substr(e.id, 5) ) ORDER BY ts DESC LIMIT 3 """).fetchall() routed_to_mauro = False for row in elon_responses: msg_id = row[0] body = row[1] or "" subject = row[3] or "manutenzione-notturna" print(f"[check] Risposta Elon: {msg_id} (subject={subject})") ack_msg(conn, msg_id) if is_actionable(body): cockpit_text = format_for_cockpit([{"body": body}], subject) db_write_cockpit(cockpit_text, conn) tg_notify(f"🔔 {cockpit_text[:300]}") routed_to_mauro = True print(f"[check] → Portato a Mauro nel cockpit.") else: print(f"[check] → Nessun actionable (tutto 'nessuno') — non disturbo Mauro.") conn.commit() # 2. Todo check: scaduti, in scadenza oggi, completati di recente today = now.strftime("%Y-%m-%d") from datetime import timedelta yesterday = (now - timedelta(days=1)).strftime("%Y-%m-%d") todo_msg = todo_check(conn, today, yesterday) if todo_msg: # Appende al cockpit (se Elon ha già scritto qualcosa, aggiungo in coda) existing = conn.execute("SELECT body FROM items WHERE id='da-adrian-a-mauro'").fetchone() cockpit_body = existing[0] if existing and existing[0] else "" combined = (cockpit_body + "\n\n" + todo_msg).strip() if cockpit_body else todo_msg db_write_cockpit(combined, conn) tg_notify(f"🔔 {todo_msg[:300]}\nhttps://cockpit.privcloud.dev") print(f"[check] Todo: {todo_msg[:200]}") else: print("[check] Todo: nessun item scaduto/in scadenza oggi.") conn.commit() # 3. Controlla msg-a2e scaduti (nessuna risposta da Elon in 48h) scaduti = check_pending_acks(conn, now) conn.close() if scaduti: lines = [f"⚠️ Manutenzione vault: {len(scaduti)} trigger senza risposta da Elon (>{ACK_TIMEOUT_HOURS}h)"] for msg_id, age_h, subj in scaduti: lines.append(f" {msg_id} ({age_h}h, {subj})") lines.append("→ Controlla se Elon ha aperto sessione.") msg = "\n".join(lines) print(msg) tg_notify(msg) elif not elon_responses: print("[check] Nessuna risposta di Elon in attesa — ok.") if __name__ == "__main__": main()