| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- 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 = "";
- let socket = null;
- let reconnectTimer = 0;
- 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 =
- '<div class="row">' +
- '<span class="category"></span>' +
- '<span class="pill"><span class="dot"></span><span class="pill-label"></span></span>' +
- '</div>' +
- '<h2 class="name"></h2>' +
- '<div class="stats">' +
- '<div class="stat">' +
- '<span class="label">HTTP Status</span>' +
- '<span class="value status-code"></span>' +
- '</div>' +
- '<div class="stat">' +
- '<span class="label">Response Time</span>' +
- '<span class="value response-time"></span>' +
- '</div>' +
- '</div>';
- 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 '<div class="modal-stat">' +
- '<span class="label">' + escapeHTML(label) + '</span>' +
- '<span class="value">' + escapeHTML(value) + '</span>' +
- '</div>';
- }
- 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 scheduleReconnect() {
- if (reconnectTimer) {
- return;
- }
- reconnectTimer = window.setTimeout(function() {
- reconnectTimer = 0;
- connectWebSocket();
- }, 2000);
- }
- function connectWebSocket() {
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
- socket = new WebSocket(protocol + "//" + window.location.host + "/ws");
- socket.addEventListener("open", function() {
- loadInitialStatus().catch(function(error) {
- console.error("Failed to reload status after websocket reconnect:", error);
- });
- });
- 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() {
- socket = null;
- scheduleReconnect();
- });
- 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();
- });
|