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.
229 lines
7 KiB
Python
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()
|