/* 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
Save
`;
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
Scan Networks
CONNECT TO HIDDEN NETWORK
Connect
`;
}
// ---- Unconnected ethernet (no active connection) ----
const disconnectedEth = devices.filter(d => d.type === 'ethernet' && !d.connection);
for (const dev of disconnectedEth) {
html += `
${dev.device.toUpperCase()}
`;
}
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 || ''}
${iface.state}
${isUp ? 'Disable' : 'Enable'}
${iface.gateway ? `
` : ''}
${iface.wireless ? `
` : ''}
Apply
`;
}
// 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
Apply
`;
// 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}
Disconnect
`;
el.querySelector('#wifiDisconnectBtn').addEventListener('click', async () => {
await Atlus.apiFetch('/api/network/wifi/disconnect', { method: 'POST' });
setTimeout(() => renderNetwork(), 1500);
});
} else {
el.innerHTML = `
`;
}
}
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 ? '' : 'Connect '}
`;
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 = `
Connect
Cancel
`;
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.
Save
`;
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
Operating System
${sys.os}
`;
}
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';
},
});
})();