Full-stack implementation: FastAPI backend with PAM auth, WebSocket stats/terminal, and vanilla JS frontend with tiling desktop shell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
251 lines
10 KiB
JavaScript
251 lines
10 KiB
JavaScript
/* 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 = `
|
|
<div class="settings-section-title">General</div>
|
|
|
|
<div class="settings-group">
|
|
<div class="settings-group-title">SYSTEM</div>
|
|
<div class="settings-row">
|
|
<div>
|
|
<div class="settings-row-label">Hostname</div>
|
|
<div class="settings-row-desc">System network name</div>
|
|
</div>
|
|
<input class="settings-input" id="setHostname" value="${sys.hostname}">
|
|
</div>
|
|
<div class="settings-row">
|
|
<div>
|
|
<div class="settings-row-label">Timezone</div>
|
|
<div class="settings-row-desc">System timezone</div>
|
|
</div>
|
|
<input class="settings-input" id="setTimezone" value="${cfg.timezone || 'System default'}" placeholder="e.g. America/New_York">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-group">
|
|
<div class="settings-group-title">DISPLAY</div>
|
|
<div class="settings-row">
|
|
<div>
|
|
<div class="settings-row-label">Stats refresh interval</div>
|
|
<div class="settings-row-desc">Seconds between stat updates</div>
|
|
</div>
|
|
<input class="settings-input" id="setStatsInterval" type="number" min="1" max="30" value="${cfg.stats_interval_seconds || 2}" style="width:80px;">
|
|
</div>
|
|
<div class="settings-row">
|
|
<div>
|
|
<div class="settings-row-label">Session timeout</div>
|
|
<div class="settings-row-desc">Minutes before auto-logout</div>
|
|
</div>
|
|
<input class="settings-input" id="setSessionTimeout" type="number" min="5" max="10080" value="${cfg.session_timeout_minutes || 1440}" style="width:80px;">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-actions">
|
|
<button class="settings-btn" id="saveGeneral">Save</button>
|
|
</div>
|
|
`;
|
|
|
|
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.');
|
|
});
|
|
}
|
|
|
|
async function renderNetwork() {
|
|
const res = await Atlus.apiFetch('/api/stats');
|
|
const data = await res.json();
|
|
|
|
let html = '<div class="settings-section-title">Network</div>';
|
|
const ifaces = data.network.interfaces;
|
|
|
|
for (const [name, info] of Object.entries(ifaces)) {
|
|
html += `
|
|
<div class="settings-group">
|
|
<div class="settings-group-title">${name.toUpperCase()}</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">Status</div>
|
|
<span style="color:${info.up ? 'var(--status-green)' : 'var(--status-red)'};">${info.up ? 'Up' : 'Down'}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">IPv4</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${info.ipv4 || '--'}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">IPv6</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${info.ipv6 || '--'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
contentEl.innerHTML = html;
|
|
}
|
|
|
|
async function renderServicesConfig() {
|
|
const cfgRes = await Atlus.apiFetch('/api/settings');
|
|
const cfg = await cfgRes.json();
|
|
const panelServices = cfg.panel_services || [];
|
|
|
|
contentEl.innerHTML = `
|
|
<div class="settings-section-title">Panel Services</div>
|
|
<div class="settings-group">
|
|
<div class="settings-group-title">PINNED TO RIGHT PANEL</div>
|
|
<div class="settings-row-desc" style="margin-bottom:16px;">
|
|
Comma-separated list of systemd unit names to show in the right panel.
|
|
</div>
|
|
<textarea class="settings-input" id="setPanelServices" rows="4"
|
|
style="width:100%;height:100px;padding:12px;resize:vertical;"
|
|
placeholder="e.g. nginx.service, docker.service">${panelServices.join(', ')}</textarea>
|
|
</div>
|
|
<div class="settings-actions">
|
|
<button class="settings-btn" id="saveServices">Save</button>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="settings-section-title">About</div>
|
|
<div class="settings-group">
|
|
<div style="text-align:center;margin-bottom:32px;">
|
|
<div style="font-family:var(--font-mono);font-size:48px;font-weight:500;color:var(--accent);margin-bottom:8px;">A</div>
|
|
<div style="font-family:var(--font-mono);font-size:12px;color:var(--text-muted);letter-spacing:4px;">ATLUS v0.1.0</div>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">Hostname</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${sys.hostname}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">Operating System</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${sys.os}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">Kernel</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${sys.kernel}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">Architecture</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${sys.arch}</span>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">Python</div>
|
|
<span style="font-family:var(--font-mono);font-size:13px;">${sys.python}</span>
|
|
</div>
|
|
</div>
|
|
<div style="text-align:center;margin-top:32px;">
|
|
<div style="color:var(--text-muted);font-size:12px;font-family:var(--font-mono);">
|
|
Licensed under GPL-3.0
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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';
|
|
},
|
|
});
|
|
})();
|