Update UI

This commit is contained in:
Raven Scott
2026-05-30 18:17:15 -04:00
parent 7c233008ac
commit 43f69a59a7
2 changed files with 172 additions and 65 deletions
+167 -45
View File
@@ -3,6 +3,13 @@
let healthData = null;
let healthUpdateInterval = null;
const SERVICE_DEFS = [
{ key: 'dns', name: 'DNS Service', icon: '🌐' },
{ key: 'proxy', name: 'Proxy Service', icon: '🔒' },
{ key: 'swarm', name: 'Swarm', icon: '🔗' },
{ key: 'corestore', name: 'Corestore', icon: '💾' }
];
// Fetch health data
async function fetchHealth() {
try {
@@ -32,11 +39,12 @@ async function renderHealth() {
// Apply health payload (HTTP initial or WebSocket)
function applyHealthPayload(data) {
if (!data) return;
healthData = data;
updateHealthStatus(data);
renderServiceCards(data);
updateHealthHistory(data);
const incoming = normalizeHealthPayload(data);
if (!incoming) return;
healthData = mergeHealthPayload(healthData, incoming);
updateHealthStatus(healthData);
renderServiceCards(healthData);
updateHealthHistory(healthData);
}
// Update health status display
@@ -44,57 +52,172 @@ function updateHealthStatus(data) {
const statusEl = document.getElementById('health-status');
if (statusEl) {
statusEl.textContent = data.status === 'healthy' ? 'Healthy' : 'Degraded';
statusEl.className = data.status === 'healthy'
statusEl.className = data.status === 'healthy'
? 'text-2xl font-bold text-green-600 dark:text-green-400'
: 'text-2xl font-bold text-yellow-600 dark:text-yellow-400';
}
const uptimeEl = document.getElementById('health-uptime');
if (uptimeEl && data.uptime) {
uptimeEl.textContent = window.formatUptime ? window.formatUptime(data.uptime) : `${Math.floor(data.uptime / 1000)}s`;
}
}
// Render service status cards
function getServiceData(data, key) {
return data.services?.[key] || data.dependencies?.[key] || null;
}
function normalizeHealthPayload(data) {
if (!data) return null;
return {
status: data.status,
timestamp: data.timestamp,
uptime: data.uptime,
services: data.services,
dependencies: data.dependencies
};
}
function mergeServiceData(existing, incoming) {
if (!incoming) return existing || null;
if (!existing) return incoming;
const merged = { ...existing, ...incoming };
if (incoming.details != null) {
merged.details = { ...(existing.details || {}), ...incoming.details };
} else if (existing.details) {
merged.details = existing.details;
}
return merged;
}
function mergeHealthPayload(prev, next) {
if (!next) return prev;
if (!prev) return next;
const merged = {
...prev,
...next,
services: { ...(prev.services || {}) },
dependencies: { ...(prev.dependencies || {}) }
};
for (const [key, val] of Object.entries(next.services || {})) {
merged.services[key] = mergeServiceData(prev.services?.[key], val);
}
for (const [key, val] of Object.entries(next.dependencies || {})) {
merged.dependencies[key] = mergeServiceData(prev.dependencies?.[key], val);
}
return merged;
}
function formatDetailEntries(details) {
if (!details || typeof details !== 'object') return [];
return Object.entries(details).map(([key, value]) => ({ key, value: String(value) }));
}
function ensureServiceCards(container) {
if (container.dataset.initialized === '1') return;
container.dataset.initialized = '1';
container.innerHTML = SERVICE_DEFS.map((service) => `
<div class="rounded-lg shadow-md p-4 theme-card flex flex-col" data-service="${service.key}">
<div class="flex items-center justify-between mb-2 gap-2">
<div class="flex items-center gap-2 min-w-0">
<span class="text-2xl shrink-0" aria-hidden="true">${service.icon}</span>
<h3 class="text-lg font-semibold truncate">${service.name}</h3>
</div>
<span class="health-service-badge px-3 py-1 rounded-full text-sm font-semibold shrink-0 bg-gray-500" style="color: var(--text-primary);">-</span>
</div>
<p class="health-service-enabled text-sm text-gray-600 dark:text-gray-400 mb-2">-</p>
<dl class="health-service-details text-xs text-gray-600 dark:text-gray-400 space-y-1 min-h-[3rem] font-mono tabular-nums"></dl>
</div>
`).join('');
}
function updateServiceCardElement(card, serviceData) {
if (!serviceData) return;
const healthy = serviceData.healthy !== false;
const enabled = serviceData.enabled !== false;
const badge = card.querySelector('.health-service-badge');
if (badge) {
const statusText = healthy ? 'Healthy' : 'Unhealthy';
if (badge.textContent !== statusText) {
badge.textContent = statusText;
}
const nextClass = `health-service-badge px-3 py-1 rounded-full text-sm font-semibold shrink-0 ${
healthy ? 'bg-green-500' : 'bg-red-500'
}`;
if (badge.className !== nextClass) {
badge.className = nextClass;
}
badge.style.color = 'var(--text-primary)';
}
const enabledEl = card.querySelector('.health-service-enabled');
if (enabledEl) {
const enabledText = enabled ? 'Enabled' : 'Disabled';
if (enabledEl.textContent !== enabledText) {
enabledEl.textContent = enabledText;
}
}
const detailsEl = card.querySelector('.health-service-details');
if (!detailsEl || serviceData.details == null) return;
const entries = formatDetailEntries(serviceData.details);
const existingRows = new Map();
detailsEl.querySelectorAll('[data-detail-key]').forEach((row) => {
existingRows.set(row.dataset.detailKey, row);
});
for (const { key, value } of entries) {
let row = existingRows.get(key);
if (!row) {
row = document.createElement('div');
row.className = 'flex justify-between gap-3';
row.dataset.detailKey = key;
const label = document.createElement('span');
label.className = 'truncate opacity-80';
label.textContent = key;
const valueEl = document.createElement('span');
valueEl.className = 'health-detail-value shrink-0 text-right';
valueEl.textContent = value;
row.appendChild(label);
row.appendChild(valueEl);
detailsEl.appendChild(row);
} else {
const valueEl = row.querySelector('.health-detail-value');
if (valueEl && valueEl.textContent !== value) {
valueEl.textContent = value;
}
}
existingRows.delete(key);
}
// Only remove rows when the payload explicitly includes a new details object
for (const row of existingRows.values()) {
row.remove();
}
}
// Render or update service status cards in place (avoids layout shift on refresh)
function renderServiceCards(data) {
const services = [
{ key: 'dns', name: 'DNS Service', icon: '🌐' },
{ key: 'proxy', name: 'Proxy Service', icon: '🔒' },
{ key: 'swarm', name: 'Swarm', icon: '🔗' },
{ key: 'corestore', name: 'Corestore', icon: '💾' }
];
const container = document.getElementById('health-services');
if (!container) return;
container.innerHTML = services.map(service => {
const serviceData = data.services?.[service.key] || data.dependencies?.[service.key];
const healthy = serviceData?.healthy !== false;
const enabled = serviceData?.enabled !== false;
const statusColor = healthy ? 'green' : 'red';
const statusText = healthy ? 'Healthy' : 'Unhealthy';
return `
<div class="rounded-lg shadow-md p-4 theme-card">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-2xl">${service.icon}</span>
<h3 class="text-lg font-semibold">${service.name}</h3>
</div>
<span class="px-3 py-1 rounded-full text-sm font-semibold ${
healthy ? 'bg-green-500' :
'bg-red-500'
}" style="color: var(--text-primary);">
${statusText}
</span>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
${enabled ? 'Enabled' : 'Disabled'}
${serviceData?.details ? `${JSON.stringify(serviceData.details).replace(/[{}"]/g, '').substring(0, 50)}...` : ''}
</div>
</div>
`;
}).join('');
ensureServiceCards(container);
for (const service of SERVICE_DEFS) {
const card = container.querySelector(`[data-service="${service.key}"]`);
if (!card) continue;
updateServiceCardElement(card, getServiceData(data, service.key));
}
}
// Update health history
@@ -129,4 +252,3 @@ window.applyHealthPayload = applyHealthPayload;
window.renderHealth = renderHealth;
window.startHealthUpdates = startHealthUpdates;
window.stopHealthUpdates = stopHealthUpdates;