Spaces:
Sleeping
Sleeping
| const tbody = document.getElementById("statusTbody"); | |
| const lastRefresh = document.getElementById("lastRefresh"); | |
| const checkNowBtn = document.getElementById("checkNowBtn"); | |
| const addSiteBtn = document.getElementById("addSiteBtn"); | |
| const addDialog = document.getElementById("addDialog"); | |
| const addForm = document.getElementById("addForm"); | |
| const cancelAdd = document.getElementById("cancelAdd"); | |
| const siteName = document.getElementById("siteName"); | |
| const siteUrl = document.getElementById("siteUrl"); | |
| const incidentPane = document.getElementById("incidentPane"); | |
| const incidentTitle = document.getElementById("incidentTitle"); | |
| const incidentsList = document.getElementById("incidentsList"); | |
| const closeIncidents = document.getElementById("closeIncidents"); | |
| function fmtTs(s){ | |
| if(!s) return "β"; | |
| const d = new Date(s); | |
| return d.toLocaleString(); | |
| } | |
| function fmtPct(v){ | |
| if(v === null || v === undefined) return "β"; | |
| return `${v.toFixed ? v.toFixed(2) : v}%`; | |
| } | |
| function dot(ok){ | |
| return `<span class="dot ${ok ? 'green':'red'}" title="${ok?'UP':'DOWN'}"></span>`; | |
| } | |
| async function fetchStatus(){ | |
| const res = await fetch("/api/status"); | |
| const data = await res.json(); | |
| tbody.innerHTML = ""; | |
| data.forEach(item => { | |
| const last = item.last || {}; | |
| const tr = document.createElement("tr"); | |
| tr.innerHTML = ` | |
| <td>${dot(last.ok)}</td> | |
| <td>${item.name}</td> | |
| <td><a class="url" href="${item.url}" target="_blank" rel="noopener">${item.url}</a></td> | |
| <td>${fmtTs(last.ts)}</td> | |
| <td>${last.ms ?? "β"}</td> | |
| <td>${last.status_code ?? "β"}</td> | |
| <td><span class="badge">${fmtPct(item.uptime24h)}</span></td> | |
| <td><span class="badge">${fmtPct(item.uptime7d)}</span></td> | |
| <td class="row-actions"> | |
| <button class="ghost" data-action="incidents" data-url="${item.url}" data-name="${item.name}">Incidents</button> | |
| <button class="ghost" data-action="delete" data-url="${item.url}">Delete</button> | |
| </td> | |
| `; | |
| tbody.appendChild(tr); | |
| }); | |
| lastRefresh.textContent = `Last refresh: ${new Date().toLocaleTimeString()}`; | |
| } | |
| async function checkNow(){ | |
| checkNowBtn.disabled = true; | |
| try{ | |
| await fetch("/api/check-now", {method:"POST"}); | |
| await fetchStatus(); | |
| } finally { | |
| checkNowBtn.disabled = false; | |
| } | |
| } | |
| function openAdd(){ | |
| siteName.value = ""; | |
| siteUrl.value = ""; | |
| addDialog.showModal(); | |
| } | |
| function closeAdd(){ | |
| addDialog.close(); | |
| } | |
| addForm.addEventListener("submit", async (e) => { | |
| e.preventDefault(); | |
| const body = { name: siteName.value || siteUrl.value, url: siteUrl.value }; | |
| if(!body.url) return; | |
| const res = await fetch("/api/sites", { | |
| method:"POST", | |
| headers: { "Content-Type":"application/json" }, | |
| body: JSON.stringify(body) | |
| }); | |
| if (res.ok){ | |
| closeAdd(); | |
| await fetchStatus(); | |
| } else { | |
| const msg = await res.text(); // <-- show backend detail | |
| alert("Failed to add site:\n" + msg); | |
| } | |
| }); | |
| cancelAdd.addEventListener("click", (e)=>{ e.preventDefault(); closeAdd(); }); | |
| tbody.addEventListener("click", async (e) => { | |
| const btn = e.target.closest("button"); | |
| if(!btn) return; | |
| const action = btn.dataset.action; | |
| const url = btn.dataset.url; | |
| if(action === "delete"){ | |
| if(confirm(`Delete monitor for:\n${url}?`)){ | |
| await fetch(`/api/sites?url=${encodeURIComponent(url)}`, { method: "DELETE" }); | |
| await fetchStatus(); | |
| } | |
| } | |
| if(action === "incidents"){ | |
| await loadIncidents(url, btn.dataset.name || url); | |
| } | |
| }); | |
| async function loadIncidents(url, name){ | |
| const res = await fetch(`/api/incidents?url=${encodeURIComponent(url)}`); | |
| const data = await res.json(); | |
| incidentPane.classList.remove("hidden"); | |
| incidentTitle.textContent = `Incidents β ${name}`; | |
| if(!data.length){ | |
| incidentsList.innerHTML = `<div class="muted" style="padding:8px 2px">No incidents recorded.</div>`; | |
| return; | |
| } | |
| incidentsList.innerHTML = ""; | |
| data.forEach(x => { | |
| const end = x.end_ts ? new Date(x.end_ts) : null; | |
| const start = new Date(x.start_ts); | |
| const durationMin = end ? Math.max(0, Math.round((end - start)/60000)) : null; | |
| const div = document.createElement("div"); | |
| div.className = "incident"; | |
| div.innerHTML = ` | |
| <div> | |
| <div><strong class="down">DOWN</strong> ${start.toLocaleString()}</div> | |
| ${end ? `<div><strong class="ok">UP</strong> ${end.toLocaleString()}</div>` : `<div class="muted">ongoing...</div>`} | |
| </div> | |
| <div class="muted">${durationMin !== null ? durationMin + " min" : ""}</div> | |
| `; | |
| incidentsList.appendChild(div); | |
| }); | |
| } | |
| closeIncidents.addEventListener("click", ()=> incidentPane.classList.add("hidden")); | |
| checkNowBtn.addEventListener("click", checkNow); | |
| addSiteBtn.addEventListener("click", openAdd); | |
| fetchStatus(); | |
| setInterval(fetchStatus, 30000); | |