#!/usr/bin/env python3 """ atlas_tray.py - Atlas always-on system tray icon. Right-click menu for quick access to Atlas Command / HUD / Chat / Portal + util actions. Icon color reflects cluster health. Run with pythonw.exe so no console flash: pythonw atlas_tray.py """ import os import sys import time import json import webbrowser import threading import urllib.request import subprocess from pathlib import Path try: import pystray from pystray import MenuItem as Item, Menu from PIL import Image, ImageDraw, ImageFont except ImportError as e: print(f"Missing dep: {e}\nInstall: pip install pystray pillow") sys.exit(1) HOME = Path.home() ATLAS = HOME / ".config" / "atlas" LOG = ATLAS / "atlas_tray.log" CFG = { "ollama_url": "http://atlas-01/ollama/api/tags", "priorities": "http://atlas-01/priorities.json", "poll_seconds": 30, } STATE = { "status": "unknown", "overdue_eur": 0, "disk_free_gb": 0, "containers_down": 0, "last_update": None, "models": 0, } def log(msg): try: LOG.parent.mkdir(parents=True, exist_ok=True) with open(LOG, "a", encoding="utf-8") as f: f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n") except Exception: pass def fetch_json(url, timeout=4): try: with urllib.request.urlopen(url, timeout=timeout) as r: return json.loads(r.read().decode("utf-8", errors="replace")) except Exception: return None def make_icon(color_hex="#3B82F6", text="A"): """Generate 64x64 icon: filled circle + white letter.""" size = 64 img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) d = ImageDraw.Draw(img) d.ellipse((4, 4, size - 4, size - 4), fill=color_hex) try: font = ImageFont.truetype("arial.ttf", 38) except Exception: font = ImageFont.load_default() bbox = d.textbbox((0, 0), text, font=font) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] d.text(((size - tw) / 2 - bbox[0], (size - th) / 2 - bbox[1]), text, fill="white", font=font) return img def color_for(status): return { "ok": "#10B981", "warn": "#F59E0B", "err": "#EF4444", "unknown": "#6B7280", }.get(status, "#3B82F6") def poll_loop(icon): while True: try: pri = fetch_json(CFG["priorities"]) tags = fetch_json(CFG["ollama_url"]) if pri: STATE["overdue_eur"] = pri.get("odoo", {}).get("overdue_eur", 0) STATE["disk_free_gb"] = pri.get("disk_free_gb", 0) STATE["containers_down"] = pri.get("containers_down", 0) STATE["last_update"] = time.strftime("%H:%M:%S") STATE["models"] = len(tags.get("models", [])) if tags else 0 if tags is None and pri is None: STATE["status"] = "err" elif STATE["containers_down"] > 0 or STATE["disk_free_gb"] < 5: STATE["status"] = "err" elif STATE["overdue_eur"] > 0 or STATE["disk_free_gb"] < 15: STATE["status"] = "warn" else: STATE["status"] = "ok" try: icon.icon = make_icon(color_for(STATE["status"])) icon.title = ( f"Atlas — {STATE['status'].upper()}\n" f"Overdue: €{STATE['overdue_eur']}\n" f"Disk: {STATE['disk_free_gb']}GB free\n" f"Containers down: {STATE['containers_down']}\n" f"Models on atlas-01: {STATE['models']}\n" f"Updated: {STATE['last_update']}" ) except Exception as e: log(f"icon update fail: {e}") except Exception as e: log(f"poll error: {e}") time.sleep(CFG["poll_seconds"]) def open_url(url): def _open(icon, item): webbrowser.open(url) log(f"opened url {url}") return _open def open_app(url, profile): """Open in Edge --app mode for native-window feel.""" edge = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" profile_dir = HOME / "AppData" / "Local" / "Atlas" / profile def _open(icon, item): if not Path(edge).exists(): webbrowser.open(url) return try: subprocess.Popen([ edge, f"--app={url}", f"--user-data-dir={profile_dir}", "--no-first-run", "--no-default-browser-check", ]) log(f"app opened {url}") except Exception as e: log(f"app open fail: {e}") return _open def restart_ollama(icon, item): log("restart ollama requested") try: subprocess.run(["taskkill", "/F", "/IM", "ollama.exe"], capture_output=True, timeout=5) subprocess.run(["taskkill", "/F", "/IM", "ollama app.exe"], capture_output=True, timeout=5) time.sleep(1) ollama = HOME / "AppData" / "Local" / "Programs" / "Ollama" / "ollama.exe" if ollama.exists(): env = os.environ.copy() env["OLLAMA_HOST"] = "0.0.0.0:11434" env["OLLAMA_KEEP_ALIVE"] = "30m" subprocess.Popen([str(ollama), "serve"], env=env, creationflags=0x08000000) log("ollama restarted") except Exception as e: log(f"restart ollama fail: {e}") def open_log(icon, item): try: subprocess.Popen(["notepad.exe", str(LOG)]) except Exception: webbrowser.open(f"file:///{LOG}") def quit_tray(icon, item): log("tray quit") icon.stop() def status_label(item): return f"Status: {STATE['status'].upper()} · €{STATE['overdue_eur']} open · {STATE['disk_free_gb']}GB" def updated_label(item): return f"Updated: {STATE['last_update'] or 'never'}" def main(): log("=== atlas_tray starting ===") menu = Menu( Item(status_label, lambda i, x: None, enabled=False), Item(updated_label, lambda i, x: None, enabled=False), Menu.SEPARATOR, Item("Atlas Command · Ctrl+Shift+P", open_app("http://atlas-01/atlas-command.html", "Cockpit-Profile"), default=True), Item("Atlas HUD · Ctrl+Shift+H", open_app("http://atlas-01/atlas-hud.html", "HUD-Profile")), Item("Atlas Chat · Ctrl+Shift+C", open_app("http://atlas-01/atlas-chat.html", "Chat-Profile")), Item("Atlas Portal (legacy)", open_url("http://atlas-01/portal.html")), Menu.SEPARATOR, Item("Odoo", open_url("http://:8069")), Item("Nextcloud", open_url("http://cloud.your-domain.tld")), Item("MeshCentral", open_url("http://mesh.your-domain.tld")), Item("Open WebUI", open_url("http://:8082")), Menu.SEPARATOR, Item("Restart Ollama (PC)", restart_ollama), Item("Open tray log", open_log), Menu.SEPARATOR, Item("Quit", quit_tray), ) img = make_icon(color_for("unknown")) icon = pystray.Icon("atlas", img, "Atlas (loading...)", menu) threading.Thread(target=poll_loop, args=(icon,), daemon=True).start() icon.run() if __name__ == "__main__": main()