"""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 _anchor(cfg: Config, sess) -> int: """Anchor core (injectable session for tests). Returns 0 ok / 0 skip.""" from cayceops.components.publisher import nostr from cayceops.config import ConfigError from cayceops.models import AuditEntry, audit if not cfg.nostr_private_key or not cfg.nostr_relays: raise ConfigError( "anchor requires CAYCE_OPS_NOSTR_PRIVATE_KEY and CAYCE_OPS_NOSTR_RELAYS" ) if not cfg.deployment_id: raise ConfigError("anchor requires CAYCE_OPS_DEPLOYMENT_ID") last = ( sess.query(AuditEntry) .filter(AuditEntry.entry_hash.isnot(None)) .order_by(AuditEntry.id.desc()) .first() ) if last is None: print("nothing to anchor: chain empty") return 0 today = datetime.now(UTC).date() last_anchor = ( sess.query(AuditEntry) .filter(AuditEntry.action == "anchor") .order_by(AuditEntry.id.desc()) .first() ) if last_anchor is not None and last_anchor.ts.date() == today: print("already anchored today") return 0 secret = nostr.normalize_key(cfg.nostr_private_key) event = nostr.build_event( secret, f"cayce-ops audit anchor deployment={cfg.deployment_id} " f"last_hash={last.entry_hash} entries={last.id}", tags=[["t", "cayce-ops"], ["t", "audit-anchor"]], ) if not nostr.verify_event(event): raise RuntimeError("anchor event failed self-verification") results = nostr.publish(cfg.nostr_relays, event) audit( sess, actor="daemon:monitor", action="anchor", detail={ "last_hash": last.entry_hash, "last_entry": last.id, "event_id": event["id"], "relays": results, "deployment": cfg.deployment_id, }, ) print(f"anchored: {event['id']}") return 0 def anchor(cfg: Config | None = None) -> int: """CLI: anchor once per day (external anchoring, audit round 2 point 4).""" from cayceops.config import ConfigError, load from cayceops.db import engine cfg = cfg or load() try: with session_scope() as sess: try: return _anchor(cfg, sess) except ConfigError as exc: print(f"anchor refused: {exc}") return 2 except Exception as exc: # noqa: BLE001 — CLI boundary print(f"anchor failed: {str(exc)[:200]}") return 1 finally: engine().dispose() 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()