const grid = document.getElementById("service-grid");
const modalBackdrop = document.getElementById("service-modal-backdrop");
const modalClose = document.getElementById("service-modal-close");
const modalTitle = document.getElementById("service-modal-title");
const modalCategory = document.getElementById("service-modal-category");
const modalGrid = document.getElementById("service-modal-grid");
const services = new Map();
const tiles = new Map();
let openServiceName = "";
function formatTimestamp(value) {
const date = new Date(value);
return date.toLocaleString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short"
});
}
function formatTime(value) {
const date = new Date(value);
return date.toLocaleTimeString(undefined, {
hour: "numeric",
minute: "2-digit",
second: "2-digit"
});
}
function escapeHTML(value) {
return String(value).replace(/[&<>"']/g, function(char) {
const entities = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return entities[char];
});
}
function sortServices(items) {
return items.sort(function(a, b) {
if (a.healthy === b.healthy) {
return a.name.localeCompare(b.name);
}
return a.healthy ? -1 : 1;
});
}
function serviceTile(service) {
const tile = document.createElement("article");
tile.className = "tile tile-enter";
tile.dataset.serviceName = service.name;
tile.innerHTML =
'
' +
'' +
'' +
'
' +
'' +
'' +
'
' +
'HTTP Status' +
'' +
'
' +
'
' +
'Response Time' +
'' +
'
' +
'
';
applyServiceToTile(tile, service);
tile.setAttribute("role", "button");
tile.setAttribute("tabindex", "0");
tile.addEventListener("click", function() {
openModal(service.name);
});
tile.addEventListener("keydown", function(event) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openModal(service.name);
}
});
window.setTimeout(function() {
tile.classList.remove("tile-enter");
}, 500);
return tile;
}
function applyServiceToTile(tile, service) {
const pill = tile.querySelector(".pill");
const pillLabel = tile.querySelector(".pill-label");
const statusCode = service.status_code > 0 ? service.status_code : "n/a";
tile.querySelector(".category").textContent = service.category + " · " + String(service.protocol || "").toUpperCase();
tile.querySelector(".name").textContent = service.name;
tile.querySelector(".status-code").textContent = statusCode;
tile.querySelector(".response-time").textContent = service.response_time;
pill.classList.toggle("ok", service.healthy);
pill.classList.toggle("bad", !service.healthy);
pillLabel.textContent = service.healthy ? "Healthy" : "Down";
}
function modalField(label, value) {
return '' +
'' + escapeHTML(label) + '' +
'' + escapeHTML(value) + '' +
'
';
}
function renderModal(service) {
const statusCode = service.status_code > 0 ? String(service.status_code) : "n/a";
modalCategory.textContent = service.category + " · " + String(service.protocol || "").toUpperCase();
modalTitle.textContent = service.name;
modalGrid.innerHTML =
modalField("Status", service.healthy ? "Healthy" : "Down") +
modalField("URL", service.url || "n/a") +
modalField("HTTP Status", statusCode) +
modalField("Response Time", service.response_time || "n/a") +
modalField("Last Checked", formatTimestamp(service.last_checked)) +
modalField("Message", service.message || "n/a") +
modalField("Details", service.details || "n/a") +
modalField("Category", service.category || "n/a") +
modalField("Protocol", String(service.protocol || "").toUpperCase() || "n/a");
}
function openModal(serviceName) {
const service = services.get(serviceName);
if (!service) {
return;
}
openServiceName = serviceName;
renderModal(service);
modalBackdrop.classList.remove("hidden");
document.body.classList.add("modal-open");
}
function closeModal() {
openServiceName = "";
modalBackdrop.classList.add("hidden");
document.body.classList.remove("modal-open");
}
function syncGridOrder(items) {
items.forEach(function(service) {
let tile = tiles.get(service.name);
if (!tile) {
tile = serviceTile(service);
tiles.set(service.name, tile);
} else {
applyServiceToTile(tile, service);
}
grid.appendChild(tile);
});
}
function syncTiles(items) {
const liveNames = new Set(items.map(function(service) {
return service.name;
}));
Array.from(tiles.keys()).forEach(function(name) {
if (!liveNames.has(name)) {
const tile = tiles.get(name);
if (tile) {
tile.remove();
}
tiles.delete(name);
}
});
}
function renderSnapshot() {
const items = sortServices(Array.from(services.values()));
if (items.length === 0) {
grid.replaceChildren();
return;
}
syncTiles(items);
syncGridOrder(items);
}
function upsertService(service) {
services.set(service.name, service);
const items = sortServices(Array.from(services.values()));
syncGridOrder(items);
if (openServiceName === service.name) {
renderModal(service);
}
}
function connectWebSocket() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const socket = new WebSocket(protocol + "//" + window.location.host + "/ws");
socket.addEventListener("open", function() {
});
socket.addEventListener("message", function(event) {
const payload = JSON.parse(event.data);
if (payload.type === "service_update" && payload.service) {
upsertService(payload.service);
}
});
socket.addEventListener("close", function() {
window.setTimeout(connectWebSocket, 2000);
});
socket.addEventListener("error", function() {
socket.close();
});
}
modalClose.addEventListener("click", closeModal);
modalBackdrop.addEventListener("click", function(event) {
if (event.target === modalBackdrop) {
closeModal();
}
});
document.addEventListener("keydown", function(event) {
if (event.key === "Escape" && !modalBackdrop.classList.contains("hidden")) {
closeModal();
}
});
function loadInitialStatus() {
return fetch("/api/status", {
headers: {
"Accept": "application/json"
}
}).then(function(response) {
if (!response.ok) {
throw new Error("Request failed with status " + response.status);
}
return response.json();
}).then(function(payload) {
services.clear();
(payload.services || []).forEach(function(service) {
services.set(service.name, service);
});
renderSnapshot();
});
}
loadInitialStatus().finally(function() {
connectWebSocket();
});