diff --git a/backend/config.py b/backend/config.py index 2517978..23e679b 100644 --- a/backend/config.py +++ b/backend/config.py @@ -72,6 +72,8 @@ _DEFAULT_CONFIG: dict = { }, "session_timeout_minutes": 1440, # 24 h "stats_interval_seconds": 2, + "update_check_enabled": True, + "update_check_interval": 60, # seconds between update checks } diff --git a/backend/display.py b/backend/display.py index 8c59a96..66612e1 100644 --- a/backend/display.py +++ b/backend/display.py @@ -6,6 +6,7 @@ Each GUI app window is captured independently and streamed as JPEG frames. """ import asyncio +import getpass import logging import os import re @@ -473,6 +474,39 @@ class DisplayManager: app.kill() return [a.to_dict() for a in user_apps.values()] + async def autostart_apps(self, gui_apps: list[dict]): + """Launch all apps with autostart=True. Called on service startup.""" + if not HAS_DISPLAY_DEPS: + log.info("Display deps not available, skipping autostart") + return + + username = getpass.getuser() + started = 0 + for app_cfg in gui_apps: + if not app_cfg.get("autostart"): + continue + command = app_cfg.get("command", "") + if not command: + continue + # Skip if already running + if self.get_app_by_command(username, command): + log.debug("Autostart skip (already running): %s", command) + continue + try: + await self.launch_app( + username=username, + command=command, + title=app_cfg.get("name", command), + args=app_cfg.get("args", []), + target_fps=app_cfg.get("target_fps", 10), + ) + started += 1 + log.info("Autostarted: %s", command) + except Exception as e: + log.warning("Failed to autostart %s: %s", command, e) + if started: + log.info("Autostarted %d application(s)", started) + async def shutdown_all(self): """Kill all apps and Xvfb displays.""" for username, apps in self._apps.items(): diff --git a/backend/main.py b/backend/main.py index 6f40a8d..2d2ce77 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,7 +10,7 @@ from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from backend.auth import authenticate_user, create_token, logout -from backend.config import FRONTEND_DIR, HOST, PORT +from backend.config import FRONTEND_DIR, HOST, PORT, load_config from backend.routers import stats, terminal, files, services, processes, settings, network, packages, updates, session, display from backend.sessions import manager as session_manager from backend.display import display_manager @@ -30,6 +30,16 @@ log = logging.getLogger("atlus") @asynccontextmanager async def lifespan(app: FastAPI): log.info("Atlus starting on %s:%d", HOST, PORT) + + # Autostart configured GUI apps (always-on desktop session) + cfg = load_config() + gui_apps = cfg.get("gui_apps", []) + if gui_apps: + try: + await display_manager.autostart_apps(gui_apps) + except Exception as e: + log.warning("Autostart failed: %s", e) + # Start stats broadcaster broadcaster = asyncio.create_task(stats.stats_broadcaster()) diff --git a/backend/routers/settings.py b/backend/routers/settings.py index f73852c..7d3d16a 100644 --- a/backend/routers/settings.py +++ b/backend/routers/settings.py @@ -24,6 +24,8 @@ class ConfigUpdate(BaseModel): gui_apps: Optional[list[dict]] = None session_timeout_minutes: Optional[int] = None stats_interval_seconds: Optional[int] = None + update_check_enabled: Optional[bool] = None + update_check_interval: Optional[int] = None class HostnameRequest(BaseModel): diff --git a/frontend/css/apps/settings.css b/frontend/css/apps/settings.css index ec87858..77dacb9 100644 --- a/frontend/css/apps/settings.css +++ b/frontend/css/apps/settings.css @@ -289,6 +289,74 @@ white-space: nowrap; } +/* Applications management */ +.app-config-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--border-structural); +} + +.app-config-info { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} + +.app-config-icon { + font-size: 20px; + flex-shrink: 0; +} + +.app-config-details { + min-width: 0; +} + +.app-config-name { + font-family: var(--font-ui); + font-size: 14px; + color: var(--text-primary); +} + +.app-config-cmd { + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-config-controls { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; +} + +.app-autostart-label { + display: flex; + align-items: center; + gap: 5px; + cursor: pointer; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + user-select: none; +} + +.app-autostart-label input[type="checkbox"] { + width: 14px; + height: 14px; + accent-color: var(--accent); + cursor: pointer; +} + /* WiFi network list */ .wifi-network-list { max-height: 360px; diff --git a/frontend/css/panel.css b/frontend/css/panel.css index 87ea6d3..ef875e2 100644 --- a/frontend/css/panel.css +++ b/frontend/css/panel.css @@ -136,54 +136,41 @@ .stat-bar-fill.warn { background: var(--status-amber); } .stat-bar-fill.crit { background: var(--status-red); } -/* Services */ -.panel-service-list { +/* Applications (docked GUI apps) */ +.panel-app-list { display: flex; flex-direction: column; gap: 2px; } -.panel-service-row { +.panel-app-row { display: flex; align-items: center; gap: 8px; - min-height: 44px; - padding: 6px 0; + min-height: 38px; + padding: 4px 0; } -.service-toggle { - position: relative; - width: 36px; - height: 20px; +.panel-app-dot { + width: 8px; + height: 8px; + border-radius: 50%; background: var(--border-component); - border-radius: 10px; - border: none; - cursor: pointer; - transition: background var(--transition-fast); flex-shrink: 0; + transition: background var(--transition-fast); } -.service-toggle.on { +.panel-app-dot.active { background: var(--status-green); } -.service-toggle::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - width: 16px; - height: 16px; - border-radius: 50%; - background: #fff; - transition: transform var(--transition-fast); +.panel-app-icon { + font-size: 14px; + flex-shrink: 0; + cursor: pointer; } -.service-toggle.on::after { - transform: translateX(16px); -} - -.service-name { +.panel-app-name { flex: 1; font-family: var(--font-mono); font-size: 12px; @@ -191,9 +178,14 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + cursor: pointer; } -.service-open { +.panel-app-name:hover { + color: var(--accent); +} + +.panel-app-action { width: 28px; height: 28px; background: none; @@ -204,15 +196,47 @@ display: flex; align-items: center; justify-content: center; - font-size: 14px; + font-size: 12px; flex-shrink: 0; } -.service-open:hover { +.panel-app-action:hover { background: var(--accent-hover); color: var(--accent); } +.panel-app-action.stop:hover { + color: var(--status-red); +} + +.panel-app-action:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.panel-app-add { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 32px; + margin-top: 8px; + background: none; + border: 1px dashed var(--border-structural); + border-radius: var(--radius-sm); + color: var(--text-muted); + font-family: var(--font-mono); + font-size: 11px; + cursor: pointer; + transition: all var(--transition-fast); +} + +.panel-app-add:hover { + border-color: var(--accent); + color: var(--accent); + background: var(--accent-hover); +} + /* Update status */ .panel-updates { transition: all var(--transition-base); diff --git a/frontend/desktop.html b/frontend/desktop.html index d15bec5..5ae7dc6 100644 --- a/frontend/desktop.html +++ b/frontend/desktop.html @@ -172,10 +172,10 @@ - -
-
SERVICES
-
+ +
+
APPLICATIONS
+
diff --git a/frontend/js/apps/settings.js b/frontend/js/apps/settings.js index 656dfb6..bfbd14d 100644 --- a/frontend/js/apps/settings.js +++ b/frontend/js/apps/settings.js @@ -8,6 +8,7 @@ const SECTIONS = [ { id: 'general', label: 'General' }, + { id: 'applications', label: 'Applications' }, { id: 'network', label: 'Network' }, { id: 'services', label: 'Services' }, { id: 'about', label: 'About' }, @@ -21,6 +22,7 @@ }); if (section === 'general') await renderGeneral(); + else if (section === 'applications') await renderApplications(); else if (section === 'network') await renderNetwork(); else if (section === 'services') await renderServicesConfig(); else if (section === 'about') await renderAbout(); @@ -69,6 +71,19 @@
+
+
+
Update scanner
+
Seconds between update checks
+
+
+ + +
+
@@ -76,11 +91,20 @@
`; + // Update scanner toggle — enable/disable the interval input + const updateEnabledCb = contentEl.querySelector('#setUpdateEnabled'); + const updateIntervalInput = contentEl.querySelector('#setUpdateInterval'); + updateEnabledCb.addEventListener('change', () => { + updateIntervalInput.disabled = !updateEnabledCb.checked; + }); + contentEl.querySelector('#saveGeneral').addEventListener('click', async () => { const hostname = contentEl.querySelector('#setHostname').value.trim(); const timezone = contentEl.querySelector('#setTimezone').value.trim(); const interval = parseInt(contentEl.querySelector('#setStatsInterval').value); const timeout = parseInt(contentEl.querySelector('#setSessionTimeout').value); + const updateEnabled = updateEnabledCb.checked; + const updateInterval = parseInt(updateIntervalInput.value) || 60; // Update hostname if changed if (hostname && hostname !== sys.hostname) { @@ -105,10 +129,12 @@ stats_interval_seconds: interval, session_timeout_minutes: timeout, timezone: timezone === 'System default' ? null : timezone, + update_check_enabled: updateEnabled, + update_check_interval: updateInterval, }, }); - alert('Settings saved.'); + alert('Settings saved. Update scanner changes take effect on page reload.'); }); } @@ -804,17 +830,201 @@ } } + // --------------------------------------------------------------- + // Applications section — manage GUI apps + autostart + // --------------------------------------------------------------- + + function _slugify(str) { + return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + } + + async function renderApplications() { + const cfgRes = await Atlus.apiFetch('/api/settings'); + const cfg = await cfgRes.json(); + const guiApps = cfg.gui_apps || []; + + // Check display availability + let runningApps = []; + let displayAvailable = false; + try { + const statusRes = await Atlus.apiFetch('/api/display/status'); + if (statusRes.ok) { + const status = await statusRes.json(); + displayAvailable = status.available; + runningApps = status.apps || []; + } + } catch (e) { /* ignore */ } + + let html = '
Applications
'; + + // Display deps status + if (!displayAvailable) { + html += `
+
+ Display dependencies not available. Install xvfb, xdotool, and imagemagick to enable GUI app support. +
+
`; + } + + // Configured apps list + html += '
CONFIGURED APPLICATIONS
'; + + if (guiApps.length === 0) { + html += '
No applications configured. Add one below.
'; + } else { + for (let i = 0; i < guiApps.length; i++) { + const app = guiApps[i]; + const running = runningApps.find(r => r.command === app.command && r.alive); + const statusColor = running ? 'var(--status-green)' : 'var(--text-muted)'; + const statusText = running ? 'Running' : 'Stopped'; + + html += ` +
+
+ ${app.icon || '🖥'} +
+
${app.name || app.command}
+
${app.command}${app.args && app.args.length ? ' ' + app.args.join(' ') : ''}
+
+
+
+ ${statusText} + + +
+
+ `; + } + } + html += '
'; + + // Add app form + html += ` +
+
ADD APPLICATION
+
+
Name
Display name for the app
+ +
+
+
Command
Executable name (must be in PATH)
+ +
+
+
Icon
Emoji icon for dock and panel
+ +
+
+
Arguments
Optional, space-separated
+ +
+
+
Target FPS
Frame rate for streaming
+ +
+
+
Autostart
Launch automatically when Atlus starts
+ +
+
+ +
+
+ `; + + contentEl.innerHTML = html; + + // ---- Event handlers ---- + + // Autostart toggle + contentEl.querySelectorAll('.app-autostart-cb').forEach(cb => { + cb.addEventListener('change', async () => { + const idx = parseInt(cb.dataset.appIdx); + const apps = [...guiApps]; + if (apps[idx]) { + apps[idx] = { ...apps[idx], autostart: cb.checked }; + await Atlus.apiFetch('/api/settings', { + method: 'PUT', + body: { gui_apps: apps }, + }); + } + }); + }); + + // Remove app + contentEl.querySelectorAll('[data-remove-app]').forEach(btn => { + btn.addEventListener('click', async () => { + const idx = parseInt(btn.dataset.removeApp); + const app = guiApps[idx]; + if (!confirm(`Remove ${app.name || app.command} from applications?`)) return; + + const apps = guiApps.filter((_, i) => i !== idx); + await Atlus.apiFetch('/api/settings', { + method: 'PUT', + body: { gui_apps: apps }, + }); + renderApplications(); + }); + }); + + // Add app + contentEl.querySelector('#appAddSave').addEventListener('click', async () => { + const name = contentEl.querySelector('#appAddName').value.trim(); + const command = contentEl.querySelector('#appAddCommand').value.trim(); + const icon = contentEl.querySelector('#appAddIcon').value.trim() || '🖥'; + const argsRaw = contentEl.querySelector('#appAddArgs').value.trim(); + const fps = parseInt(contentEl.querySelector('#appAddFps').value) || 10; + const autostart = contentEl.querySelector('#appAddAutostart').checked; + + if (!name || !command) { + alert('Name and command are required.'); + return; + } + + // Check for duplicate command + if (guiApps.some(a => a.command === command)) { + alert(`An application with command "${command}" already exists.`); + return; + } + + const newApp = { + id: _slugify(name), + name: name, + command: command, + icon: icon, + args: argsRaw ? argsRaw.split(' ').filter(Boolean) : [], + target_fps: fps, + autostart: autostart, + }; + + const apps = [...guiApps, newApp]; + await Atlus.apiFetch('/api/settings', { + method: 'PUT', + body: { gui_apps: apps }, + }); + + alert(`${name} added. It will appear in the dock and panel.`); + renderApplications(); + }); + } + async function renderServicesConfig() { const cfgRes = await Atlus.apiFetch('/api/settings'); const cfg = await cfgRes.json(); const panelServices = cfg.panel_services || []; contentEl.innerHTML = ` -
Panel Services
+
Services
-
PINNED TO RIGHT PANEL
+
MONITORED SERVICES
- Comma-separated list of systemd unit names to show in the right panel. + Comma-separated list of systemd unit names to monitor via the Services app.