Files
agent-dashboard/web/app.js
T
2026-06-02 14:36:41 +08:00

240 lines
6.2 KiB
JavaScript

(function () {
const STORAGE_KEY = 'agent_notify_token';
const POLL_MS = 3000;
const elHost = document.getElementById('filter-host');
const elAgent = document.getElementById('filter-agent');
const elStatus = document.getElementById('filter-status');
const elCwd = document.getElementById('filter-cwd');
const elSessions = document.getElementById('sessions');
const elError = document.getElementById('error');
const btnToken = document.getElementById('btn-token');
let expanded = new Set();
let pollTimer = null;
function getToken() {
return sessionStorage.getItem(STORAGE_KEY) || '';
}
function promptToken() {
const current = getToken();
const t = window.prompt('API token (Bearer):', current);
if (t === null) return false;
const trimmed = t.trim();
if (!trimmed) {
sessionStorage.removeItem(STORAGE_KEY);
return false;
}
sessionStorage.setItem(STORAGE_KEY, trimmed);
return true;
}
function ensureToken() {
if (getToken()) return true;
return promptToken();
}
function showError(msg) {
if (!msg) {
elError.textContent = '';
elError.classList.add('hidden');
return;
}
elError.textContent = msg;
elError.classList.remove('hidden');
}
function basename(path) {
if (!path) return '';
const i = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
return i >= 0 ? path.slice(i + 1) : path;
}
function formatMachine(row) {
const ips = (row.ips || []).join(', ');
return row.hostname + (ips ? ' (' + ips + ')' : '');
}
function formatTime(iso) {
if (!iso) return '';
try {
const d = new Date(iso);
return isNaN(d.getTime()) ? iso : d.toLocaleString();
} catch {
return iso;
}
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
async function apiGet(path) {
const token = getToken();
if (!token) throw new Error('No token');
const res = await fetch(path, {
headers: { Authorization: 'Bearer ' + token },
});
if (res.status === 401) {
sessionStorage.removeItem(STORAGE_KEY);
throw new Error('Unauthorized — check token');
}
if (!res.ok) {
throw new Error(res.status + ' ' + res.statusText);
}
return res.json();
}
function queryParams() {
const q = new URLSearchParams();
if (elHost.value) q.set('host', elHost.value);
if (elAgent.value) q.set('agent', elAgent.value);
if (elStatus.value) q.set('status', elStatus.value);
if (elCwd.value) q.set('cwd', elCwd.value);
const s = q.toString();
return s ? '?' + s : '';
}
function fillSelect(select, values, keepValue) {
const current = keepValue ? select.value : '';
while (select.options.length > 1) select.remove(1);
for (const v of values || []) {
const opt = document.createElement('option');
opt.value = v;
opt.textContent = v;
select.appendChild(opt);
}
if (current && [...select.options].some((o) => o.value === current)) {
select.value = current;
}
}
async function loadMeta() {
const meta = await apiGet('/api/v1/meta');
fillSelect(elHost, meta.hosts, true);
fillSelect(elAgent, meta.agents, true);
fillSelect(elCwd, meta.cwds, true);
}
async function loadStatus() {
const rows = await apiGet('/api/v1/status' + queryParams());
renderTable(Array.isArray(rows) ? rows : []);
}
function renderTable(rows) {
if (rows.length === 0) {
elSessions.innerHTML =
'<tr><td colspan="6" class="empty">No sessions</td></tr>';
return;
}
const parts = [];
for (const row of rows) {
const key = row.session_key || row.hostname + row.cwd + row.agent;
const offline = row.status === 'offline';
const cls = offline ? ' class="offline"' : '';
const open = expanded.has(key);
parts.push(
'<tr' +
cls +
' data-key="' +
escapeHtml(key) +
'">' +
'<td>' +
escapeHtml(formatMachine(row)) +
'</td>' +
'<td title="' +
escapeHtml(row.cwd || '') +
'">' +
escapeHtml(basename(row.cwd)) +
'</td>' +
'<td>' +
escapeHtml(row.agent || '') +
'</td>' +
'<td><span class="status">' +
escapeHtml(row.status || '') +
'</span></td>' +
'<td>' +
escapeHtml(formatTime(row.updated_at)) +
'</td>' +
'<td><button type="button" class="btn-expand" data-key="' +
escapeHtml(key) +
'">' +
(open ? 'Hide' : 'Details') +
'</button></td>' +
'</tr>'
);
if (open) {
parts.push(
'<tr class="details-row"><td colspan="6">' +
'<div class="label">last_user</div>' +
'<pre>' +
escapeHtml(row.last_user || '(empty)') +
'</pre>' +
'<div class="label">last_agent</div>' +
'<pre>' +
escapeHtml(row.last_agent || '(empty)') +
'</pre>' +
'</td></tr>'
);
}
}
elSessions.innerHTML = parts.join('');
}
async function refresh() {
if (!ensureToken()) {
showError('Token required');
return;
}
try {
showError('');
await Promise.all([loadMeta(), loadStatus()]);
} catch (e) {
showError(e.message || String(e));
if (String(e.message || '').includes('Unauthorized')) {
if (promptToken()) refresh();
}
}
}
function startPoll() {
stopPoll();
pollTimer = setInterval(refresh, POLL_MS);
}
function stopPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
[elHost, elAgent, elStatus, elCwd].forEach((el) => {
el.addEventListener('change', refresh);
});
btnToken.addEventListener('click', () => {
promptToken();
refresh();
});
elSessions.addEventListener('click', (e) => {
const btn = e.target.closest('.btn-expand');
if (!btn) return;
const key = btn.getAttribute('data-key');
if (expanded.has(key)) expanded.delete(key);
else expanded.add(key);
refresh();
});
if (!getToken()) promptToken();
refresh();
startPoll();
})();