02b1d155d4
The autonomous agent writes all observations to agent.* tables consumed by Homelab Command on port 8765. Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
4.6 KiB
Python
153 lines
4.6 KiB
Python
"""PostgreSQL — alle observaties voor dashboard :8765."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Optional
|
|
|
|
import psycopg2
|
|
import psycopg2.extras
|
|
|
|
from agent.brain import AgentDecision
|
|
from agent.observer import ObservationReport
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _pg_enabled() -> bool:
|
|
return os.getenv("PG_DISABLED", "").lower() not in ("1", "true", "yes")
|
|
|
|
|
|
def _connect():
|
|
url = os.getenv("DATABASE_URL", "").strip()
|
|
if url:
|
|
return psycopg2.connect(url)
|
|
return psycopg2.connect(
|
|
host=os.getenv("PG_HOST", "192.168.1.211"),
|
|
port=int(os.getenv("PG_PORT", "5433")),
|
|
user=os.getenv("PG_USER", "mo"),
|
|
password=os.getenv("PG_PASSWORD", ""),
|
|
dbname=os.getenv("PG_DATABASE", "homelab"),
|
|
)
|
|
|
|
|
|
def _parse_ts(ts: str) -> datetime:
|
|
if ts.endswith("Z"):
|
|
ts = ts[:-1] + "+00:00"
|
|
return datetime.fromisoformat(ts)
|
|
|
|
|
|
def persist_observation(report: ObservationReport, decision: AgentDecision) -> Optional[int]:
|
|
"""Schrijf run + findings naar agent.*; retourneert run_id."""
|
|
if not _pg_enabled():
|
|
return None
|
|
summary = report.to_dict().get("summary") or {}
|
|
decision_json = {
|
|
"alert": decision.alert,
|
|
"severity": decision.severity,
|
|
"title": decision.title,
|
|
"body": decision.body,
|
|
"fingerprint": decision.fingerprint,
|
|
"actions": decision.actions,
|
|
}
|
|
try:
|
|
conn = _connect()
|
|
conn.autocommit = False
|
|
with conn.cursor() as cur:
|
|
cur.execute("SET search_path TO agent, public;")
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO observation_runs
|
|
(observed_at, total_checks, failed_checks, summary, decision)
|
|
VALUES (%s, %s, %s, %s::jsonb, %s::jsonb)
|
|
RETURNING id
|
|
""",
|
|
(
|
|
_parse_ts(report.timestamp),
|
|
int(summary.get("total", len(report.findings))),
|
|
int(summary.get("failed", sum(1 for f in report.findings if not f.ok))),
|
|
json.dumps(report.to_dict(), default=str),
|
|
json.dumps(decision_json, default=str),
|
|
),
|
|
)
|
|
run_id = cur.fetchone()[0]
|
|
rows = [
|
|
(
|
|
run_id,
|
|
f.kind,
|
|
f.name,
|
|
f.ok,
|
|
f.detail,
|
|
json.dumps(f.meta or {}, default=str),
|
|
)
|
|
for f in report.findings
|
|
]
|
|
if rows:
|
|
psycopg2.extras.execute_batch(
|
|
cur,
|
|
"""
|
|
INSERT INTO findings (run_id, kind, name, ok, detail, meta)
|
|
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
|
|
""",
|
|
rows,
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
logger.info("PostgreSQL run %s: %d findings", run_id, len(rows))
|
|
return int(run_id)
|
|
except Exception as e:
|
|
logger.warning("PostgreSQL observatie mislukt: %s", e)
|
|
return None
|
|
|
|
|
|
def persist_incident(
|
|
run_id: Optional[int],
|
|
fingerprint: str,
|
|
severity: str,
|
|
title: str,
|
|
body: str,
|
|
notified: bool,
|
|
) -> None:
|
|
if not _pg_enabled():
|
|
return
|
|
try:
|
|
conn = _connect()
|
|
conn.autocommit = True
|
|
with conn.cursor() as cur:
|
|
cur.execute("SET search_path TO agent, public;")
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO incidents (run_id, fingerprint, severity, title, body, notified)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
""",
|
|
(run_id, fingerprint, severity, title, body, notified),
|
|
)
|
|
conn.close()
|
|
except Exception as e:
|
|
logger.warning("PostgreSQL incident mislukt: %s", e)
|
|
|
|
|
|
def was_notified_recently_pg(fingerprint: str, minutes: int) -> bool:
|
|
if not _pg_enabled():
|
|
return False
|
|
try:
|
|
conn = _connect()
|
|
with conn.cursor() as cur:
|
|
cur.execute("SET search_path TO agent, public;")
|
|
cur.execute(
|
|
"""
|
|
SELECT 1 FROM incidents
|
|
WHERE fingerprint = %s AND notified = true
|
|
AND created_at > NOW() - (%s || ' minutes')::interval
|
|
LIMIT 1
|
|
""",
|
|
(fingerprint, str(int(minutes))),
|
|
)
|
|
row = cur.fetchone()
|
|
conn.close()
|
|
return row is not None
|
|
except Exception:
|
|
return False
|