atlas-os/scripts/atlas_tray.py
Chaib Aarab 301cc75de9 init: Atlas OS — open-source patterns + tools for solo founder LLM cluster
3 browser-native chat surfaces (atlas-chat / atlas-hud / atlas-command, no build step)
+ 11 Python automation scripts (cluster, tray, finance, reminders, autonomy loops)
+ 2 patterns documented (cluster-then-polish, anti-impulse R1-R10)
MIT licensed, anonymized from production stack.
2026-05-26 16:21:16 +02:00

229 lines
7 KiB
Python

#!/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://<YOUR_VPS_IP>: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://<YOUR_VPS_IP>: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()