Files
Kareem Horstink 331de4bc9e
Build Android APK / build (push) Failing after 4m21s
Build Server .exe / build (push) Failing after 2m34s
Add settings page, PIN manager, and auto-config
- New settings page at /settings (localhost-only) accessible via tray icon
- PIN manager with 6-digit codes, 5-min TTL, single-use
- API token uses secrets.token_urlsafe(32) for 256-bit entropy
- Config auto-creates config.yaml on first run with detected local IP
- Default PIN and token generated randomly on first startup
- Added pin_ttl setting (default 300s)
- Updated build_exe.py for onefile exe with settings assets bundled
- Added start.bat and start.sh for easy development startup
2026-07-03 08:21:46 +00:00

137 lines
3.7 KiB
Python

"""System tray wrapper for PaperDash.
Runs the FastAPI server in a background thread and displays a tray icon
with a quit option. This is the entry point for the compiled .exe.
"""
from __future__ import annotations
import logging
import multiprocessing
import os
import signal
import subprocess
import sys
import time
import pystray
from PIL import Image, ImageDraw
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger("paperdash.tray")
def create_icon() -> Image.Image:
"""Create a simple 16x16 tray icon."""
img = Image.new("RGB", (16, 16), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
# Dashboard frame
draw.rectangle([1, 1, 15, 15], outline=(255, 255, 255), width=1)
# Screen lines
draw.line([3, 3, 13, 3], fill=(255, 255, 255), width=1)
draw.line([3, 5, 13, 5], fill=(255, 255, 255), width=1)
draw.line([3, 7, 8, 7], fill=(255, 255, 255), width=1)
# Stand
draw.line([6, 15, 10, 15], fill=(255, 255, 255), width=1)
return img
def _run_server() -> None:
"""Run the FastAPI server in this process (for multiprocessing)."""
import uvicorn
uvicorn.run(
"main:app",
host=CONFIG_HOST,
port=CONFIG_PORT,
log_level=CONFIG_LOG_LEVEL,
reload=False,
)
def _on_quit(icon: pystray.Icon, item: pystray.MenuItem) -> None:
"""Handle quit from tray menu."""
logger.info("Quit requested from tray")
icon.stop()
def _on_settings(icon: pystray.Icon, item: pystray.MenuItem) -> None:
"""Open the settings page in the default browser."""
logger.info("Opening settings page...")
url = "http://127.0.0.1:8921/settings"
try:
if sys.platform == "win32":
os.startfile(url)
elif sys.platform == "darwin":
subprocess.run(["open", url])
else:
subprocess.run(["xdg-open", url])
except Exception as e:
logger.error(f"Failed to open settings page: {e}")
def main() -> None:
"""Entry point for the compiled .exe."""
logger.info("PaperDash starting...")
# Start server in a subprocess
server_process = multiprocessing.Process(target=_run_server, daemon=True)
server_process.start()
logger.info(f"Server process started (PID {server_process.pid})")
# Wait briefly for server to be ready
time.sleep(2)
# Create and run tray icon
icon_image = create_icon()
menu = pystray.Menu(
pystray.MenuItem("PaperDash", pystray.MenuItem.default, enabled=False),
pystray.Menu.Separator(),
pystray.MenuItem("Settings", _on_settings),
pystray.Menu.Separator(),
pystray.MenuItem("Quit", _on_quit),
)
icon = pystray.Icon(
name="PaperDash",
image=icon_image,
title="PaperDash",
menu=menu,
)
# Handle OS exit signals
def _signal_handler(signum, frame):
logger.info(f"Received signal {signum}, shutting down")
icon.stop()
server_process.terminate()
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
logger.info("PaperDash running in system tray. Quit from notification area.")
icon.run()
# Cleanup
logger.info("Shutting down server process")
server_process.terminate()
server_process.join(timeout=5)
def _load_config_values() -> tuple[str, int, str]:
"""Load config and return (host, port, log_level) for the server."""
from .config import config
config.load()
return config.host or "127.0.0.1", config.port, config.log_level.lower()
# Load config at module level so _run_server uses detected values
CONFIG_HOST, CONFIG_PORT, CONFIG_LOG_LEVEL = _load_config_values()
if __name__ == "__main__":
main()