214 lines
8.1 KiB
JavaScript
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)} ·
|
|
Total recv: ${Atlus.formatBytes(data.network.bytes_recv)}
|
|
</div>
|
|
`;
|
|
html += '</div>';
|
|
el.innerHTML = html;
|
|
});
|
|
},
|
|
|
|
destroy() {},
|
|
});
|
|
})();
|