/* Atlus — Service Manager app */
(function () {
'use strict';
let container = null;
let listEl = null;
let searchInput = null;
let allServices = [];
let filterActive = false;
let pinnedServices = []; // unit names pinned to right panel
async function loadPinnedServices() {
try {
const res = await Atlus.apiFetch('/api/settings');
if (!res || !res.ok) return;
const cfg = await res.json();
pinnedServices = cfg.panel_services || [];
} catch (e) { /* ignore */ }
}
async function togglePin(unit) {
const idx = pinnedServices.indexOf(unit);
if (idx >= 0) {
pinnedServices.splice(idx, 1);
} else {
pinnedServices.push(unit);
}
// Save to settings
try {
await Atlus.apiFetch('/api/settings', {
method: 'PUT',
body: { panel_services: pinnedServices },
});
} catch (e) { /* ignore */ }
renderServices();
}
async function loadServices() {
try {
const res = await Atlus.apiFetch('/api/services');
if (!res.ok) return;
allServices = await res.json();
renderServices();
} catch (e) {}
}
function renderServices() {
if (!listEl) return;
const query = searchInput ? searchInput.value.toLowerCase() : '';
let filtered = allServices;
if (query) {
filtered = filtered.filter(s =>
s.name.toLowerCase().includes(query) ||
s.description.toLowerCase().includes(query)
);
}
if (filterActive) {
filtered = filtered.filter(s => s.active === 'active');
}
listEl.innerHTML = '';
// Header
const header = document.createElement('div');
header.className = 'services-header';
header.innerHTML = 'ServiceStateSub';
listEl.appendChild(header);
filtered.forEach(svc => {
const row = document.createElement('div');
row.className = 'service-row';
const isActive = svc.active === 'active';
const stateClass = svc.active === 'active' ? 'active' :
svc.active === 'failed' ? 'failed' : 'inactive';
const isPinned = pinnedServices.includes(svc.unit);
row.innerHTML = `
${svc.name}
${svc.description}
${svc.active}
${svc.sub}
`;
// Toggle handler
const toggle = row.querySelector('.service-toggle');
toggle.addEventListener('click', async () => {
const action = toggle.classList.contains('on') ? 'stop' : 'start';
await Atlus.apiFetch('/api/services/action', {
method: 'POST',
body: { unit: svc.unit, action },
});
loadServices();
});
// Restart handler
const restartBtn = row.querySelector('[data-action="restart"]');
restartBtn.addEventListener('click', async () => {
await Atlus.apiFetch('/api/services/action', {
method: 'POST',
body: { unit: svc.unit, action: 'restart' },
});
loadServices();
});
// Pin handler
const pinBtn = row.querySelector('.svc-pin-btn');
pinBtn.addEventListener('click', () => togglePin(svc.unit));
listEl.appendChild(row);
});
}
Atlus.registerApp('services', {
title: 'Services',
init(el) {
container = el;
container.classList.add('app-services');
// Toolbar
const toolbar = document.createElement('div');
toolbar.className = 'services-toolbar';
searchInput = document.createElement('input');
searchInput.className = 'services-search';
searchInput.type = 'text';
searchInput.placeholder = 'Search services…';
searchInput.addEventListener('input', renderServices);
toolbar.appendChild(searchInput);
const filterBtn = document.createElement('button');
filterBtn.className = 'services-filter-btn';
filterBtn.textContent = 'Active';
filterBtn.addEventListener('click', () => {
filterActive = !filterActive;
filterBtn.classList.toggle('active', filterActive);
renderServices();
});
toolbar.appendChild(filterBtn);
container.appendChild(toolbar);
// List
listEl = document.createElement('div');
listEl.className = 'services-list';
container.appendChild(listEl);
loadPinnedServices().then(() => loadServices());
},
destroy() {
container = null;
listEl = null;
searchInput = null;
allServices = [];
filterActive = false;
pinnedServices = [];
},
});
// Also register as "network" since it's a similar list-based view
Atlus.registerApp('network', {
title: 'Network',
init(el) {
el.classList.add('app-view');
el.style.padding = '24px';
el.style.overflow = 'auto';
el.innerHTML = 'Loading network info…
';
Atlus.apiFetch('/api/stats').then(res => res.json()).then(data => {
let html = '';
const ifaces = data.network.interfaces;
for (const [name, info] of Object.entries(ifaces)) {
html += `
${name}
Status
${info.up ? 'Up' : 'Down'}
IPv4
${info.ipv4 || '--'}
IPv6
${info.ipv6 || '--'}
`;
}
html += `
Total sent: ${Atlus.formatBytes(data.network.bytes_sent)} ·
Total recv: ${Atlus.formatBytes(data.network.bytes_recv)}
`;
html += '
';
el.innerHTML = html;
});
},
destroy() {},
});
})();