#!/usr/bin/env python3 """Write a sanitized Hermes ops snapshot for the unified dashboard. Run this as the Hermes instance owner (root for Vijay, uma for Bheem). It writes booleans, counts, timestamps, and short Git metadata only. It never copies tokens, state.db, logs, prompts, session content, or environment files. """ from __future__ import annotations import json import os import subprocess import tempfile from pathlib import Path from typing import Any HERMES_HOME = Path(os.getenv("HERMES_HOME", str(Path.home() / ".hermes"))) OUTPUT_PATH = Path(os.getenv("HERMES_OPS_EXPORT_PATH", str(HERMES_HOME / "ops-export.json"))) GATEWAY_SERVICE = os.getenv("HERMES_GATEWAY_SERVICE", "hermes-gateway.service") DASHBOARD_SERVICE = os.getenv("HERMES_DASHBOARD_SERVICE", "hermes-root-dashboard.service") BACKUP_TIMER = os.getenv("HERMES_BACKUP_TIMER", "hermes-root-backup.timer") BACKUP_REPO = Path(os.getenv("HERMES_BACKUP_REPO", str(Path.home() / "repos" / "bytelyst_hostinger_hermes_vm"))) def run(cmd: list[str], cwd: Path | None = None, timeout: int = 10) -> tuple[bool, str]: try: result = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, timeout=timeout, check=False) except (FileNotFoundError, subprocess.TimeoutExpired): return False, "" return True, result.stdout.strip() def probe_active(unit: str) -> dict[str, Any]: ran, out = run(["systemctl", "--user", "is-active", unit]) if not ran: ran, out = run(["systemctl", "is-active", unit]) active = out == "active" return {"active": active, "status": "up" if active else "down" if ran else "unknown"} def probe_enabled(unit: str) -> bool: ran, out = run(["systemctl", "--user", "is-enabled", unit]) if not ran: ran, out = run(["systemctl", "is-enabled", unit]) return ran and out == "enabled" def probe_timer(name: str) -> dict[str, Any]: active = probe_active(name) ran, out = run([ "systemctl", "--user", "show", name, "-p", "NextElapseUSecRealtime", "-p", "LastTriggerUSec", "--no-pager", ]) if not ran: ran, out = run([ "systemctl", "show", name, "-p", "NextElapseUSecRealtime", "-p", "LastTriggerUSec", "--no-pager", ]) props: dict[str, str | None] = {} for line in out.splitlines() if ran else []: key, _, value = line.partition("=") props[key] = value or None return { "name": name, "active": active["active"], "status": active["status"], "nextRun": props.get("NextElapseUSecRealtime"), "lastRun": props.get("LastTriggerUSec"), } def probe_repo(path: Path) -> dict[str, Any]: ran_head, head = run(["git", "rev-parse", "--short", "HEAD"], cwd=path) ran_branch, branch = run(["git", "branch", "--show-current"], cwd=path) ran_status, status = run(["git", "status", "--porcelain"], cwd=path) ran_commit, last_commit = run(["git", "log", "-1", "--format=%cI"], cwd=path) return { "path": str(path), "branch": branch if ran_branch and branch else None, "clean": ran_status and status == "", "head": head if ran_head and head else None, "lastCommitAt": last_commit if ran_commit and last_commit else None, "size": None, "status": "up" if ran_head else "unknown", } def restore_stats(path: Path) -> dict[str, int | None]: try: manifest = json.loads((path / "hermes_persistent_backup" / "MANIFEST.json").read_text(encoding="utf-8")) files = manifest.get("files") file_count = len(files) if isinstance(files, list) else None except Exception: file_count = None try: jobs = json.loads((path / "hermes_persistent_backup" / "cron" / "jobs.json").read_text(encoding="utf-8")) cron_jobs = jobs.get("jobs") if isinstance(jobs, dict) else jobs cron_count = len(cron_jobs) if isinstance(cron_jobs, list) else None except Exception: cron_count = None return {"restoredFileCount": file_count, "restoredCronJobs": cron_count} def write_atomic(path: Path, payload: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as tmp: json.dump(payload, tmp, indent=2, sort_keys=True) tmp.write("\n") tmp_path = Path(tmp.name) tmp_path.replace(path) path.chmod(0o644) def main() -> int: payload: dict[str, Any] = { "generatedAt": subprocess.check_output(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"], text=True).strip(), "gateway": {**probe_active(GATEWAY_SERVICE), "enabled": probe_enabled(GATEWAY_SERVICE)}, "dashboard": probe_active(DASHBOARD_SERVICE), "backupTimer": probe_timer(BACKUP_TIMER), "repo": probe_repo(BACKUP_REPO), "googleWorkspaceToken": (HERMES_HOME / "google_token.json").is_file(), } payload.update(restore_stats(BACKUP_REPO)) write_atomic(OUTPUT_PATH, payload) return 0 if __name__ == "__main__": raise SystemExit(main())