| Name | Message | Date |
|---|---|---|
| 📄 app.js | 7 hours ago | |
| 📄 index.html | 7 hours ago | |
| 📄 styles.css | 7 hours ago |
📄
wwwroot/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
const statusCards = document.getElementById("status-cards"); const liveStatus = document.getElementById("live-status"); const detectionList = document.getElementById("detection-list"); const detectionsEmpty = document.getElementById("detections-empty"); const refreshBtn = document.getElementById("refresh-btn"); const drawer = document.getElementById("detail-drawer"); const drawerBackdrop = document.getElementById("drawer-backdrop"); const drawerTitle = document.getElementById("drawer-title"); const drawerMeta = document.getElementById("drawer-meta"); const drawerPreview = document.getElementById("drawer-preview"); const closeDrawerBtn = document.getElementById("close-drawer"); function formatDate(iso) { if (!iso) return "—"; return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", }).format(new Date(iso)); } function formatInterval(seconds) { if (seconds < 60) return `${seconds}s`; if (seconds % 60 === 0) return `${seconds / 60} min`; return `${(seconds / 60).toFixed(1)} min`; } function card(label, value, { link = false } = {}) { const el = document.createElement("article"); el.className = "card"; el.innerHTML = ` <p class="card__label">${label}</p> <p class="card__value">${link ? `<a class="card__value--link" href="${value}" target="_blank" rel="noopener">${value}</a>` : value}</p> `; return el; } async function loadStatus() { const res = await fetch("/api/status"); if (!res.ok) throw new Error("Failed to load status"); return res.json(); } async function loadDetections() { const res = await fetch("/api/detections?limit=100"); if (!res.ok) throw new Error("Failed to load detections"); return res.json(); } function renderStatus(status) { statusCards.replaceChildren( card("Monitored URL", status.url, { link: true }), card("Check interval", formatInterval(status.intervalSeconds)), card("CSS selector", status.selector), card("Total detections", String(status.totalDetections)), card("Latest change", formatDate(status.latestDetectionAt)), card("SMS recipients", status.phoneNumbers.join(", ") || "—") ); liveStatus.querySelector("span:last-child").textContent = `Checking every ${formatInterval(status.intervalSeconds)}`; } function renderDetections(detections) { detectionList.replaceChildren(); if (detections.length === 0) { detectionsEmpty.classList.remove("hidden"); return; } detectionsEmpty.classList.add("hidden"); for (const item of detections) { const li = document.createElement("li"); li.className = "detection-item"; li.innerHTML = ` <div> <p class="detection-item__time">${formatDate(item.detectedAt)}</p> <p class="detection-item__preview">${escapeHtml(item.preview)}</p> </div> <span class="detection-item__action">View →</span> `; li.addEventListener("click", () => openDetection(item.id)); detectionList.appendChild(li); } } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } async function openDetection(id) { const res = await fetch(`/api/detections/${id}`); if (!res.ok) return; const detection = await res.json(); drawerTitle.textContent = `Detection #${detection.id}`; drawerMeta.textContent = `Detected ${formatDate(detection.detectedAt)}`; drawerPreview.replaceChildren(); const iframe = document.createElement("iframe"); iframe.sandbox = "allow-same-origin"; iframe.srcdoc = `<!DOCTYPE html><html><head><meta charset="utf-8"><base target="_blank"></head><body>${detection.html}</body></html>`; drawerPreview.appendChild(iframe); drawer.classList.remove("hidden"); drawerBackdrop.classList.remove("hidden"); drawer.setAttribute("aria-hidden", "false"); } function closeDrawer() { drawer.classList.add("hidden"); drawerBackdrop.classList.add("hidden"); drawer.setAttribute("aria-hidden", "true"); drawerPreview.replaceChildren(); } async function refresh() { try { const [status, detections] = await Promise.all([loadStatus(), loadDetections()]); renderStatus(status); renderDetections(detections); } catch { liveStatus.querySelector("span:last-child").textContent = "Connection error"; } } refreshBtn.addEventListener("click", refresh); closeDrawerBtn.addEventListener("click", closeDrawer); drawerBackdrop.addEventListener("click", closeDrawer); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDrawer(); }); refresh(); setInterval(refresh, 30_000);