147 lines
4.3 KiB
Python
Executable file
147 lines
4.3 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Life Manager system tray icon.
|
|
|
|
Uses lilith-tray for cross-platform tray abstraction.
|
|
AyatanaAppIndicator3 backend on Linux (auto-detected).
|
|
|
|
Mode is set via LM_TRAY_MODE env var:
|
|
- "dev" → checks ports (API 3700, frontend 5701, postgres 25471, redis 26370)
|
|
- "prod" → checks systemd units (life-manager-api, caddy, vram)
|
|
"""
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
|
|
from lilith_tray import TrayApp, TrayConfig, TrayIcon, TrayMenuItem
|
|
|
|
MODE = os.environ.get("LM_TRAY_MODE", "dev")
|
|
|
|
DEV_APP_URL = f"http://{socket.gethostname()}.local:5701"
|
|
PROD_APP_URL = f"https://{socket.gethostname()}.local:5700"
|
|
APP_URL = PROD_APP_URL if MODE == "prod" else DEV_APP_URL
|
|
|
|
DEV_SERVICES = {
|
|
"api": 3700,
|
|
"frontend": 5701,
|
|
"postgres": 25471,
|
|
"redis": 26370,
|
|
}
|
|
|
|
PROD_UNITS = [
|
|
"life-manager-api.service",
|
|
"life-manager-caddy.service",
|
|
"life-manager-vram.service",
|
|
]
|
|
|
|
# Use @lilith/tray-resources generated icons (24px hexagon-hub)
|
|
RESOURCES_DIR = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"..", "..", "..", "@packages", "@tray", "tray-resources", "generated", "hexagon-hub",
|
|
)
|
|
|
|
|
|
def run_cmd(*args: str) -> str:
|
|
result = subprocess.run(args, capture_output=True, text=True, timeout=30)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def systemctl_cmd(*args: str) -> None:
|
|
subprocess.run(["systemctl", "--user", *args], capture_output=True, timeout=30)
|
|
|
|
|
|
def port_open(port: int) -> bool:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.settimeout(1)
|
|
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
|
|
|
|
def open_browser() -> None:
|
|
subprocess.Popen(
|
|
["xdg-open", APP_URL],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
|
|
|
|
class LifeManagerTray(TrayApp):
|
|
def __init__(self) -> None:
|
|
icons = {
|
|
"green": TrayIcon.from_file(os.path.join(RESOURCES_DIR, "green-24.png")),
|
|
"red": TrayIcon.from_file(os.path.join(RESOURCES_DIR, "red-24.png")),
|
|
"yellow": TrayIcon.from_file(os.path.join(RESOURCES_DIR, "yellow-24.png")),
|
|
}
|
|
|
|
menu = [
|
|
TrayMenuItem.action("Open Life Manager", open_browser),
|
|
TrayMenuItem.separator(),
|
|
TrayMenuItem.separator(),
|
|
TrayMenuItem.action("Start", self._start_services),
|
|
TrayMenuItem.action("Stop", self._stop_services),
|
|
TrayMenuItem.action("Restart", self._restart_services),
|
|
TrayMenuItem.separator(),
|
|
TrayMenuItem.quit("Quit Tray"),
|
|
]
|
|
|
|
config = TrayConfig(
|
|
name="life-manager",
|
|
icons=icons,
|
|
initial_icon="red",
|
|
menu=menu,
|
|
poll_interval=10,
|
|
)
|
|
super().__init__(config)
|
|
|
|
def poll_status(self) -> str:
|
|
statuses = self._get_service_statuses()
|
|
all_up = all(s == "active" for s in statuses.values())
|
|
return "green" if all_up else "red"
|
|
|
|
def get_status_labels(self) -> dict[str, str]:
|
|
return self._get_service_statuses()
|
|
|
|
def _get_service_statuses(self) -> dict[str, str]:
|
|
if MODE == "prod":
|
|
return self._check_systemd()
|
|
return self._check_ports()
|
|
|
|
def _check_ports(self) -> dict[str, str]:
|
|
statuses = {}
|
|
for name, port in DEV_SERVICES.items():
|
|
statuses[name] = "active" if port_open(port) else "inactive"
|
|
return statuses
|
|
|
|
def _check_systemd(self) -> dict[str, str]:
|
|
statuses = {}
|
|
for unit in PROD_UNITS:
|
|
try:
|
|
status = run_cmd("systemctl", "--user", "is-active", unit)
|
|
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
status = "error"
|
|
name = unit.replace("life-manager-", "").replace(".service", "")
|
|
statuses[name] = status
|
|
return statuses
|
|
|
|
def _start_services(self) -> None:
|
|
if MODE == "prod":
|
|
for unit in PROD_UNITS:
|
|
systemctl_cmd("start", unit)
|
|
|
|
def _stop_services(self) -> None:
|
|
if MODE == "prod":
|
|
for unit in PROD_UNITS:
|
|
systemctl_cmd("stop", unit)
|
|
|
|
def _restart_services(self) -> None:
|
|
if MODE == "prod":
|
|
for unit in PROD_UNITS:
|
|
systemctl_cmd("restart", unit)
|
|
|
|
|
|
def main() -> None:
|
|
app = LifeManagerTray()
|
|
app.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|