Explorer
/tmp/aa045_schema.py
← Zurück ↓ Download
"""
AA-045 Patch-Modul: Schema-Migration + Observability für den Multi-Source-Radar.
Wird von signal_db.init_db() importiert (idempotent).
"""

import json
import hashlib
from datetime import datetime, timezone


AA045_COLUMNS = [
    ("source_platform",  "TEXT DEFAULT ''"),
    ("external_id",      "TEXT DEFAULT ''"),
    ("observed_at",      "TEXT"),
    ("published_at",     "TEXT"),
    ("content_hash",     "TEXT DEFAULT ''"),
    ("cluster_id",       "TEXT DEFAULT ''"),
    ("engagement",       "TEXT DEFAULT ''"),
    ("processing_state", "TEXT DEFAULT 'neu'"),
    ("ad_start_at",      "TEXT"),
    ("last_seen_at",     "TEXT"),
    ("active_status",    "TEXT DEFAULT 'unknown'"),
]

AA045_INDEXES = [
    "CREATE INDEX IF NOT EXISTS idx_signals_external_id ON signals(external_id)",
    "CREATE INDEX IF NOT EXISTS idx_signals_cluster_id ON signals(cluster_id)",
    "CREATE INDEX IF NOT EXISTS idx_signals_source_platform ON signals(source_platform)",
    "CREATE INDEX IF NOT EXISTS idx_signals_meta_activity ON signals(source_type, active_status, last_seen_at)",
]


def apply_migration(c) -> list[str]:
    applied = []
    existing = {row[1] for row in c.execute("PRAGMA table_info(signals)").fetchall()}
    for col, col_type in AA045_COLUMNS:
        if col not in existing:
            c.execute(f"ALTER TABLE signals ADD COLUMN {col} {col_type}")
            applied.append(col)
    for stmt in AA045_INDEXES:
        try:
            c.execute(stmt)
        except Exception:
            pass
    c.execute("""
        CREATE TABLE IF NOT EXISTS source_runs (
            id INTEGER PRIMARY KEY AUTOINCREMENT, source_name TEXT NOT NULL,
            run_at TEXT NOT NULL, last_success TEXT, last_error TEXT,
            items_seen INTEGER DEFAULT 0, items_new INTEGER DEFAULT 0,
            items_duplicate INTEGER DEFAULT 0, items_clustered INTEGER DEFAULT 0,
            items_rejected INTEGER DEFAULT 0, runtime_seconds REAL DEFAULT 0,
            quota_usage TEXT DEFAULT ''
        )
    """)
    return applied


def content_hash_for(title: str, summary: str = "") -> str:
    norm = " ".join(f"{title} {summary}".lower().split())
    return hashlib.sha256(norm.encode("utf-8")).hexdigest()[:16]


def record_run(conn, source_name: str, t0_monotonic: float, items_seen: int,
               items_new: int, items_duplicate: int, items_clustered: int,
               items_rejected: int, error: str | None, quota_usage: str = ""):
    now = datetime.now(timezone.utc).isoformat()
    conn.execute("""INSERT INTO source_runs
        (source_name,run_at,last_success,last_error,items_seen,items_new,
         items_duplicate,items_clustered,items_rejected,runtime_seconds,quota_usage)
        VALUES (?,?,?,?,?,?,?,?,?,?,?)""", (source_name, now, None if error else now,
        error, items_seen, items_new, items_duplicate, items_clustered,
        items_rejected, 0.0, quota_usage))