atlus/frontend/js/apps/services.js
2026-03-14 22:41:00 -05:00

214 lines
8.1 KiB
JavaScript

/* 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 = '<span></span><span>Service</span><span>State</span><span>Sub</span><span></span>';
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 = `
<div class="svc-toggle">
<button class="service-toggle ${isActive ? 'on' : ''}" data-unit="${svc.unit}"></button>
</div>
<div>
<div class="svc-name">${svc.name}</div>
<div class="svc-desc">${svc.description}</div>
</div>
<span class="svc-state ${stateClass}">${svc.active}</span>
<span class="svc-sub">${svc.sub}</span>
<div class="svc-actions">
<button class="svc-action-btn svc-pin-btn ${isPinned ? 'pinned' : ''}" data-unit="${svc.unit}" title="${isPinned ? 'Unpin from panel' : 'Pin to panel'}">📌</button>
<button class="svc-action-btn" data-unit="${svc.unit}" data-action="restart" title="Restart">↻</button>
</div>
`;
// 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 = '<div style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);">Loading network info…</div>';
Atlus.apiFetch('/api/stats').then(res => res.json()).then(data => {
let html = '<div style="font-family:var(--font-mono);font-size:13px;">';
const ifaces = data.network.interfaces;
for (const [name, info] of Object.entries(ifaces)) {
html += `
<div style="margin-bottom:24px;padding:16px;background:var(--bg-titlebar);border:1px solid var(--border-structural);border-radius:var(--radius-md);">
<div style="font-size:16px;font-weight:500;color:var(--text-primary);margin-bottom:12px;">${name}</div>
<div style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;color:var(--text-secondary);">
<span style="color:var(--text-muted);">Status</span>
<span style="color:${info.up ? 'var(--status-green)' : 'var(--status-red)'};">${info.up ? 'Up' : 'Down'}</span>
<span style="color:var(--text-muted);">IPv4</span>
<span>${info.ipv4 || '--'}</span>
<span style="color:var(--text-muted);">IPv6</span>
<span>${info.ipv6 || '--'}</span>
</div>
</div>
`;
}
html += `
<div style="color:var(--text-muted);margin-top:8px;">
Total sent: ${Atlus.formatBytes(data.network.bytes_sent)} &middot;
Total recv: ${Atlus.formatBytes(data.network.bytes_recv)}
</div>
`;
html += '</div>';
el.innerHTML = html;
});
},
destroy() {},
});
})();