/* Atlus — Settings app */ (function () { 'use strict'; let container = null; let contentEl = null; let activeSection = 'general'; const SECTIONS = [ { id: 'general', label: 'General' }, { 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 === '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
`; 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); // 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, }, }); alert('Settings saved.'); }); } // --------------------------------------------------------------- // 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'}
`; } } } async function renderServicesConfig() { const cfgRes = await Atlus.apiFetch('/api/settings'); const cfg = await cfgRes.json(); const panelServices = cfg.panel_services || []; contentEl.innerHTML = `
Panel Services
PINNED TO RIGHT PANEL
Comma-separated list of systemd unit names to show in the right panel.
`; 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'; }, }); })();