Spaces:
Sleeping
Sleeping
Delete index.html
Browse files- index.html +0 -272
index.html
DELETED
|
@@ -1,272 +0,0 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="utf-8"/>
|
| 5 |
-
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
| 6 |
-
<title>Uptime Monitor</title>
|
| 7 |
-
<style>
|
| 8 |
-
:root{
|
| 9 |
-
--bg:#0b1220; --panel:#121a2e; --card:#0f1730; --text:#e6edf7; --muted:#9fb0cf;
|
| 10 |
-
--green:#18c37e; --red:#ff6363; --accent:#3a8dde; --ring: rgba(58,141,222,.35);
|
| 11 |
-
--radius:16px; --shadow: 0 10px 28px rgba(2,8,23,.35);
|
| 12 |
-
}
|
| 13 |
-
*{box-sizing:border-box}
|
| 14 |
-
html,body{height:100%}
|
| 15 |
-
body{margin:0; font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
|
| 16 |
-
color:var(--text); background:linear-gradient(180deg,#0b1220,#0b1220 60%, #0d1530);}
|
| 17 |
-
header{display:flex; align-items:center; justify-content:space-between; padding:16px 22px;
|
| 18 |
-
position:sticky; top:0; background:#0b1220cc; backdrop-filter:saturate(120%) blur(6px); border-bottom:1px solid #1b2542;}
|
| 19 |
-
.brand{display:flex; align-items:center; gap:12px;}
|
| 20 |
-
.logo-dot{width:14px; height:14px; border-radius:50%; background:linear-gradient(135deg,var(--accent),#6ea8ff); box-shadow:0 0 20px #2e6fd6;}
|
| 21 |
-
.title{font-weight:700; letter-spacing:.3px}
|
| 22 |
-
.actions button{background:var(--accent); color:white; border:none; padding:10px 14px; border-radius:12px; cursor:pointer;
|
| 23 |
-
box-shadow:var(--shadow); margin-left:8px; font-weight:600;}
|
| 24 |
-
.actions button#addSiteBtn{background:#243254; border:1px solid #2d3c63}
|
| 25 |
-
main{max-width:1100px; margin:26px auto; padding:0 16px; display:grid; gap:18px}
|
| 26 |
-
.card{background:var(--panel); border-radius:var(--radius); box-shadow:var(--shadow); border:1px solid #1b2542;}
|
| 27 |
-
.card-head{display:flex; align-items:center; justify-content:space-between; padding:16px 18px; border-bottom:1px solid #1b2542}
|
| 28 |
-
.muted{color:var(--muted); font-size:13px}
|
| 29 |
-
.table-wrap{overflow:auto}
|
| 30 |
-
table{width:100%; border-collapse:collapse}
|
| 31 |
-
th, td{padding:12px 14px; border-bottom:1px solid #1b2542; text-align:left}
|
| 32 |
-
th{color:var(--muted); font-weight:600; background:#0f1730}
|
| 33 |
-
tbody tr:hover{background:#0f1730}
|
| 34 |
-
.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;}
|
| 35 |
-
.dot.green{background:var(--green)} .dot.red{background:var(--red)}
|
| 36 |
-
a.url{color:#a9c6ff; text-decoration:none} a.url:hover{text-decoration:underline}
|
| 37 |
-
.badge{padding:4px 8px; border-radius:999px; font-size:12px; border:1px solid #263257; color:#c9d7ff; background:#102143}
|
| 38 |
-
td .row-actions{display:flex; gap:8px}
|
| 39 |
-
button.ghost{background:transparent; border:1px solid #27355f; color:#c9d7ff; padding:8px 12px; border-radius:12px; cursor:pointer;}
|
| 40 |
-
.hidden{display:none}
|
| 41 |
-
.incidents{padding:10px 16px}
|
| 42 |
-
.incident{display:flex; align-items:center; justify-content:space-between; background:#0f1730; border:1px solid #1b2542;
|
| 43 |
-
border-radius:12px; padding:10px 12px; margin-bottom:10px;}
|
| 44 |
-
.incident .down{color:#ff9f9f} .incident .ok{color:#9fffc7}
|
| 45 |
-
dialog{border:none; border-radius:18px; padding:0; background:#111a31; color:var(--text); box-shadow: var(--shadow);}
|
| 46 |
-
.dialog-card{padding:18px; width:380px}
|
| 47 |
-
.dialog-card h3{margin:0 0 10px}
|
| 48 |
-
.dialog-card label{display:block; margin:10px 0}
|
| 49 |
-
.dialog-card input{width:100%; padding:10px 12px; border-radius:12px; border:1px solid #283663; background:#0f1730; color:var(--text);}
|
| 50 |
-
.dialog-card small{color:var(--muted)}
|
| 51 |
-
.dialog-card .row{display:flex; gap:10px; margin-top:14px}
|
| 52 |
-
.dialog-card button{background:var(--accent); color:white; border:none; padding:10px 14px; border-radius:12px; cursor:pointer; font-weight:600;}
|
| 53 |
-
.dialog-card button.ghost{background:transparent; border:1px solid #27355f; color:#c9d7ff}
|
| 54 |
-
</style>
|
| 55 |
-
</head>
|
| 56 |
-
<body>
|
| 57 |
-
<header>
|
| 58 |
-
<div class="brand">
|
| 59 |
-
<div class="logo-dot"></div>
|
| 60 |
-
<div class="title">Uptime Monitor</div>
|
| 61 |
-
</div>
|
| 62 |
-
<div class="actions">
|
| 63 |
-
<button id="checkNowBtn">Check Now</button>
|
| 64 |
-
<button id="addSiteBtn">+ Add Site</button>
|
| 65 |
-
</div>
|
| 66 |
-
</header>
|
| 67 |
-
|
| 68 |
-
<main>
|
| 69 |
-
<section class="card">
|
| 70 |
-
<div class="card-head">
|
| 71 |
-
<h2>Monitors</h2>
|
| 72 |
-
<span id="lastRefresh" class="muted"></span>
|
| 73 |
-
</div>
|
| 74 |
-
<div class="table-wrap">
|
| 75 |
-
<table id="statusTable">
|
| 76 |
-
<thead>
|
| 77 |
-
<tr>
|
| 78 |
-
<th>Status</th>
|
| 79 |
-
<th>Name</th>
|
| 80 |
-
<th>URL</th>
|
| 81 |
-
<th>Last Check</th>
|
| 82 |
-
<th>Resp (ms)</th>
|
| 83 |
-
<th>Code</th>
|
| 84 |
-
<th>Uptime 24h</th>
|
| 85 |
-
<th>Uptime 7d</th>
|
| 86 |
-
<th></th>
|
| 87 |
-
</tr>
|
| 88 |
-
</thead>
|
| 89 |
-
<tbody id="statusTbody"></tbody>
|
| 90 |
-
</table>
|
| 91 |
-
</div>
|
| 92 |
-
</section>
|
| 93 |
-
|
| 94 |
-
<section id="incidentPane" class="card hidden">
|
| 95 |
-
<div class="card-head">
|
| 96 |
-
<h2 id="incidentTitle">Incidents</h2>
|
| 97 |
-
<button id="closeIncidents" class="ghost">Close</button>
|
| 98 |
-
</div>
|
| 99 |
-
<div id="incidentsList" class="incidents"></div>
|
| 100 |
-
</section>
|
| 101 |
-
</main>
|
| 102 |
-
|
| 103 |
-
<!-- Add site modal -->
|
| 104 |
-
<dialog id="addDialog">
|
| 105 |
-
<form method="dialog" id="addForm" class="dialog-card">
|
| 106 |
-
<h3>Add Site</h3>
|
| 107 |
-
<label>Display Name
|
| 108 |
-
<input type="text" id="siteName" placeholder="e.g., Weather API"/>
|
| 109 |
-
</label>
|
| 110 |
-
<label>URL
|
| 111 |
-
<input type="url" id="siteUrl" placeholder="https://example.com/api/ping" required/>
|
| 112 |
-
</label>
|
| 113 |
-
<label>Hugging Face token (optional)
|
| 114 |
-
<input type="password" id="hfToken" placeholder="hf_..." autocomplete="off"/>
|
| 115 |
-
<small>Needed for private/Org Spaces. Paste only the token (without “Bearer”).</small>
|
| 116 |
-
</label>
|
| 117 |
-
<div class="row">
|
| 118 |
-
<button type="submit" id="saveSite">Save</button>
|
| 119 |
-
<button id="cancelAdd" class="ghost">Cancel</button>
|
| 120 |
-
</div>
|
| 121 |
-
</form>
|
| 122 |
-
</dialog>
|
| 123 |
-
|
| 124 |
-
<script>
|
| 125 |
-
const tbody = document.getElementById("statusTbody");
|
| 126 |
-
const lastRefresh = document.getElementById("lastRefresh");
|
| 127 |
-
const checkNowBtn = document.getElementById("checkNowBtn");
|
| 128 |
-
const addSiteBtn = document.getElementById("addSiteBtn");
|
| 129 |
-
const addDialog = document.getElementById("addDialog");
|
| 130 |
-
const addForm = document.getElementById("addForm");
|
| 131 |
-
const cancelAdd = document.getElementById("cancelAdd");
|
| 132 |
-
const siteName = document.getElementById("siteName");
|
| 133 |
-
const siteUrl = document.getElementById("siteUrl");
|
| 134 |
-
const hfToken = document.getElementById("hfToken");
|
| 135 |
-
|
| 136 |
-
const incidentPane = document.getElementById("incidentPane");
|
| 137 |
-
const incidentTitle = document.getElementById("incidentTitle");
|
| 138 |
-
const incidentsList = document.getElementById("incidentsList");
|
| 139 |
-
const closeIncidents = document.getElementById("closeIncidents");
|
| 140 |
-
|
| 141 |
-
function fmtTs(s){
|
| 142 |
-
if(!s) return "—";
|
| 143 |
-
const d = new Date(s);
|
| 144 |
-
return d.toLocaleString();
|
| 145 |
-
}
|
| 146 |
-
function fmtPct(v){
|
| 147 |
-
if(v === null || v === undefined) return "—";
|
| 148 |
-
return `${v.toFixed ? v.toFixed(2) : v}%`;
|
| 149 |
-
}
|
| 150 |
-
function dot(ok){
|
| 151 |
-
return `<span class="dot ${ok ? 'green':'red'}" title="${ok?'UP':'DOWN'}"></span>`;
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
async function fetchStatus(){
|
| 155 |
-
const res = await fetch("/api/status");
|
| 156 |
-
const data = await res.json();
|
| 157 |
-
tbody.innerHTML = "";
|
| 158 |
-
data.forEach(item => {
|
| 159 |
-
const last = item.last || {};
|
| 160 |
-
const tr = document.createElement("tr");
|
| 161 |
-
tr.innerHTML = `
|
| 162 |
-
<td>${dot(last.ok)}</td>
|
| 163 |
-
<td>${item.name}</td>
|
| 164 |
-
<td><a class="url" href="${item.url}" target="_blank" rel="noopener">${item.url}</a></td>
|
| 165 |
-
<td>${fmtTs(last.ts)}</td>
|
| 166 |
-
<td>${last.ms ?? "—"}</td>
|
| 167 |
-
<td>${last.status_code ?? "—"}</td>
|
| 168 |
-
<td><span class="badge">${fmtPct(item.uptime24h)}</span></td>
|
| 169 |
-
<td><span class="badge">${fmtPct(item.uptime7d)}</span></td>
|
| 170 |
-
<td class="row-actions">
|
| 171 |
-
<button class="ghost" data-action="incidents" data-url="${item.url}" data-name="${item.name}">Incidents</button>
|
| 172 |
-
<button class="ghost" data-action="delete" data-url="${item.url}">Delete</button>
|
| 173 |
-
</td>
|
| 174 |
-
`;
|
| 175 |
-
tbody.appendChild(tr);
|
| 176 |
-
});
|
| 177 |
-
lastRefresh.textContent = `Last refresh: ${new Date().toLocaleTimeString()}`;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
async function checkNow(){
|
| 181 |
-
checkNowBtn.disabled = true;
|
| 182 |
-
try{
|
| 183 |
-
await fetch("/api/check-now", {method:"POST"});
|
| 184 |
-
await fetchStatus();
|
| 185 |
-
} finally {
|
| 186 |
-
checkNowBtn.disabled = false;
|
| 187 |
-
}
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
function openAdd(){
|
| 191 |
-
siteName.value = "";
|
| 192 |
-
siteUrl.value = "";
|
| 193 |
-
hfToken.value = "";
|
| 194 |
-
addDialog.showModal();
|
| 195 |
-
}
|
| 196 |
-
function closeAdd(){ addDialog.close(); }
|
| 197 |
-
|
| 198 |
-
addForm.addEventListener("submit", async (e) => {
|
| 199 |
-
e.preventDefault();
|
| 200 |
-
const body = { name: siteName.value || siteUrl.value, url: siteUrl.value };
|
| 201 |
-
const tok = (hfToken.value || "").trim();
|
| 202 |
-
if (tok) {
|
| 203 |
-
body.hf_token = tok.startsWith("Bearer ") ? tok.slice(7).trim() : tok;
|
| 204 |
-
}
|
| 205 |
-
const res = await fetch("/api/sites", {
|
| 206 |
-
method:"POST",
|
| 207 |
-
headers: { "Content-Type":"application/json" },
|
| 208 |
-
body: JSON.stringify(body)
|
| 209 |
-
});
|
| 210 |
-
if (res.ok){
|
| 211 |
-
closeAdd();
|
| 212 |
-
await fetchStatus();
|
| 213 |
-
} else {
|
| 214 |
-
const msg = await res.text();
|
| 215 |
-
alert("Failed to add site:\n" + msg);
|
| 216 |
-
}
|
| 217 |
-
});
|
| 218 |
-
|
| 219 |
-
cancelAdd.addEventListener("click", (e)=>{ e.preventDefault(); closeAdd(); });
|
| 220 |
-
|
| 221 |
-
tbody.addEventListener("click", async (e) => {
|
| 222 |
-
const btn = e.target.closest("button");
|
| 223 |
-
if(!btn) return;
|
| 224 |
-
const action = btn.dataset.action;
|
| 225 |
-
const url = btn.dataset.url;
|
| 226 |
-
if(action === "delete"){
|
| 227 |
-
if(confirm(`Delete monitor for:\n${url}?`)){
|
| 228 |
-
await fetch(`/api/sites?url=${encodeURIComponent(url)}`, { method: "DELETE" });
|
| 229 |
-
await fetchStatus();
|
| 230 |
-
}
|
| 231 |
-
}
|
| 232 |
-
if(action === "incidents"){
|
| 233 |
-
await loadIncidents(url, btn.dataset.name || url);
|
| 234 |
-
}
|
| 235 |
-
});
|
| 236 |
-
|
| 237 |
-
async function loadIncidents(url, name){
|
| 238 |
-
const res = await fetch(`/api/incidents?url=${encodeURIComponent(url)}`);
|
| 239 |
-
const data = await res.json();
|
| 240 |
-
incidentPane.classList.remove("hidden");
|
| 241 |
-
incidentTitle.textContent = `Incidents — ${name}`;
|
| 242 |
-
if(!data.length){
|
| 243 |
-
incidentsList.innerHTML = `<div class="muted" style="padding:8px 2px">No incidents recorded.</div>`;
|
| 244 |
-
return;
|
| 245 |
-
}
|
| 246 |
-
incidentsList.innerHTML = "";
|
| 247 |
-
data.forEach(x => {
|
| 248 |
-
const end = x.end_ts ? new Date(x.end_ts) : null;
|
| 249 |
-
const start = new Date(x.start_ts);
|
| 250 |
-
const durationMin = end ? Math.max(0, Math.round((end - start)/60000)) : null;
|
| 251 |
-
const div = document.createElement("div");
|
| 252 |
-
div.className = "incident";
|
| 253 |
-
div.innerHTML = `
|
| 254 |
-
<div>
|
| 255 |
-
<div><strong class="down">DOWN</strong> ${start.toLocaleString()}</div>
|
| 256 |
-
${end ? `<div><strong class="ok">UP</strong> ${end.toLocaleString()}</div>` : `<div class="muted">ongoing...</div>`}
|
| 257 |
-
</div>
|
| 258 |
-
<div class="muted">${durationMin !== null ? durationMin + " min" : ""}</div>
|
| 259 |
-
`;
|
| 260 |
-
incidentsList.appendChild(div);
|
| 261 |
-
});
|
| 262 |
-
}
|
| 263 |
-
closeIncidents.addEventListener("click", ()=> incidentPane.classList.add("hidden"));
|
| 264 |
-
|
| 265 |
-
document.getElementById("checkNowBtn").addEventListener("click", checkNow);
|
| 266 |
-
document.getElementById("addSiteBtn").addEventListener("click", openAdd);
|
| 267 |
-
|
| 268 |
-
fetchStatus();
|
| 269 |
-
setInterval(fetchStatus, 30000);
|
| 270 |
-
</script>
|
| 271 |
-
</body>
|
| 272 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|