Update UI
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user