ntdservices commited on
Commit
7f13b58
·
verified ·
1 Parent(s): 1e4ca11

Upload 3 files

Browse files
Files changed (2) hide show
  1. static/app.js +141 -0
  2. static/style.css +82 -0
static/app.js ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const tbody = document.getElementById("statusTbody");
2
+ const lastRefresh = document.getElementById("lastRefresh");
3
+ const checkNowBtn = document.getElementById("checkNowBtn");
4
+ const addSiteBtn = document.getElementById("addSiteBtn");
5
+ const addDialog = document.getElementById("addDialog");
6
+ const addForm = document.getElementById("addForm");
7
+ const cancelAdd = document.getElementById("cancelAdd");
8
+ const siteName = document.getElementById("siteName");
9
+ const siteUrl = document.getElementById("siteUrl");
10
+
11
+ const incidentPane = document.getElementById("incidentPane");
12
+ const incidentTitle = document.getElementById("incidentTitle");
13
+ const incidentsList = document.getElementById("incidentsList");
14
+ const closeIncidents = document.getElementById("closeIncidents");
15
+
16
+ function fmtTs(s){
17
+ if(!s) return "—";
18
+ const d = new Date(s);
19
+ return d.toLocaleString();
20
+ }
21
+ function fmtPct(v){
22
+ if(v === null || v === undefined) return "—";
23
+ return `${v.toFixed ? v.toFixed(2) : v}%`;
24
+ }
25
+ function dot(ok){
26
+ return `<span class="dot ${ok ? 'green':'red'}" title="${ok?'UP':'DOWN'}"></span>`;
27
+ }
28
+
29
+ async function fetchStatus(){
30
+ const res = await fetch("/api/status");
31
+ const data = await res.json();
32
+ tbody.innerHTML = "";
33
+ data.forEach(item => {
34
+ const last = item.last || {};
35
+ const tr = document.createElement("tr");
36
+ tr.innerHTML = `
37
+ <td>${dot(last.ok)}</td>
38
+ <td>${item.name}</td>
39
+ <td><a class="url" href="${item.url}" target="_blank" rel="noopener">${item.url}</a></td>
40
+ <td>${fmtTs(last.ts)}</td>
41
+ <td>${last.ms ?? "—"}</td>
42
+ <td>${last.status_code ?? "—"}</td>
43
+ <td><span class="badge">${fmtPct(item.uptime24h)}</span></td>
44
+ <td><span class="badge">${fmtPct(item.uptime7d)}</span></td>
45
+ <td class="row-actions">
46
+ <button class="ghost" data-action="incidents" data-url="${item.url}" data-name="${item.name}">Incidents</button>
47
+ <button class="ghost" data-action="delete" data-url="${item.url}">Delete</button>
48
+ </td>
49
+ `;
50
+ tbody.appendChild(tr);
51
+ });
52
+ lastRefresh.textContent = `Last refresh: ${new Date().toLocaleTimeString()}`;
53
+ }
54
+
55
+ async function checkNow(){
56
+ checkNowBtn.disabled = true;
57
+ try{
58
+ await fetch("/api/check-now", {method:"POST"});
59
+ await fetchStatus();
60
+ } finally {
61
+ checkNowBtn.disabled = false;
62
+ }
63
+ }
64
+
65
+ function openAdd(){
66
+ siteName.value = "";
67
+ siteUrl.value = "";
68
+ addDialog.showModal();
69
+ }
70
+ function closeAdd(){
71
+ addDialog.close();
72
+ }
73
+
74
+ addForm.addEventListener("submit", async (e) => {
75
+ e.preventDefault();
76
+ const body = { name: siteName.value || siteUrl.value, url: siteUrl.value };
77
+ if(!body.url) return;
78
+ const res = await fetch("/api/sites", {
79
+ method:"POST",
80
+ headers: { "Content-Type":"application/json" },
81
+ body: JSON.stringify(body)
82
+ });
83
+ if(res.ok){
84
+ closeAdd();
85
+ await fetchStatus();
86
+ } else {
87
+ alert("Failed to add site");
88
+ }
89
+ });
90
+
91
+ cancelAdd.addEventListener("click", (e)=>{ e.preventDefault(); closeAdd(); });
92
+
93
+ tbody.addEventListener("click", async (e) => {
94
+ const btn = e.target.closest("button");
95
+ if(!btn) return;
96
+ const action = btn.dataset.action;
97
+ const url = btn.dataset.url;
98
+ if(action === "delete"){
99
+ if(confirm(`Delete monitor for:\n${url}?`)){
100
+ await fetch(`/api/sites?url=${encodeURIComponent(url)}`, { method: "DELETE" });
101
+ await fetchStatus();
102
+ }
103
+ }
104
+ if(action === "incidents"){
105
+ await loadIncidents(url, btn.dataset.name || url);
106
+ }
107
+ });
108
+
109
+ async function loadIncidents(url, name){
110
+ const res = await fetch(`/api/incidents?url=${encodeURIComponent(url)}`);
111
+ const data = await res.json();
112
+ incidentPane.classList.remove("hidden");
113
+ incidentTitle.textContent = `Incidents — ${name}`;
114
+ if(!data.length){
115
+ incidentsList.innerHTML = `<div class="muted" style="padding:8px 2px">No incidents recorded.</div>`;
116
+ return;
117
+ }
118
+ incidentsList.innerHTML = "";
119
+ data.forEach(x => {
120
+ const end = x.end_ts ? new Date(x.end_ts) : null;
121
+ const start = new Date(x.start_ts);
122
+ const durationMin = end ? Math.max(0, Math.round((end - start)/60000)) : null;
123
+ const div = document.createElement("div");
124
+ div.className = "incident";
125
+ div.innerHTML = `
126
+ <div>
127
+ <div><strong class="down">DOWN</strong> ${start.toLocaleString()}</div>
128
+ ${end ? `<div><strong class="ok">UP</strong> ${end.toLocaleString()}</div>` : `<div class="muted">ongoing...</div>`}
129
+ </div>
130
+ <div class="muted">${durationMin !== null ? durationMin + " min" : ""}</div>
131
+ `;
132
+ incidentsList.appendChild(div);
133
+ });
134
+ }
135
+ closeIncidents.addEventListener("click", ()=> incidentPane.classList.add("hidden"));
136
+
137
+ checkNowBtn.addEventListener("click", checkNow);
138
+ addSiteBtn.addEventListener("click", openAdd);
139
+
140
+ fetchStatus();
141
+ setInterval(fetchStatus, 30000);
static/style.css ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root{
2
+ --bg:#0b1220; --panel:#121a2e; --card:#0f1730; --text:#e6edf7; --muted:#9fb0cf;
3
+ --green:#18c37e; --red:#ff6363; --accent:#3a8dde; --ring: rgba(58,141,222,.35);
4
+ --radius:16px; --shadow: 0 10px 28px rgba(2,8,23,.35);
5
+ }
6
+
7
+ *{box-sizing:border-box}
8
+ html,body{height:100%}
9
+ body{
10
+ margin:0; font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial;
11
+ color:var(--text); background:linear-gradient(180deg,#0b1220,#0b1220 60%, #0d1530);
12
+ }
13
+
14
+ header{
15
+ display:flex; align-items:center; justify-content:space-between;
16
+ padding:16px 22px; position:sticky; top:0; background:#0b1220cc; backdrop-filter:saturate(120%) blur(6px);
17
+ border-bottom:1px solid #1b2542;
18
+ }
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
+
23
+ .actions button{
24
+ background:var(--accent); color:white; border:none; padding:10px 14px; border-radius:12px; cursor:pointer;
25
+ box-shadow:var(--shadow); margin-left:8px; font-weight:600;
26
+ }
27
+ .actions button#addSiteBtn{background:#243254; border:1px solid #2d3c63}
28
+
29
+ main{max-width:1100px; margin:26px auto; padding:0 16px; display:grid; gap:18px}
30
+ .card{
31
+ background:var(--panel); border-radius:var(--radius); box-shadow:var(--shadow); border:1px solid #1b2542;
32
+ }
33
+ .card-head{display:flex; align-items:center; justify-content:space-between; padding:16px 18px; border-bottom:1px solid #1b2542}
34
+ .muted{color:var(--muted); font-size:13px}
35
+ .table-wrap{overflow:auto}
36
+ table{width:100%; border-collapse:collapse}
37
+ th, td{padding:12px 14px; border-bottom:1px solid #1b2542; text-align:left}
38
+ th{color:var(--muted); font-weight:600; background: #0f1730}
39
+ tbody tr:hover{background:#0f1730}
40
+
41
+ .dot{
42
+ width:12px; height:12px; border-radius:50%;
43
+ box-shadow:0 0 0 3px #0b1220, 0 0 16px rgba(0,0,0,.25);
44
+ display:inline-block;
45
+ }
46
+ .dot.green{background:var(--green)}
47
+ .dot.red{background:var(--red)}
48
+
49
+ a.url{color:#a9c6ff; text-decoration:none}
50
+ a.url:hover{text-decoration:underline}
51
+
52
+ .badge{padding:4px 8px; border-radius:999px; font-size:12px; border:1px solid #263257; color:#c9d7ff; background:#102143}
53
+
54
+ td .row-actions{display:flex; gap:8px}
55
+ button.ghost{
56
+ background:transparent; border:1px solid #27355f; color:#c9d7ff; padding:8px 12px; border-radius:12px; cursor:pointer;
57
+ }
58
+
59
+ .hidden{display:none}
60
+
61
+ .incidents{padding:10px 16px}
62
+ .incident{
63
+ display:flex; align-items:center; justify-content:space-between;
64
+ background:#0f1730; border:1px solid #1b2542; border-radius:12px; padding:10px 12px; margin-bottom:10px;
65
+ }
66
+ .incident .down{color:#ff9f9f}
67
+ .incident .ok{color:#9fffc7}
68
+
69
+ dialog{
70
+ border:none; border-radius:18px; padding:0; background:#111a31; color:var(--text); box-shadow: var(--shadow);
71
+ }
72
+ .dialog-card{padding:18px; width:360px}
73
+ .dialog-card h3{margin:0 0 10px}
74
+ .dialog-card label{display:block; margin:10px 0}
75
+ .dialog-card input{
76
+ width:100%; padding:10px 12px; border-radius:12px; border:1px solid #283663; background:#0f1730; color:var(--text);
77
+ }
78
+ .dialog-card .row{display:flex; gap:10px; margin-top:14px}
79
+ .dialog-card button{
80
+ background:var(--accent); color:white; border:none; padding:10px 14px; border-radius:12px; cursor:pointer; font-weight:600;
81
+ }
82
+ .dialog-card button.ghost{background:transparent; border:1px solid #27355f; color:#c9d7ff}