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 ``; } 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 = ` ${dot(last.ok)} ${item.name} ${item.url} ${fmtTs(last.ts)} ${last.ms ?? "—"} ${last.status_code ?? "—"} ${fmtPct(item.uptime24h)} ${fmtPct(item.uptime7d)} `; 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 = `
No incidents recorded.
`; 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 = `
DOWN ${start.toLocaleString()}
${end ? `
UP ${end.toLocaleString()}
` : `
ongoing...
`}
${durationMin !== null ? durationMin + " min" : ""}
`; incidentsList.appendChild(div); }); } closeIncidents.addEventListener("click", ()=> incidentPane.classList.add("hidden")); checkNowBtn.addEventListener("click", checkNow); addSiteBtn.addEventListener("click", openAdd); fetchStatus(); setInterval(fetchStatus, 30000);