"""monitor — static status page builder. Reads the shared database and renders a self-contained HTML file (no external assets, no JS, everything escaped). Contains NO sensitive data: daemon health, proposal queue counts, and recent audit activity (actor/action/result only — no email bodies, no detail payloads, no URLs). """ from __future__ import annotations import html from datetime import UTC, datetime from sqlalchemy import func, select from cayceops.config import Config from cayceops.db import session_scope from cayceops.models import ( AuditEntry, ContentItem, DaemonRun, MailMessage, Proposal, ProposalStatus, ) DAEMONS = ("scanner", "executor", "maild", "publisher", "monitor") _STYLE = """""" def _fmt_ts(dt) -> str: return dt.strftime("%Y-%m-%d %H:%M:%SZ") if dt else "—" def _daemon_rows(sess) -> str: rows = [] for name in DAEMONS: last = ( sess.query(DaemonRun) .filter(DaemonRun.daemon == name) .order_by(DaemonRun.started_at.desc()) .first() ) if last is None: rows.append( "" + name + "never run—" ) else: cls = "ok" if last.status == "ok" else "error" when = _fmt_ts(last.finished_at or last.started_at) rows.append( "" + html.escape(name) + "" + '' + html.escape(last.status) + "" + "" + when + "" ) head = "daemonlast statuslast run" return "" + head + "".join(rows) + "
" def _queue_cards(sess) -> str: counts = dict( sess.query(Proposal.status, func.count(Proposal.id)).group_by(Proposal.status).all() ) cards = [] for status in ProposalStatus: value = counts.get(status, 0) cards.append( "
" + html.escape(status.value) + ": " + str(value) + "
" ) return "".join(cards) def _recent_audit(sess, limit: int = 20) -> str: rows = [] query = sess.query(AuditEntry).order_by(AuditEntry.ts.desc()).limit(limit) for entry in query.all(): cls = "ok" if entry.result == "ok" else "error" rows.append( "" + _fmt_ts(entry.ts) + "" + "" + html.escape(entry.actor) + "" + "" + html.escape(entry.action) + "" + '' + html.escape(entry.result) + "" ) body = "".join(rows) or "no activity yet" return ( "" + body + "
time (utc)actoractionresult
" ) def _build(cfg: Config, sess) -> str: now = datetime.now(UTC) mail_unprocessed = ( sess.query(func.count(MailMessage.id)) .filter( MailMessage.direction == "in", MailMessage.draft_status.in_(["drafted", "none"]), MailMessage.classification.isnot(None), ) .scalar() ) content_pending = ( sess.query(func.count(ContentItem.id)) .filter(ContentItem.status.in_(["draft", "approved"])) .scalar() ) generated = _fmt_ts(now) page = f""" cayce-ops status{_STYLE}

cayce-ops · status

generated {generated} · static page, no sensitive data by design

daemon health

{_daemon_rows(sess)}

proposal queue

{_queue_cards(sess)}

gates

mail drafts awaiting approval: {mail_unprocessed}
content items draft/approved: {content_pending}

recent audit activity

{_recent_audit(sess)}

audit entries: {sess.scalar(select(func.count(AuditEntry.id)))} total

""" import os os.makedirs(os.path.dirname(cfg.monitor_output) or ".", exist_ok=True) with open(cfg.monitor_output, "w", encoding="utf-8") as fh: fh.write(page) return cfg.monitor_output def build(cfg: Config | None = None) -> int: """CLI entry: render the status page inside a DaemonRun bookkeeping wrap.""" from cayceops.config import load from cayceops.db import engine cfg = cfg or load() try: with session_scope() as sess: run_row = DaemonRun(daemon="monitor") sess.add(run_row) sess.flush() try: out = _build(cfg, sess) except Exception as exc: # noqa: BLE001 — daemon boundary run_row.status = "error" run_row.detail = {"error": str(exc)[:300]} run_row.finished_at = datetime.now(UTC) return 1 run_row.status = "ok" run_row.detail = {"output": out} run_row.finished_at = datetime.now(UTC) print(out) return 0 finally: engine().dispose()