uptimer2 / static /index.html
ntdservices's picture
Upload 4 files
18cd33a verified
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Uptime Monitor</title>
<style>
:root{
--bg:#0b1220; --panel:#121a2e; --card:#0f1730; --text:#e6edf7; --muted:#9fb0cf;
--green:#18c37e; --red:#ff6363; --accent:#3a8dde; --ring: rgba(58,141,222,.35);
--radius:16px; --shadow: 0 10px 28px rgba(2,8,23,.35);
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0; font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
color:var(--text); background:linear-gradient(180deg,#0b1220,#0b1220 60%, #0d1530);}
header{display:flex; align-items:center; justify-content:space-between; padding:16px 22px;
position:sticky; top:0; background:#0b1220cc; backdrop-filter:saturate(120%) blur(6px); border-bottom:1px solid #1b2542;}
.brand{display:flex; align-items:center; gap:12px;}
.logo-dot{width:14px; height:14px; border-radius:50%; background:linear-gradient(135deg,var(--accent),#6ea8ff); box-shadow:0 0 20px #2e6fd6;}
.title{font-weight:700; letter-spacing:.3px}
.actions button{background:var(--accent); color:white; border:none; padding:10px 14px; border-radius:12px; cursor:pointer;
box-shadow:var(--shadow); margin-left:8px; font-weight:600;}
.actions button#addSiteBtn{background:#243254; border:1px solid #2d3c63}
main{max-width:1100px; margin:26px auto; padding:0 16px; display:grid; gap:18px}
.card{background:var(--panel); border-radius:var(--radius); box-shadow:var(--shadow); border:1px solid #1b2542;}
.card-head{display:flex; align-items:center; justify-content:space-between; padding:16px 18px; border-bottom:1px solid #1b2542}
.muted{color:var(--muted); font-size:13px}
.table-wrap{overflow:auto}
table{width:100%; border-collapse:collapse}
th, td{padding:12px 14px; border-bottom:1px solid #1b2542; text-align:left}
th{color:var(--muted); font-weight:600; background:#0f1730}
tbody tr:hover{background:#0f1730}
.dot{width:12px; height:12px; border-radius:50%; box-shadow:0 0 0 3px #0b1220, 0 0 16px rgba(0,0,0,.25); display:inline-block;}
.dot.green{background:var(--green)} .dot.red{background:var(--red)}
a.url{color:#a9c6ff; text-decoration:none} a.url:hover{text-decoration:underline}
.badge{padding:4px 8px; border-radius:999px; font-size:12px; border:1px solid #263257; color:#c9d7ff; background:#102143}
td .row-actions{display:flex; gap:8px}
button.ghost{background:transparent; border:1px solid #27355f; color:#c9d7ff; padding:8px 12px; border-radius:12px; cursor:pointer;}
.hidden{display:none}
.incidents{padding:10px 16px}
.incident{display:flex; align-items:center; justify-content:space-between; background:#0f1730; border:1px solid #1b2542;
border-radius:12px; padding:10px 12px; margin-bottom:10px;}
.incident .down{color:#ff9f9f} .incident .ok{color:#9fffc7}
dialog{border:none; border-radius:18px; padding:0; background:#111a31; color:var(--text); box-shadow: var(--shadow);}
.dialog-card{padding:18px; width:380px}
.dialog-card h3{margin:0 0 10px}
.dialog-card label{display:block; margin:10px 0}
.dialog-card input{width:100%; padding:10px 12px; border-radius:12px; border:1px solid #283663; background:#0f1730; color:var(--text);}
.dialog-card small{color:var(--muted)}
.dialog-card .row{display:flex; gap:10px; margin-top:14px}
.dialog-card button{background:var(--accent); color:white; border:none; padding:10px 14px; border-radius:12px; cursor:pointer; font-weight:600;}
.dialog-card button.ghost{background:transparent; border:1px solid #27355f; color:#c9d7ff}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="logo-dot"></div>
<div class="title">Uptime Monitor</div>
</div>
<div class="actions">
<button id="checkNowBtn">Check Now</button>
<button id="addSiteBtn">+ Add Site</button>
</div>
</header>
<main>
<section class="card">
<div class="card-head">
<h2>Monitors</h2>
<span id="lastRefresh" class="muted"></span>
</div>
<div class="table-wrap">
<table id="statusTable">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>URL</th>
<th>Last Check</th>
<th>Resp (ms)</th>
<th>Code</th>
<th>Uptime 24h</th>
<th>Uptime 7d</th>
<th></th>
</tr>
</thead>
<tbody id="statusTbody"></tbody>
</table>
</div>
</section>
<section id="incidentPane" class="card hidden">
<div class="card-head">
<h2 id="incidentTitle">Incidents</h2>
<button id="closeIncidents" class="ghost">Close</button>
</div>
<div id="incidentsList" class="incidents"></div>
</section>
</main>
<!-- Add site modal -->
<dialog id="addDialog">
<form method="dialog" id="addForm" class="dialog-card">
<h3>Add Site</h3>
<label>Display Name
<input type="text" id="siteName" placeholder="e.g., Weather API"/>
</label>
<label>URL
<input type="url" id="siteUrl" placeholder="https://example.com/api/ping" required/>
</label>
<label>Hugging Face token (optional)
<input type="password" id="hfToken" placeholder="hf_..." autocomplete="off"/>
<small>Needed for private/Org Spaces. Paste only the token (without “Bearer”).</small>
</label>
<div class="row">
<button type="submit" id="saveSite">Save</button>
<button id="cancelAdd" class="ghost">Cancel</button>
</div>
</form>
</dialog>
<script>
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 hfToken = document.getElementById("hfToken");
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 = "";
hfToken.value = "";
addDialog.showModal();
}
function closeAdd(){ addDialog.close(); }
addForm.addEventListener("submit", async (e) => {
e.preventDefault();
const body = { name: siteName.value || siteUrl.value, url: siteUrl.value };
const tok = (hfToken.value || "").trim();
if (tok) {
body.hf_token = tok.startsWith("Bearer ") ? tok.slice(7).trim() : tok;
}
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();
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"));
document.getElementById("checkNowBtn").addEventListener("click", checkNow);
document.getElementById("addSiteBtn").addEventListener("click", openAdd);
fetchStatus();
setInterval(fetchStatus, 30000);
</script>
</body>
</html>