ntdservices commited on
Commit
3e965b8
·
verified ·
1 Parent(s): 6e028f2

Upload index.html

Browse files
Files changed (1) hide show
  1. static/index.html +272 -0
static/index.html ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>