/* Atlus — Settings app */ (function () { 'use strict'; let container = null; let contentEl = null; let activeSection = 'general'; const SECTIONS = [ { id: 'general', label: 'General' }, { id: 'applications', label: 'Applications' }, { id: 'network', label: 'Network' }, { id: 'services', label: 'Services' }, { id: 'about', label: 'About' }, ]; async function loadSection(section) { activeSection = section; // Update nav container.querySelectorAll('.settings-nav-item').forEach(item => { item.classList.toggle('active', item.dataset.section === section); }); 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(); } async function renderGeneral() { const res = await Atlus.apiFetch('/api/settings'); const cfg = await res.json(); const sysRes = await Atlus.apiFetch('/api/settings/system'); const sys = await sysRes.json(); contentEl.innerHTML = `
General
SYSTEM
Hostname
System network name
Timezone
System timezone
DISPLAY
Stats refresh interval
Seconds between stat updates
Session timeout
Minutes before auto-logout
Update scanner
Seconds between update checks
`; // 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) { await Atlus.apiFetch('/api/settings/hostname', { method: 'POST', body: { hostname }, }); } // Update timezone if changed if (timezone && timezone !== 'System default') { await Atlus.apiFetch('/api/settings/timezone', { method: 'POST', body: { timezone }, }); } // Update config await Atlus.apiFetch('/api/settings', { method: 'PUT', body: { 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. Update scanner changes take effect on page reload.'); }); } // --------------------------------------------------------------- // Network section — full NetworkManager integration // --------------------------------------------------------------- function _signalLabel(signal) { if (signal >= 80) return { text: 'Excellent', color: 'var(--status-green)' }; if (signal >= 60) return { text: 'Good', color: 'var(--status-green)' }; if (signal >= 40) return { text: 'Fair', color: 'var(--status-amber, #e09a2a)' }; return { text: 'Weak', color: 'var(--status-red)' }; } function _signalBars(signal) { const filled = Math.ceil(signal / 25); return '▂▄▆█'.split('').map((c, i) => `${c}` ).join(''); } async function renderNetwork() { contentEl.innerHTML = '
Network
Loading…
'; // Try NetworkManager first const statusRes = await Atlus.apiFetch('/api/network/status'); if (statusRes.status === 503) { // No nmcli — fall back to read-only psutil view return await _renderNetworkReadonly(); } if (!statusRes.ok) { contentEl.innerHTML = '
Network
Failed to load network status
'; return; } const status = await statusRes.json(); const devices = status.devices; let html = '
Network
'; // ---- Device overview ---- html += '
INTERFACES
'; for (const dev of devices) { const isUp = dev.state === 'connected'; const dotColor = isUp ? 'var(--status-green)' : 'var(--status-red)'; const ip = dev.ipv4 && dev.ipv4.address ? dev.ipv4.address : '--'; html += `
${dev.device}
${dev.type} · ${dev.connection || 'disconnected'}
${ip}
`; } html += '
'; // ---- Per-ethernet config ---- const ethDevices = devices.filter(d => d.type === 'ethernet' && d.connection); for (const dev of ethDevices) { html += `
${dev.device.toUpperCase()} CONFIGURATION
Loading…
`; } // ---- WiFi section ---- const wifiDev = devices.find(d => d.type === 'wifi'); if (wifiDev) { html += `
WI-FI
CONNECT TO HIDDEN NETWORK
SSID
Password
`; } // ---- Unconnected ethernet (no active connection) ---- const disconnectedEth = devices.filter(d => d.type === 'ethernet' && !d.connection); for (const dev of disconnectedEth) { html += `
${dev.device.toUpperCase()}
Status
Disconnected
`; } contentEl.innerHTML = html; // ---- Load ethernet configs ---- for (const dev of ethDevices) { _loadEthernetConfig(dev); } // ---- WiFi event handlers ---- if (wifiDev) { _renderWifiStatus(wifiDev); contentEl.querySelector('#wifiScanBtn').addEventListener('click', async (e) => { const btn = e.target; btn.disabled = true; btn.textContent = 'Scanning…'; await _loadWifiNetworks(); btn.disabled = false; btn.textContent = 'Scan Networks'; }); contentEl.querySelector('#hiddenConnectBtn').addEventListener('click', async () => { const ssid = contentEl.querySelector('#hiddenSsid').value.trim(); const pass = contentEl.querySelector('#hiddenPass').value; if (!ssid) return; await _connectWifi(ssid, pass, true); }); } } async function _renderNetworkReadonly() { // Fallback: use /api/network/interfaces (ip commands, no NetworkManager) let ifacesRes; try { ifacesRes = await Atlus.apiFetch('/api/network/interfaces'); } catch (e) { ifacesRes = null; } if (!ifacesRes || !ifacesRes.ok) { // Final fallback: read-only psutil view const res = await Atlus.apiFetch('/api/stats'); const data = await res.json(); let html = '
Network
'; const ifaces = data.network.interfaces; for (const [name, info] of Object.entries(ifaces)) { html += `
${name.toUpperCase()}
Status
${info.up ? 'Up' : 'Down'}
IPv4
${info.ipv4 || '--'}
`; } contentEl.innerHTML = html; return; } const ifData = await ifacesRes.json(); const ifaces = ifData.interfaces || []; const dnsServers = ifData.dns || []; let html = '
Network
'; // Per-interface cards for (const iface of ifaces) { const isUp = iface.up; const dotColor = isUp ? 'var(--status-green)' : 'var(--status-red)'; const ipDisplay = iface.ipv4 ? iface.ipv4.split('/')[0] : '--'; const cidr = iface.ipv4 || ''; const method = iface.config_method || 'unknown'; const isStatic = method === 'static'; html += `
${iface.name.toUpperCase()} ${iface.mac || ''}
Status
${iface.state}
IPv4
${ipDisplay}
${iface.gateway ? `
Gateway
${iface.gateway}
` : ''}
IP Configuration
${iface.wireless ? `
WiFi SSID
WiFi Password
` : ''}
IP Address
CIDR notation (e.g. 192.168.1.100/24)
Gateway
Primary DNS
Secondary DNS
`; } // DNS section html += `
DNS
Nameservers
${dnsServers.join(', ') || '--'}
`; contentEl.innerHTML = html; // ---- Event handlers ---- // Interface up/down toggle contentEl.querySelectorAll('[data-iface-toggle]').forEach(btn => { btn.addEventListener('click', async () => { const name = btn.dataset.ifaceToggle; const isUp = btn.dataset.isUp === 'true'; const action = isUp ? 'down' : 'up'; if (isUp && !confirm(`Bring down ${name}? You may lose connectivity if this is your primary interface.`)) return; btn.disabled = true; btn.textContent = isUp ? 'Disabling…' : 'Enabling…'; try { await Atlus.apiFetch(`/api/network/interfaces/${name}/${action}`, { method: 'POST' }); } catch (e) { /* may lose connection */ } setTimeout(() => _renderNetworkReadonly(), 2000); }); }); // DHCP/Static radio buttons contentEl.querySelectorAll('[data-iface-method]').forEach(group => { const name = group.dataset.ifaceMethod; const staticFields = contentEl.querySelector(`#staticFields-${name}`); group.querySelectorAll('input[type="radio"]').forEach(radio => { radio.addEventListener('change', () => { staticFields.classList.toggle('hidden', radio.value !== 'static'); }); }); }); // WiFi scan buttons contentEl.querySelectorAll('[data-wifi-scan]').forEach(btn => { btn.addEventListener('click', async () => { const name = btn.dataset.wifiScan; const resultsEl = contentEl.querySelector(`#wifiScanResults-${name}`); btn.disabled = true; btn.textContent = 'Scanning…'; resultsEl.classList.remove('hidden'); resultsEl.innerHTML = '
Scanning…
'; try { const res = await Atlus.apiFetch(`/api/network/wifi/scan/${name}`); if (!res.ok) { resultsEl.innerHTML = '
Failed to scan WiFi networks
'; } else { const networks = await res.json(); if (networks.length === 0) { resultsEl.innerHTML = '
No networks found
'; } else { resultsEl.innerHTML = ''; for (const net of networks) { const sig = _signalLabel(net.signal); const item = document.createElement('div'); item.className = 'wifi-scan-item'; item.innerHTML = ` ${net.ssid} ${sig.text} ${net.security || 'Open'} `; item.addEventListener('click', () => { contentEl.querySelector(`#ifSsid-${name}`).value = net.ssid; resultsEl.classList.add('hidden'); }); resultsEl.appendChild(item); } } } } catch (e) { resultsEl.innerHTML = '
Scan failed
'; } btn.disabled = false; btn.textContent = 'Scan'; }); }); // Apply config contentEl.querySelectorAll('[data-iface-apply]').forEach(btn => { btn.addEventListener('click', async () => { const name = btn.dataset.ifaceApply; const errEl = contentEl.querySelector(`#ifError-${name}`); errEl.classList.add('hidden'); const methodGroup = contentEl.querySelector(`[data-iface-method="${name}"]`); const selectedRadio = methodGroup.querySelector('input[type="radio"]:checked'); const method = selectedRadio ? selectedRadio.value : 'dhcp'; const isStatic = method === 'static'; const body = { method }; if (isStatic) { body.address = contentEl.querySelector(`#ifAddr-${name}`).value.trim(); body.gateway = contentEl.querySelector(`#ifGw-${name}`).value.trim(); const dns1 = contentEl.querySelector(`#ifDns1-${name}`).value.trim(); const dns2 = contentEl.querySelector(`#ifDns2-${name}`).value.trim(); body.dns = [dns1, dns2].filter(Boolean); if (!body.address || !body.gateway) { errEl.textContent = 'IP address and gateway are required for static configuration.'; errEl.classList.remove('hidden'); return; } } // Include WiFi credentials if this is a wireless interface const ssidInput = contentEl.querySelector(`#ifSsid-${name}`); const wifiPassInput = contentEl.querySelector(`#ifWifiPass-${name}`); if (ssidInput) { const ssid = ssidInput.value.trim(); if (ssid) body.ssid = ssid; if (wifiPassInput) { const wifiPass = wifiPassInput.value; if (wifiPass) body.wifi_password = wifiPass; } } if (!confirm(`Apply network changes to ${name}? If this is your primary interface, you may lose access temporarily.`)) return; btn.disabled = true; btn.textContent = 'Applying…'; try { const res = await Atlus.apiFetch(`/api/network/interfaces/${name}/config`, { method: 'PUT', body: body, }); if (res && res.ok) { btn.textContent = 'Applied ✓'; setTimeout(() => _renderNetworkReadonly(), 3000); } else { const err = res ? await res.json().catch(() => ({})) : {}; errEl.textContent = err.detail || 'Failed to apply configuration.'; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Apply'; } } catch (e) { // Connection may be lost if changing the interface we're connected through btn.textContent = 'Applied (reconnecting…)'; setTimeout(() => { window.location.reload(); }, 5000); } }); }); } async function _loadEthernetConfig(dev) { const groupEl = contentEl.querySelector(`#ethConfig-${dev.device}`); if (!groupEl) return; let connRes; try { connRes = await Atlus.apiFetch(`/api/network/connection/${encodeURIComponent(dev.connection)}`); } catch (e) { groupEl.innerHTML += '
Failed to load configuration
'; return; } if (!connRes.ok) { groupEl.innerHTML = `
${dev.device.toUpperCase()} CONFIGURATION
Failed to load configuration
`; return; } const conn = await connRes.json(); const isStatic = conn.ipv4.method === 'manual'; const currentAddr = conn.active_ipv4.address || conn.ipv4.addresses || ''; const currentGw = conn.active_ipv4.gateway || conn.ipv4.gateway || ''; const currentDns = conn.active_ipv4.dns.length ? conn.active_ipv4.dns : (conn.ipv4.dns ? conn.ipv4.dns.split(',').map(s => s.trim()) : []); groupEl.innerHTML = `
${dev.device.toUpperCase()} CONFIGURATION
IP Configuration
IP Address
CIDR notation (e.g. 192.168.1.100/24)
Gateway
Primary DNS
Secondary DNS
`; // Radio handler const methodRadios = groupEl.querySelector(`#methodRadios-${dev.device}`); const staticFields = groupEl.querySelector(`#staticFields-${dev.device}`); methodRadios.querySelectorAll('input[type="radio"]').forEach(radio => { radio.addEventListener('change', () => { staticFields.classList.toggle('hidden', radio.value !== 'static'); }); }); // Apply handler groupEl.querySelector(`#ethApply-${dev.device}`).addEventListener('click', async () => { const errEl = groupEl.querySelector(`#ethError-${dev.device}`); errEl.classList.add('hidden'); const selectedRadio = methodRadios.querySelector('input[type="radio"]:checked'); const isNowStatic = selectedRadio && selectedRadio.value === 'static'; const body = { method: isNowStatic ? 'manual' : 'auto' }; if (isNowStatic) { body.address = groupEl.querySelector(`#ethAddr-${dev.device}`).value.trim(); body.gateway = groupEl.querySelector(`#ethGw-${dev.device}`).value.trim(); const dns1 = groupEl.querySelector(`#ethDns1-${dev.device}`).value.trim(); const dns2 = groupEl.querySelector(`#ethDns2-${dev.device}`).value.trim(); body.dns = [dns1, dns2].filter(Boolean); if (!body.address || !body.gateway) { errEl.textContent = 'IP address and gateway are required for static configuration.'; errEl.classList.remove('hidden'); return; } } // Warn if configuring the interface we're connected through if (confirm(`Apply network changes to ${dev.device}? If you are connected through this interface, you may lose access temporarily.`)) { const applyBtn = groupEl.querySelector(`#ethApply-${dev.device}`); applyBtn.disabled = true; applyBtn.textContent = 'Applying…'; const res = await Atlus.apiFetch(`/api/network/connection/${encodeURIComponent(dev.connection)}/ipv4`, { method: 'PUT', body: body, }); if (res && res.ok) { applyBtn.textContent = 'Applied ✓'; setTimeout(() => renderNetwork(), 3000); } else { const err = res ? await res.json().catch(() => ({})) : {}; errEl.textContent = err.detail || 'Failed to apply configuration.'; errEl.classList.remove('hidden'); applyBtn.disabled = false; applyBtn.textContent = 'Apply'; } } }); } function _renderWifiStatus(wifiDev) { const el = contentEl.querySelector('#wifiStatus'); if (!el) return; if (wifiDev.state === 'connected' && wifiDev.connection) { const ip = wifiDev.ipv4 && wifiDev.ipv4.address ? wifiDev.ipv4.address : '--'; el.innerHTML = `
Connected to: ${wifiDev.connection}
${ip}
`; el.querySelector('#wifiDisconnectBtn').addEventListener('click', async () => { await Atlus.apiFetch('/api/network/wifi/disconnect', { method: 'POST' }); setTimeout(() => renderNetwork(), 1500); }); } else { el.innerHTML = `
Status
Disconnected
`; } } async function _loadWifiNetworks() { const listEl = contentEl.querySelector('#wifiNetworkList'); if (!listEl) return; listEl.innerHTML = '
Scanning…
'; const res = await Atlus.apiFetch('/api/network/wifi/list'); if (!res.ok) { listEl.innerHTML = '
Failed to scan WiFi networks
'; return; } const networks = await res.json(); if (networks.length === 0) { listEl.innerHTML = '
No networks found
'; return; } listEl.innerHTML = ''; for (const net of networks) { const sig = _signalLabel(net.signal); const isOpen = !net.security || net.security === '' || net.security === '--'; const lockIcon = isOpen ? '' : '🔒 '; const inUseTag = net.in_use ? ' ● CONNECTED' : ''; const item = document.createElement('div'); item.className = 'wifi-network-item'; item.innerHTML = `
${lockIcon}${net.ssid}${inUseTag}
${net.security || 'Open'} · ${sig.text} (${net.signal}%)
${_signalBars(net.signal)}
${net.in_use ? '' : ''} `; if (!net.in_use) { const connectBtn = item.querySelector('.wifi-connect-btn'); connectBtn.addEventListener('click', () => { if (isOpen) { _connectWifi(net.ssid, null, false); } else { _showWifiPasswordInput(item, net.ssid); } }); } listEl.appendChild(item); } } function _showWifiPasswordInput(itemEl, ssid) { // Check if already showing if (itemEl.querySelector('.wifi-password-row')) return; const row = document.createElement('div'); row.className = 'wifi-password-row'; row.innerHTML = ` `; row.querySelector('.wifi-pass-connect').addEventListener('click', () => { const pass = row.querySelector('.wifi-pass-input').value; _connectWifi(ssid, pass, false); }); row.querySelector('.wifi-pass-cancel').addEventListener('click', () => { row.remove(); }); // Enter key support row.querySelector('.wifi-pass-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { const pass = row.querySelector('.wifi-pass-input').value; _connectWifi(ssid, pass, false); } }); itemEl.appendChild(row); row.querySelector('.wifi-pass-input').focus(); } async function _connectWifi(ssid, password, hidden) { const body = { ssid, hidden }; if (password) body.password = password; const listEl = contentEl.querySelector('#wifiNetworkList'); if (listEl) { listEl.innerHTML = `
Connecting to ${ssid}…
`; } const res = await Atlus.apiFetch('/api/network/wifi/connect', { method: 'POST', body: body, }); if (res && res.ok) { setTimeout(() => renderNetwork(), 2000); } else { const err = res ? await res.json().catch(() => ({})) : {}; if (listEl) { listEl.innerHTML = `
Failed to connect: ${err.detail || 'Unknown error'}
`; } } } // --------------------------------------------------------------- // 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 = `
Services
MONITORED SERVICES
Comma-separated list of systemd unit names to monitor via the Services app.
`; contentEl.querySelector('#saveServices').addEventListener('click', async () => { const raw = contentEl.querySelector('#setPanelServices').value; const units = raw.split(',').map(s => s.trim()).filter(Boolean); await Atlus.apiFetch('/api/settings', { method: 'PUT', body: { panel_services: units }, }); alert('Panel services updated.'); }); } async function renderAbout() { const res = await Atlus.apiFetch('/api/settings/system'); const sys = await res.json(); contentEl.innerHTML = `
About
A
ATLUS v0.1.0
Hostname
${sys.hostname}
Operating System
${sys.os}
Kernel
${sys.kernel}
Architecture
${sys.arch}
Python
${sys.python}
Licensed under GPL-3.0
`; } Atlus.registerApp('settings', { title: 'Settings', init(el) { container = el; container.classList.add('app-settings'); // Nav const nav = document.createElement('div'); nav.className = 'settings-nav'; SECTIONS.forEach(s => { const item = document.createElement('button'); item.className = 'settings-nav-item' + (s.id === activeSection ? ' active' : ''); item.dataset.section = s.id; item.textContent = s.label; item.addEventListener('click', () => loadSection(s.id)); nav.appendChild(item); }); container.appendChild(nav); // Content contentEl = document.createElement('div'); contentEl.className = 'settings-content'; container.appendChild(contentEl); loadSection(activeSection); }, destroy() { container = null; contentEl = null; activeSection = 'general'; }, }); })();