"""Atlas Brain - the collector behind Atlas Operations. Pulls every signal into one atlas-state.json: mail : 7 IMAP mailboxes (unread + recent headers), strictly read-only calendar : Google secret-iCal feeds, recurrences expanded, next 14 days command : atlas-01 today.json (attention / mesh) business : atlas-01 atlas_data.json (Odoo CRM) Then synthesizes a proactive brief via the local model-router. Supersedes jarvis-brain-hourly.py / jarvis-brain-hourly-v2.py. Run: python atlas_brain.py full collect + synth + write state python atlas_brain.py --quick collect only, skip the LLM synth (fast 15-min tick) """ from __future__ import annotations import base64 import email import imaplib import json import os import re import sys import time import traceback from datetime import datetime, timezone, timedelta from email.header import decode_header, make_header from pathlib import Path import httpx HOME = Path.home() ATLAS = HOME / ".config" / "atlas" OPS_DIR = ATLAS / "operations" CACHE = ATLAS / "cache" OPS_DIR.mkdir(parents=True, exist_ok=True) CACHE.mkdir(parents=True, exist_ok=True) STATE_JSON = OPS_DIR / "atlas-state.json" SPOKEN_JSON = ATLAS / "atlas-brief-spoken.json" MAIL_CFG = ATLAS / "mail-accounts.json" CAL_CFG = ATLAS / "calendar-feeds.json" CAL_CACHE = CACHE / "calendar-last.json" LOG = ATLAS / "atlas-brain.log" ATLAS01 = os.environ.get("ATLAS01_URL", "http://atlas-01.tailab40ed.ts.net") A01_USER = "atlas" A01_PASS = os.environ.get("ATLAS_BASIC_PASS", "") SERVICES = [ {"name": "Command Center", "url": "https://atlascorporation.nl/command"}, {"name": "Nextcloud", "url": "http://atlas-01:8443"}, {"name": "MeshCentral", "url": "https://mesh.atlascorporation.nl/"}, {"name": "Odoo", "url": "http://:8069"}, {"name": "Atlas Portal", "url": "http://atlas-01/portal.html"}, {"name": "n8n", "url": "http://:5678"}, {"name": "Stalwart", "url": "http://:8080"}, {"name": "Authentik", "url": "http://:9000"}, {"name": "Ollama", "url": "http://localhost:11434"}, ] sys.path.insert(0, str(ATLAS)) try: import model_router except Exception: model_router = None def log(msg: str) -> None: try: with LOG.open("a", encoding="utf-8") as f: f.write(f"[{datetime.now().isoformat(timespec='seconds')}] {msg}\n") except Exception: pass print(f"[brain] {msg}") def _safe(fn, name: str): try: return fn() except Exception as e: log(f"{name} FAILED: {e}") log(traceback.format_exc()) return {"error": f"{name}: {e}"} def _decode(s) -> str: if not s: return "" try: return str(make_header(decode_header(s))) except Exception: return str(s) def _iso(d) -> str: if isinstance(d, datetime): if d.tzinfo is None: d = d.replace(tzinfo=timezone.utc) return d.astimezone().isoformat(timespec="minutes") return str(d) def _strip_md(text: str) -> str: t = re.sub(r"```.*?```", "", text or "", flags=re.S) t = re.sub(r"[`*_#>]", "", t) t = re.sub(r"^\s*[-+]\s+", "", t, flags=re.M) t = re.sub(r"^\s*\d+\.\s+", "", t, flags=re.M) t = re.sub(r"\n{2,}", ". ", t) t = re.sub(r"\n", " ", t) t = re.sub(r"\s{2,}", " ", t) return t.strip() def _extract_section(md: str, name: str) -> str: m = re.search(rf"##\s*{re.escape(name)}\s*\n(.*?)(?:\n##\s|\Z)", md or "", re.S) return m.group(1).strip() if m else "" # ---------------- MAIL ---------------- def collect_mail() -> dict: if not MAIL_CFG.exists(): return {"error": "mail-accounts.json not found", "accounts": [], "total_unread": 0} accounts = json.loads(MAIL_CFG.read_text(encoding="utf-8")) out, total_unread = [], 0 for acc in accounts: rec = {"label": acc["label"], "group": acc.get("group", ""), "total": 0, "unread": 0, "recent": [], "error": None} _last_err = None for _attempt in range(2): try: M = imaplib.IMAP4_SSL(acc["host"], acc.get("port", 993), timeout=25) M.login(acc["user"], acc["password"]) M.select("INBOX", readonly=True) ids = M.search(None, "ALL")[1][0].split() uids = M.search(None, "UNSEEN")[1][0].split() rec["total"] = len(ids) rec["unread"] = len(uids) total_unread += len(uids) for mid in reversed((uids or ids)[-6:]): try: md = M.fetch(mid.decode(), "(BODY.PEEK[HEADER.FIELDS (SUBJECT FROM DATE)])")[1] raw = next((p[1] for p in md if isinstance(p, tuple)), b"") msg = email.message_from_bytes(raw) rec["recent"].append({ "subject": _decode(msg.get("Subject"))[:160], "from": _decode(msg.get("From"))[:120], "date": (msg.get("Date") or "")[:40], "unread": mid in uids, }) except Exception as fe: log(f" fetch glitch {acc['label']}: {fe}") M.logout() _last_err = None break # success — exit retry loop except Exception as e: _last_err = e if _attempt == 0: log(f" mail retry {acc['label']}: {e}") time.sleep(1) if _last_err is not None: rec["error"] = str(_last_err)[:200] out.append(rec) log(f"mail {acc['label']}: {rec['unread']} unread / {rec['total']}" + (f" ERR {rec['error']}" if rec["error"] else "")) return {"error": None, "accounts": out, "total_unread": total_unread, "total_accounts": len(out)} # ---------------- CALENDAR ---------------- def _split_events(events: list, errmsg=None) -> dict: """Sort events and split into today vs upcoming, by the current local date.""" events = sorted(events, key=lambda e: e.get("start", "")) today_str = datetime.now().strftime("%Y-%m-%d") return { "error": errmsg, "today": [e for e in events if e.get("start", "")[:10] == today_str], "upcoming": [e for e in events if e.get("start", "")[:10] > today_str][:40], "count": len(events), } def collect_calendar(days: int = 14) -> dict: cfg = json.loads(CAL_CFG.read_text(encoding="utf-8")) if CAL_CFG.exists() else {} feeds = cfg.get("feeds", []) if not feeds: # No live feed: re-split the cached events by today's date so a seed # stays valid as days pass. if CAL_CACHE.exists(): try: cached = json.loads(CAL_CACHE.read_text(encoding="utf-8")) res = _split_events(cached.get("events", []), "geen live feed - getoond uit cache") res["feeds"] = 0 return res except Exception as e: return {"error": f"cache unreadable: {e}", "today": [], "upcoming": [], "feeds": 0, "count": 0} return {"error": "no calendar feeds configured", "today": [], "upcoming": [], "feeds": 0, "count": 0} try: import recurring_ical_events from icalendar import Calendar except Exception as e: return {"error": f"ical libs missing ({e}) - run: pip install recurring-ical-events icalendar", "today": [], "upcoming": [], "feeds": len(feeds), "count": 0} now = datetime.now().astimezone() end = now + timedelta(days=days) events, errors = [], [] for feed in feeds: try: r = httpx.get(feed["url"], timeout=25, follow_redirects=True) r.raise_for_status() cal = Calendar.from_ical(r.content) for ev in recurring_ical_events.of(cal).between(now, end): start = ev.get("DTSTART").dt dtend = ev.get("DTEND") endt = dtend.dt if dtend else start events.append({ "summary": str(ev.get("SUMMARY", "(no title)")), "start": _iso(start), "end": _iso(endt), "location": str(ev.get("LOCATION", "")), "calendar": feed.get("label", ""), "kind": feed.get("kind", ""), "all_day": not isinstance(start, datetime), }) except Exception as e: errors.append(f"{feed.get('label', '?')}: {str(e)[:120]}") if events: try: CAL_CACHE.write_text(json.dumps( {"events": events, "saved_at": now.isoformat(timespec="seconds")}, indent=2, ensure_ascii=False), encoding="utf-8") except Exception: pass res = _split_events(events, "; ".join(errors) if errors else None) res["feeds"] = len(feeds) return res # ---------------- COMMAND / BUSINESS ---------------- def _fetch_basic(url: str, timeout: int = 8): try: creds = base64.b64encode(f"{A01_USER}:{A01_PASS}".encode()).decode() r = httpx.get(url, headers={"Authorization": f"Basic {creds}"}, timeout=timeout, follow_redirects=True) r.raise_for_status() return r.json() except Exception as e: return {"_error": str(e)[:160]} def collect_command() -> dict: today = _fetch_basic(f"{ATLAS01}/today.json") biz = _fetch_basic(f"{ATLAS01}/atlas_data.json") online = not (isinstance(today, dict) and today.get("_error")) return {"today": today, "business": biz, "atlas01_online": online} # ---------------- SERVICE HEALTH ---------------- LOCAL_SERVICES = [ {"name": "Ollama", "url": "http://localhost:11434/api/tags"}, {"name": "Operations", "url": "http://localhost:7799/"}, {"name": "Atlas CFO", "url": "http://localhost:7902/health"}, {"name": "Learning Agent", "url": "http://localhost:7901/health"}, {"name": "Atlas Portal", "url": "http:///portal.html"}, ] def collect_service_health() -> list: results = [] for svc in LOCAL_SERVICES: try: r = httpx.get(svc["url"], timeout=4, follow_redirects=True) results.append({"name": svc["name"], "url": svc["url"], "ok": r.status_code < 400, "status": r.status_code}) except Exception as e: results.append({"name": svc["name"], "url": svc["url"], "ok": False, "status": 0, "error": str(e)[:80]}) return results # ---------------- OLLAMA ---------------- def collect_ollama() -> dict: """Get Ollama model list and count.""" try: r = httpx.get("http://localhost:11434/api/tags", timeout=5) if r.status_code != 200: return {"ok": False, "status": r.status_code} models = r.json().get("models", []) return {"ok": True, "model_count": len(models), "models": [m.get("name", "") for m in models]} except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- SYNCTHING ---------------- def collect_syncthing() -> dict: """Check Syncthing sync health via local REST API.""" import xml.etree.ElementTree as ET import subprocess key_path = HOME / "AppData" / "Local" / "Syncthing" / "config.xml" try: tree = ET.parse(key_path) api_key = (tree.find(".//apikey") or tree.find(".//{*}apikey")) api_key = api_key.text if api_key is not None else "" except Exception as e: return {"ok": False, "error": f"key_read: {e}"} if not api_key: return {"ok": False, "error": "apikey not found in config.xml"} try: r = httpx.get("http://localhost:8384/rest/system/status", headers={"X-API-Key": api_key}, timeout=5) d = r.json() uptime_h = round(d.get("uptime", 0) / 3600, 1) # Get folder completion summary fr = httpx.get("http://localhost:8384/rest/db/completion", headers={"X-API-Key": api_key}, timeout=5) completion = round(fr.json().get("completion", 0), 1) if fr.status_code == 200 else None return {"ok": True, "uptime_h": uptime_h, "completion_pct": completion, "myID_short": d.get("myID", "")[:8]} except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- TAILSCALE ---------------- def collect_tailscale() -> dict: """Get Tailscale mesh node status via CLI.""" import subprocess exe = r"C:\Program Files\Tailscale\tailscale.exe" try: r = subprocess.run([exe, "status", "--json"], capture_output=True, text=True, timeout=8) if r.returncode != 0: return {"ok": False, "error": r.stderr[:120]} d = json.loads(r.stdout) peers = d.get("Peer", {}) online = sum(1 for p in peers.values() if p.get("Online", False)) my_ips = d.get("TailscaleIPs") or [] my_ip = my_ips[0] if my_ips else "?" return {"ok": True, "nodes_total": len(peers) + 1, "nodes_online": online + 1, "peers_online": online, "my_ip": my_ip} except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- SYNTHESIS ---------------- def synthesize(state: dict) -> dict: if model_router is None: return {"markdown": "_model-router niet beschikbaar_", "ok": False, "model": ""} mail = state.get("mail", {}) cal = state.get("calendar", {}) cmd = state.get("command", {}) ctx = { "now": state["generated_at"], "mail_unread_total": mail.get("total_unread"), "mail_by_account": [ {"account": a["label"], "unread": a["unread"], "recent_unread": [f"{r['from']} | {r['subject']}" for r in a["recent"] if r["unread"]][:4]} for a in mail.get("accounts", []) if not a.get("error") ], "today_events": [ f"{e['start'][11:16]}-{e['end'][11:16]} {e['summary']}" + (f" @ {e['location']}" if e["location"] else "") for e in cal.get("today", []) ], "upcoming_events": [ f"{e['start'][:16]} {e['summary']}" + (f" @ {e['location']}" if e["location"] else "") for e in cal.get("upcoming", [])[:18] ], "command_center": json.dumps(cmd.get("today", {}), default=str, ensure_ascii=False)[:1400], "business_crm": json.dumps(cmd.get("business", {}), default=str, ensure_ascii=False)[:1400], "odoo_summary": { "open_tasks": state.get("odoo", {}).get("open_tasks"), "overdue_invoices": state.get("odoo", {}).get("overdue_count"), "outstanding_eur": state.get("odoo", {}).get("total_residual"), }, "infrastructure": { "atlas01_ok": state.get("atlas01", {}).get("ok"), "atlas01_ram_pct": state.get("atlas01", {}).get("used_ram_pct"), "atlas01_disk_free_gb": state.get("atlas01", {}).get("disk_free_gb"), "backup_hourly_age_h": state.get("backup", {}).get("hourly_age_h"), "backup_critical": state.get("backup", {}).get("critical"), "pos_fleet_active": state.get("fleet", {}).get("pos_active"), "dmarc_ok": state.get("dmarc", {}).get("ok"), }, } system = ( "You are Atlas Brain — the operational staff for Chaib, solo entrepreneur " "(Atlas Agency, Atlas POS, and collaborations). You receive his mailboxes, calendar, " "and command-center status. Write a SHARP briefing. Be concrete: name real senders, " "real appointments, real times. Spot calendar conflicts — can he be in two places at once? " "Flag explicitly what he risks forgetting: unread mail from important senders, appointments " "still needing confirmation. Give 3-5 concrete actions. English only. No inventions — " "only what is in the data." ) user = ( "Context (JSON):\n```json\n" + json.dumps(ctx, indent=1, default=str, ensure_ascii=False)[:7000] + "\n```\n\nRespond in EXACTLY this structure (markdown):\n" "## Headline\n\n\n" "## Today's agenda\n\n\n" "## At risk of forgetting\n- ...\n- ...\n\n" "## Actions today\n1. ...\n2. ...\n3. ...\n\n" "## Mail that needs attention\n<1 to 3 lines: from whom, about what>\n\n" "## Spoken\n\n" ) out = model_router.ask(user, task="chat", system=system, budget_s=240, num_predict=780) return {"markdown": (out.get("text") or "").strip() or "_lege synthese_", "ok": out.get("ok", False), "model": out.get("model", "")} # ---------------- SPOKEN ---------------- def build_spoken(state: dict) -> dict: mail = state.get("mail", {}) cal = state.get("calendar", {}) md = state.get("brief", {}).get("markdown", "") now_hm = datetime.now().strftime("%H:%M") future = [e for e in cal.get("today", []) if e.get("start", "")[11:16] >= now_hm] nxt = future[0] if future else None unread = mail.get("total_unread", 0) spoken_para = _strip_md(_extract_section(md, "Spoken")) if spoken_para and len(spoken_para) > 25: full = spoken_para else: parts = [state.get("headline") or "Here is your briefing."] if nxt: parts.append(f"Your next appointment is {nxt['summary']} at {nxt['start'][11:16]}.") parts.append(f"You have {unread} unread messages across your mailboxes.") full = " ".join(parts) sp = [f"It is {now_hm}."] if nxt: sp.append(f"Next appointment: {nxt['summary']} at {nxt['start'][11:16]}.") else: sp.append("No more appointments today.") sp.append(f"{unread} unread emails.") return {"full": full, "short": " ".join(sp)} # ---------------- ODOO ---------------- def _odoo_connect(): """Return (models_proxy, uid, DB, PASS) or raise.""" import xmlrpc.client ODOO = os.environ.get("ATLAS_ODOO_URL", "http://:8069") DB = os.environ.get("ATLAS_ODOO_DB", "atlas") USER = os.environ.get("ATLAS_ODOO_USER", "chaib@atlascorporation.nl") PASS = os.environ.get("ATLAS_ODOO_PW", "") common = xmlrpc.client.ServerProxy(f"{ODOO}/xmlrpc/2/common") uid = common.authenticate(DB, USER, PASS, {}) if not uid: raise RuntimeError("Odoo auth_failed") models = xmlrpc.client.ServerProxy(f"{ODOO}/xmlrpc/2/object") return models, uid, DB, PASS def collect_odoo_overdue() -> dict: """Fetch overdue invoices: posted, unpaid/partial, past invoice_date_due.""" try: models, uid, DB, PASS = _odoo_connect() today_str = datetime.now().strftime("%Y-%m-%d") domain = [ ["move_type", "in", ["out_invoice"]], ["state", "=", "posted"], ["payment_state", "in", ["not_paid", "partial"]], ["invoice_date_due", "<", today_str], ] inv_ids = models.execute_kw(DB, uid, PASS, "account.move", "search", [domain], {"limit": 50}) if not inv_ids: return {"ok": True, "count": 0, "total_residual": 0.0, "invoices": []} invoices = models.execute_kw(DB, uid, PASS, "account.move", "read", [inv_ids], {"fields": ["name", "partner_id", "amount_residual", "amount_total", "invoice_date_due", "payment_state"]}) result = [] total_residual = 0.0 for i in invoices: due_str = i.get("invoice_date_due") or today_str try: due_dt = datetime.strptime(due_str, "%Y-%m-%d") days_overdue = (datetime.now() - due_dt).days except Exception: days_overdue = 0 residual = float(i.get("amount_residual") or i.get("amount_total") or 0) total_residual += residual result.append({ "name": i["name"], "partner": (i.get("partner_id") or [None, "?"])[1], "amount_residual": round(residual, 2), "amount_total": round(float(i.get("amount_total") or 0), 2), "due": due_str, "days_overdue": days_overdue, "payment_state": i.get("payment_state", ""), }) result.sort(key=lambda x: x["days_overdue"], reverse=True) return { "ok": True, "count": len(result), "total_residual": round(total_residual, 2), "invoices": result, } except Exception as e: return {"ok": False, "error": str(e)[:200]} def collect_odoo() -> dict: """Pull task + invoice counts from self-hosted Odoo on atlas-01.""" try: models, uid, DB, PASS = _odoo_connect() # Open tasks task_ids = models.execute_kw(DB, uid, PASS, "project.task", "search", [[["stage_id.fold", "=", False]]], {"limit": 100}) tasks_count = len(task_ids) # All unpaid invoices (posted) today_str = datetime.now().strftime("%Y-%m-%d") inv_ids = models.execute_kw(DB, uid, PASS, "account.move", "search", [[["move_type", "in", ["out_invoice"]], ["state", "=", "posted"], ["payment_state", "in", ["not_paid", "partial"]]]], {"limit": 50}) invoices = models.execute_kw(DB, uid, PASS, "account.move", "read", [inv_ids], {"fields": ["name", "partner_id", "amount_total", "amount_residual", "invoice_date_due", "payment_state"]}) overdue = [i for i in invoices if i.get("invoice_date_due") and i["invoice_date_due"] < today_str] # Detailed overdue block (reuse collect_odoo_overdue logic inline) overdue_detail = [] total_residual = 0.0 for i in overdue: due_str = i.get("invoice_date_due") or today_str try: due_dt = datetime.strptime(due_str, "%Y-%m-%d") days_overdue = (datetime.now() - due_dt).days except Exception: days_overdue = 0 residual = float(i.get("amount_residual") or i.get("amount_total") or 0) total_residual += residual overdue_detail.append({ "name": i["name"], "partner": (i.get("partner_id") or [None, "?"])[1], "amount_residual": round(residual, 2), "amount_total": round(float(i.get("amount_total") or 0), 2), "due": due_str, "days_overdue": days_overdue, "payment_state": i.get("payment_state", ""), }) overdue_detail.sort(key=lambda x: x["days_overdue"], reverse=True) return { "ok": True, "open_tasks": tasks_count, "unpaid_invoices": len(invoices), "overdue_count": len(overdue), "total_outstanding": round(sum(float(i.get("amount_total") or 0) for i in invoices), 2), "total_residual": round(total_residual, 2), "invoices": [{"name": i["name"], "partner": (i.get("partner_id") or [None, "?"])[1], "amount": float(i.get("amount_total") or 0), "residual": float(i.get("amount_residual") or 0), "due": i.get("invoice_date_due"), "state": i.get("payment_state")} for i in invoices[:8]], "overdue_invoices": overdue_detail, } except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- DISK USAGE ---------------- def collect_disk() -> dict: import shutil drives = {} for drive in ["C:\\", "D:\\"]: try: u = shutil.disk_usage(drive) drives[drive[0]] = { "total_gb": round(u.total / 1024**3, 1), "used_gb": round(u.used / 1024**3, 1), "free_gb": round(u.free / 1024**3, 1), "used_pct": round(u.used / u.total * 100, 1), "critical": (u.free / 1024**3) < 20 or (u.used / u.total) > 0.95, "warn": (u.free / 1024**3) < 50 or (u.used / u.total) > 0.85, } except Exception: pass c = drives.get("C", {}) return { "drives": drives, "free_gb": c.get("free_gb"), "used_pct": c.get("used_pct"), "critical": c.get("critical", False), "warn": c.get("warn", False), } # ---------------- SCHEDULED TASKS ---------------- def collect_scheduled_tasks() -> dict: """Count Windows scheduled tasks via schtasks.""" import subprocess try: r = subprocess.run( ["schtasks", "/query", "/fo", "CSV", "/nh"], capture_output=True, text=True, timeout=15 ) lines = [l for l in r.stdout.splitlines() if l.strip() and not l.startswith('"TaskName"')] atlas_lines = [l for l in lines if 'atlas' in l.lower() or 'jarvis' in l.lower()] return {"ok": True, "total": len(lines), "atlas_count": len(atlas_lines)} except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- VAULT STATS ---------------- def collect_vault_stats() -> dict: """Count vault notes + size from the MEGA Obsidian vault.""" import time as _time vault = HOME / "MEGA" / "Atlas Corporation" if not vault.exists(): return {"ok": False, "error": "vault not found"} try: count = 0 total_size = 0 newest_mtime = 0.0 newest_name = None for f in vault.rglob("*.md"): try: st = f.stat() count += 1 total_size += st.st_size if st.st_mtime > newest_mtime: newest_mtime = st.st_mtime newest_name = f.name except Exception: pass newest_age_h = round((_time.time() - newest_mtime) / 3600, 1) if newest_mtime else None return { "ok": True, "note_count": count, "size_mb": round(total_size / 1024 / 1024, 1), "newest_note": newest_name, "newest_age_h": newest_age_h, } except Exception as e: return {"ok": False, "error": str(e)[:100]} # ---------------- BACKUP STATUS ---------------- def collect_backup_status() -> dict: """Check Odoo backup freshness from the MEGA-synced backup folder.""" import time as _time BACKUP_ROOT = HOME / "MEGA" / "Atlas Corporation" / "07_Finance" / "_atlas-odoo-backups" result: dict = {"ok": True, "hourly_age_h": None, "daily_age_h": None, "warn": False, "critical": False} for sub in ("hourly", "daily"): d = BACKUP_ROOT / sub if not d.exists(): result[f"{sub}_age_h"] = None continue files = sorted(d.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True) if d.exists() else [] if files: age_h = round((_time.time() - files[0].stat().st_mtime) / 3600, 1) result[f"{sub}_age_h"] = age_h result[f"{sub}_name"] = files[0].name else: result[f"{sub}_age_h"] = None hourly = result.get("hourly_age_h") daily = result.get("daily_age_h") if hourly is None or hourly > 3: result["warn"] = True if hourly is None or hourly > 12: result["critical"] = True if daily is None or daily > 48: result["warn"] = True return result # ---------------- ATLAS-01 RESOURCES ---------------- def collect_atlas01_resources() -> dict: """SSH to atlas-01 to get free RAM, CPU load, uptime, and container count.""" import subprocess cmd = [ "ssh", "-i", str(HOME / ".ssh" / "id_ed25519_atlas"), "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=accept-new", "root@", "python3 -c \"" "import os,subprocess;" "mem=open('/proc/meminfo').read();" "free_kb=int([l for l in mem.splitlines() if 'MemAvailable' in l][0].split()[1]);" "total_kb=int([l for l in mem.splitlines() if 'MemTotal' in l][0].split()[1]);" "load=open('/proc/loadavg').read().split()[:3];" "uptm=float(open('/proc/uptime').read().split()[0]);" "ctrs=len([l for l in subprocess.run(['docker','ps','-q'],capture_output=True,text=True).stdout.strip().splitlines() if l]);" "df_o=subprocess.run(['df','-BG','/'],capture_output=True,text=True).stdout.splitlines();" "dp=df_o[1].split() if len(df_o)>1 else ['?','0G','0G','0G'];" "d_tot=int(dp[1].rstrip('G'));d_free=int(dp[3].rstrip('G'));" "print(f'{free_kb},{total_kb},{load[0]},{load[1]},{load[2]},{int(uptm//3600)},{ctrs},{d_tot},{d_free}')\"" ] try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=20) if r.returncode != 0: return {"ok": False, "error": r.stderr.strip()[:100]} parts = r.stdout.strip().split(",") if len(parts) < 9: return {"ok": False, "error": "unexpected output"} free_kb, total_kb = int(parts[0]), int(parts[1]) load1, load5, load15 = float(parts[2]), float(parts[3]), float(parts[4]) uptime_h = int(parts[5]) containers = int(parts[6]) disk_total_gb, disk_free_gb = int(parts[7]), int(parts[8]) disk_used_pct = round((disk_total_gb - disk_free_gb) / disk_total_gb * 100, 1) if disk_total_gb > 0 else 0 free_gb = round(free_kb / 1024 / 1024, 2) used_pct = round((total_kb - free_kb) / total_kb * 100, 1) return { "ok": True, "free_ram_gb": free_gb, "total_ram_gb": round(total_kb / 1024 / 1024, 2), "used_ram_pct": used_pct, "load_1m": load1, "load_5m": load5, "load_15m": load15, "uptime_h": uptime_h, "containers": containers, "disk_total_gb": disk_total_gb, "disk_free_gb": disk_free_gb, "disk_used_pct": disk_used_pct, "disk_critical": disk_free_gb < 10, "warn": used_pct > 85 or load1 > 4.0 or disk_free_gb < 20, } except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- AUCTION INTEL ---------------- def collect_auction_intel() -> dict: """Pull live stats from the auction intel dashboard API (localhost:7781).""" import sqlite3 intel_db = HOME / "twa-scrape" / "intel.db" if not intel_db.exists(): return {"ok": False, "reason": "db_not_found"} try: conn = sqlite3.connect(str(intel_db)) conn.row_factory = sqlite3.Row active = conn.execute("SELECT COUNT(*) FROM lots WHERE status='active'").fetchone()[0] strong = conn.execute("SELECT COUNT(*) FROM lots WHERE status='active' AND stars>=4").fetchone()[0] avg_m = conn.execute("SELECT AVG(margin_pct) FROM lots WHERE status='active' AND margin_pct IS NOT NULL").fetchone()[0] try: urgent = conn.execute("SELECT COUNT(*) FROM lots WHERE status='active' AND urgency=1").fetchone()[0] except Exception: urgent = 0 top5 = [dict(r) for r in conn.execute( "SELECT id, title, model_key, hammer, margin_pct, stars, close_time " "FROM lots WHERE status='active' AND stars>=4 " "ORDER BY stars DESC, margin_pct DESC LIMIT 5" ).fetchall()] try: last_run = dict(conn.execute( "SELECT started_at, run_type, lots_found FROM run_log ORDER BY id DESC LIMIT 1" ).fetchone() or {}) except Exception: last_run = {} conn.close() return { "ok": True, "lots_active": active, "strong_buys": strong, "urgent_count": urgent, "avg_margin": round(avg_m or 0, 1), "top_lots": top5, "last_run": last_run, "dashboard_url": "http://localhost:7781/", } except Exception as e: return {"ok": False, "reason": str(e)} # ---------------- POS FLEET ---------------- def collect_pos_fleet() -> dict: """Read POS fleet config — static count from pos_targets.json.""" pts = ATLAS / "pos_targets.json" if not pts.exists(): return {"ok": False, "reason": "pos_targets.json not found"} try: cfg = json.loads(pts.read_text(encoding="utf-8")) pos = cfg.get("pos_agents", []) internal = cfg.get("atlas_internal", []) active = [p for p in pos if not p.get("skip_autopilot")] skipped = [p for p in pos if p.get("skip_autopilot")] return { "ok": True, "pos_count": len(pos), "pos_active": len(active), "pos_skipped": len(skipped), "internal_count": len(internal), "skip_reasons": [{"name": p["dws_name"], "reason": p.get("skip_reason", "?")[:60]} for p in skipped], } except Exception as e: return {"ok": False, "error": str(e)[:120]} # ---------------- DMARC CHECK ---------------- def collect_dmarc() -> dict: """DNS check for DMARC policy on Atlas domains. p=none = not enforced.""" import socket domains = ["atlascorporation.nl", "atlasworks.nl", "atlasworkspace.eu", "atlasoperations.nl"] results = {} for d in domains: try: txt = socket.getaddrinfo(f"_dmarc.{d}", None) except Exception: txt = None try: import subprocess r = subprocess.run( ["nslookup", "-type=TXT", f"_dmarc.{d}", "8.8.8.8"], capture_output=True, text=True, timeout=8 ) raw = r.stdout if "v=DMARC1" in raw: policy = "quarantine" if "p=quarantine" in raw else ( "reject" if "p=reject" in raw else "none") results[d] = {"ok": policy != "none", "policy": policy} else: results[d] = {"ok": False, "policy": "missing"} except Exception as e: results[d] = {"ok": None, "error": str(e)[:80]} enforced = [d for d, v in results.items() if v.get("ok")] missing = [d for d, v in results.items() if not v.get("ok")] return {"ok": len(missing) == 0, "domains": results, "enforced": enforced, "missing": missing} # ---------------- MAIN ---------------- def main() -> None: quick = "--quick" in sys.argv send_report = "--report" in sys.argv t0 = time.time() state = { "generated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "generated_unix": int(time.time()), "operator": "Chaib", "mode": "quick" if quick else "full", } state["mail"] = _safe(collect_mail, "mail") state["calendar"] = _safe(collect_calendar, "calendar") state["command"] = _safe(collect_command, "command") state["service_health"] = _safe(collect_service_health, "service_health") state["syncthing"] = _safe(collect_syncthing, "syncthing") state["tailscale"] = _safe(collect_tailscale, "tailscale") state["ollama"] = _safe(collect_ollama, "ollama") state["scheduled_tasks"] = _safe(collect_scheduled_tasks, "scheduled_tasks") state["auction_intel"] = _safe(collect_auction_intel, "auction_intel") state["disk"] = _safe(collect_disk, "disk") state["atlas01"] = _safe(collect_atlas01_resources, "atlas01") state["backup"] = _safe(collect_backup_status, "backup") state["fleet"] = _safe(collect_pos_fleet, "fleet") state["dmarc"] = _safe(collect_dmarc, "dmarc") # Slow collectors — only on full runs; quick ticks carry forward the last state if not quick: state["vault"] = _safe(collect_vault_stats, "vault") # 5s rglob over 10k files state["odoo"] = _safe(collect_odoo, "odoo") # Store overdue invoices as its own top-level key for check_overdue.py / portal odoo_data = state["odoo"] if odoo_data.get("ok"): state["overdue_invoices"] = { "ok": True, "count": odoo_data.get("overdue_count", 0), "total_residual": odoo_data.get("total_residual", 0.0), "invoices": odoo_data.get("overdue_invoices", []), "updated_at": state["generated_at"], } else: state["overdue_invoices"] = {"ok": False, "error": odoo_data.get("error", "odoo_error"), "updated_at": state["generated_at"]} else: try: prev = json.loads(STATE_JSON.read_text(encoding="utf-8")) if STATE_JSON.exists() else {} state["odoo"] = prev.get("odoo", {"ok": False, "error": "quick_tick_no_refresh"}) state["overdue_invoices"] = prev.get("overdue_invoices", {"ok": False, "error": "quick_tick_no_refresh"}) state["vault"] = prev.get("vault", {"ok": False, "error": "quick_tick_no_refresh"}) except Exception: state["odoo"] = {"ok": False, "error": "quick_tick_no_refresh"} state["overdue_invoices"] = {"ok": False, "error": "quick_tick_no_refresh"} state["vault"] = {"ok": False, "error": "quick_tick_no_refresh"} if quick: state["brief"] = {"markdown": f"_quick tick {state['generated_at'][11:16]} — no synthesis_", "ok": False, "model": ""} else: state["brief"] = _safe(lambda: synthesize(state), "synth") state["headline"] = (_extract_section(state["brief"].get("markdown", ""), "Headline").splitlines() or _extract_section(state["brief"].get("markdown", ""), "Kop").splitlines() or ["Atlas briefing."])[0].strip().lstrip("#").strip() or "Atlas briefing." state["spoken"] = build_spoken(state) state["services"] = SERVICES # static link list for portal state["build_seconds"] = round(time.time() - t0, 1) state_bytes = json.dumps(state, indent=2, default=str, ensure_ascii=False) STATE_JSON.write_text(state_bytes, encoding="utf-8") # Mirror to state/ sub-dir (served by the 7799 HTTP server) (OPS_DIR / "state" / "atlas-state.json").write_text(state_bytes, encoding="utf-8") SPOKEN_JSON.write_text(json.dumps(state["spoken"], indent=2, ensure_ascii=False), encoding="utf-8") try: cmd_dir = HOME / "MEGA" / "Atlas Corporation" / "vault" / "_command" if cmd_dir.exists(): (cmd_dir / "jarvis-brief.md").write_text(state["brief"].get("markdown", ""), encoding="utf-8") except Exception: pass log(f"state written -> {STATE_JSON} ({state['build_seconds']}s, " f"mail_unread={state['mail'].get('total_unread')}, " f"today_events={len(state['calendar'].get('today', []))})") if send_report and not quick: try: import subprocess py = sys.executable rpt = Path(__file__).parent / "atlas_morning_report.py" subprocess.Popen([py, str(rpt)], creationflags=0x08000000) log("Morning report spawned") except Exception as e: log(f"Morning report failed: {e}") print(json.dumps({ "ok": True, "mode": state["mode"], "state_file": str(STATE_JSON), "mail_unread": state["mail"].get("total_unread"), "today_events": len(state["calendar"].get("today", [])), "headline": state["headline"], "seconds": state["build_seconds"], }, indent=2, ensure_ascii=False)) if __name__ == "__main__": try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: pass main()