@hasna/uptime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export declare function dashboardHtml(): string;
2
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"AAAA,wBAAgB,aAAa,IAAI,MAAM,CA2VtC"}
@@ -0,0 +1,353 @@
1
+ // @bun
2
+ // src/dashboard.ts
3
+ function dashboardHtml() {
4
+ return `<!doctype html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <title>Open Uptime</title>
10
+ <style>
11
+ :root {
12
+ color-scheme: light;
13
+ --bg: #f7f8fb;
14
+ --panel: #ffffff;
15
+ --text: #17202a;
16
+ --muted: #5f6b7a;
17
+ --line: #d8dee8;
18
+ --up: #157347;
19
+ --down: #b42318;
20
+ --warn: #9a6700;
21
+ --accent: #2457c5;
22
+ }
23
+ * { box-sizing: border-box; }
24
+ body {
25
+ margin: 0;
26
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ }
30
+ header {
31
+ border-bottom: 1px solid var(--line);
32
+ background: var(--panel);
33
+ padding: 18px 24px;
34
+ display: flex;
35
+ justify-content: space-between;
36
+ gap: 16px;
37
+ align-items: center;
38
+ }
39
+ h1 { margin: 0; font-size: 20px; letter-spacing: 0; }
40
+ main { padding: 24px; max-width: 1180px; margin: 0 auto; }
41
+ .grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
42
+ .panel {
43
+ background: var(--panel);
44
+ border: 1px solid var(--line);
45
+ border-radius: 8px;
46
+ padding: 16px;
47
+ }
48
+ .metric { font-size: 28px; font-weight: 700; margin-top: 6px; }
49
+ .muted { color: var(--muted); font-size: 13px; }
50
+ .toolbar { display: flex; gap: 8px; align-items: center; }
51
+ .stack { display: grid; gap: 16px; margin-top: 16px; }
52
+ .form-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); align-items: end; }
53
+ label { display: grid; gap: 5px; color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; }
54
+ input, select {
55
+ width: 100%;
56
+ border: 1px solid var(--line);
57
+ border-radius: 8px;
58
+ padding: 9px 10px;
59
+ background: white;
60
+ color: var(--text);
61
+ font: inherit;
62
+ }
63
+ button {
64
+ border: 1px solid var(--line);
65
+ border-radius: 8px;
66
+ background: var(--panel);
67
+ color: var(--text);
68
+ padding: 8px 12px;
69
+ cursor: pointer;
70
+ font-weight: 600;
71
+ }
72
+ button.primary { background: var(--accent); color: white; border-color: var(--accent); }
73
+ button.danger { color: var(--down); }
74
+ table { width: 100%; border-collapse: collapse; margin-top: 16px; }
75
+ th, td { text-align: left; border-bottom: 1px solid var(--line); padding: 11px 8px; vertical-align: top; }
76
+ th { color: var(--muted); font-size: 12px; text-transform: uppercase; }
77
+ .badge { display: inline-flex; min-width: 72px; justify-content: center; border-radius: 999px; padding: 4px 8px; font-size: 12px; font-weight: 700; }
78
+ .up { background: #dcfce7; color: var(--up); }
79
+ .down { background: #fee2e2; color: var(--down); }
80
+ .paused { background: #fef3c7; color: var(--warn); }
81
+ .unknown { background: #e5e7eb; color: #374151; }
82
+ .row-actions { display: flex; gap: 6px; flex-wrap: wrap; }
83
+ @media (max-width: 760px) {
84
+ header { align-items: flex-start; flex-direction: column; }
85
+ main { padding: 16px; }
86
+ table { display: block; overflow-x: auto; white-space: nowrap; }
87
+ }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <header>
92
+ <div>
93
+ <h1>Open Uptime</h1>
94
+ <div class="muted">Local uptime and downtime monitoring</div>
95
+ </div>
96
+ <div class="toolbar">
97
+ <button id="check-all" class="primary">Run Checks</button>
98
+ <button id="refresh">Refresh</button>
99
+ </div>
100
+ </header>
101
+ <main>
102
+ <section class="grid" id="metrics"></section>
103
+ <section class="panel" style="margin-top:16px">
104
+ <strong>Add Monitor</strong>
105
+ <form id="monitor-form" class="form-grid">
106
+ <label>Name<input id="form-name" required /></label>
107
+ <label>Kind<select id="form-kind"><option value="http">HTTP</option><option value="tcp">TCP</option></select></label>
108
+ <label>URL<input id="form-url" placeholder="https://example.com/health" /></label>
109
+ <label>Host<input id="form-host" placeholder="127.0.0.1" /></label>
110
+ <label>Port<input id="form-port" type="number" min="1" max="65535" /></label>
111
+ <label>Interval<input id="form-interval" type="number" min="1" value="60" /></label>
112
+ <label>Timeout<input id="form-timeout" type="number" min="1" value="5000" /></label>
113
+ <button id="form-submit" class="primary" type="submit">Add</button>
114
+ <button id="form-cancel" type="button">Cancel</button>
115
+ </form>
116
+ <div class="muted" id="form-status"></div>
117
+ </section>
118
+ <section class="panel" style="margin-top:16px">
119
+ <div style="display:flex;justify-content:space-between;gap:12px;align-items:center">
120
+ <div>
121
+ <strong>Monitors</strong>
122
+ <div class="muted" id="generated"></div>
123
+ </div>
124
+ </div>
125
+ <table>
126
+ <thead>
127
+ <tr><th>Status</th><th>Name</th><th>Target</th><th>Uptime</th><th>Latency</th><th>Last Check</th><th>Incident</th><th></th></tr>
128
+ </thead>
129
+ <tbody id="monitors"></tbody>
130
+ </table>
131
+ </section>
132
+ <section class="stack">
133
+ <section class="panel">
134
+ <strong>Recent Results</strong>
135
+ <table>
136
+ <thead><tr><th>Status</th><th>Monitor</th><th>Checked</th><th>Latency</th><th>Error</th></tr></thead>
137
+ <tbody id="results"></tbody>
138
+ </table>
139
+ </section>
140
+ <section class="panel">
141
+ <strong>Incidents</strong>
142
+ <table>
143
+ <thead><tr><th>Status</th><th>Monitor</th><th>Opened</th><th>Closed</th><th>Failures</th><th>Reason</th></tr></thead>
144
+ <tbody id="incidents"></tbody>
145
+ </table>
146
+ </section>
147
+ </section>
148
+ </main>
149
+ <script>
150
+ let monitorCache = [];
151
+ let editingId = null;
152
+ const fmt = (value) => value == null || value === '' ? '-' : String(value);
153
+ const pct = (value) => value == null ? '-' : Number(value).toFixed(2) + '%';
154
+ const byId = (id) => document.getElementById(id);
155
+ const text = (value) => document.createTextNode(fmt(value));
156
+ function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); }
157
+ function cell(value) {
158
+ const td = document.createElement('td');
159
+ td.appendChild(text(value));
160
+ return td;
161
+ }
162
+ function statusCell(status) {
163
+ const td = document.createElement('td');
164
+ const span = document.createElement('span');
165
+ const safe = ['up', 'down', 'paused', 'unknown'].includes(status) ? status : 'unknown';
166
+ span.className = 'badge ' + safe;
167
+ span.textContent = status || 'unknown';
168
+ td.appendChild(span);
169
+ return td;
170
+ }
171
+ function button(label, handler, className) {
172
+ const btn = document.createElement('button');
173
+ btn.type = 'button';
174
+ btn.textContent = label;
175
+ if (className) btn.className = className;
176
+ btn.addEventListener('click', handler);
177
+ return btn;
178
+ }
179
+ async function load() {
180
+ const [summary, results, incidents] = await Promise.all([
181
+ fetch('/api/summary').then((r) => r.json()),
182
+ fetch('/api/results?limit=20').then((r) => r.json()),
183
+ fetch('/api/incidents?limit=20').then((r) => r.json()),
184
+ ]);
185
+ monitorCache = summary.monitors.map((item) => item.monitor);
186
+ byId('generated').textContent = 'Generated ' + new Date(summary.generatedAt).toLocaleString();
187
+ renderMetrics(summary);
188
+ renderMonitors(summary);
189
+ renderResults(results);
190
+ renderIncidents(incidents);
191
+ }
192
+ function renderMetrics(summary) {
193
+ const root = byId('metrics');
194
+ clear(root);
195
+ for (const [label, value] of [
196
+ ['Monitors', summary.totals.monitors],
197
+ ['Up', summary.totals.up],
198
+ ['Down', summary.totals.down],
199
+ ['Open incidents', summary.totals.openIncidents],
200
+ ]) {
201
+ const panel = document.createElement('div');
202
+ panel.className = 'panel';
203
+ const small = document.createElement('div');
204
+ small.className = 'muted';
205
+ small.textContent = label;
206
+ const metric = document.createElement('div');
207
+ metric.className = 'metric';
208
+ metric.textContent = value;
209
+ panel.append(small, metric);
210
+ root.appendChild(panel);
211
+ }
212
+ }
213
+ function renderMonitors(summary) {
214
+ const root = byId('monitors');
215
+ clear(root);
216
+ for (const item of summary.monitors) {
217
+ const m = item.monitor;
218
+ const target = m.kind === 'http' ? m.url : m.host + ':' + m.port;
219
+ const incident = item.openIncident ? 'open since ' + new Date(item.openIncident.openedAt).toLocaleString() : '-';
220
+ const tr = document.createElement('tr');
221
+ const name = document.createElement('td');
222
+ const strong = document.createElement('strong');
223
+ strong.textContent = m.name;
224
+ const kind = document.createElement('div');
225
+ kind.className = 'muted';
226
+ kind.textContent = m.kind;
227
+ name.append(strong, kind);
228
+ const actions = document.createElement('td');
229
+ actions.className = 'row-actions';
230
+ actions.append(
231
+ button('Check', () => checkOne(m.id)),
232
+ button(m.enabled ? 'Pause' : 'Resume', () => setEnabled(m.id, !m.enabled)),
233
+ button('Edit', () => fillForm(m.id)),
234
+ button('Delete', () => deleteMonitor(m.id), 'danger'),
235
+ );
236
+ tr.append(
237
+ statusCell(m.status),
238
+ name,
239
+ cell(target),
240
+ cell(pct(item.uptimePercent)),
241
+ cell(item.averageLatencyMs == null ? '-' : item.averageLatencyMs + ' ms'),
242
+ cell(m.lastCheckedAt ? new Date(m.lastCheckedAt).toLocaleString() : '-'),
243
+ cell(incident),
244
+ actions,
245
+ );
246
+ root.appendChild(tr);
247
+ }
248
+ }
249
+ function renderResults(results) {
250
+ const root = byId('results');
251
+ clear(root);
252
+ for (const result of results) {
253
+ const tr = document.createElement('tr');
254
+ tr.append(
255
+ statusCell(result.status),
256
+ cell(result.monitorId),
257
+ cell(new Date(result.checkedAt).toLocaleString()),
258
+ cell(result.latencyMs == null ? '-' : result.latencyMs + ' ms'),
259
+ cell(result.error),
260
+ );
261
+ root.appendChild(tr);
262
+ }
263
+ }
264
+ function renderIncidents(incidents) {
265
+ const root = byId('incidents');
266
+ clear(root);
267
+ for (const incident of incidents) {
268
+ const tr = document.createElement('tr');
269
+ tr.append(
270
+ statusCell(incident.status),
271
+ cell(incident.monitorId),
272
+ cell(new Date(incident.openedAt).toLocaleString()),
273
+ cell(incident.closedAt ? new Date(incident.closedAt).toLocaleString() : '-'),
274
+ cell(incident.failureCount),
275
+ cell(incident.reason),
276
+ );
277
+ root.appendChild(tr);
278
+ }
279
+ }
280
+ async function checkOne(id) {
281
+ await fetch('/api/monitors/' + encodeURIComponent(id) + '/check', { method: 'POST' });
282
+ await load();
283
+ }
284
+ async function setEnabled(id, enabled) {
285
+ await fetch('/api/monitors/' + encodeURIComponent(id), {
286
+ method: 'PATCH',
287
+ headers: { 'content-type': 'application/json' },
288
+ body: JSON.stringify({ enabled }),
289
+ });
290
+ await load();
291
+ }
292
+ async function deleteMonitor(id) {
293
+ await fetch('/api/monitors/' + encodeURIComponent(id), { method: 'DELETE' });
294
+ await load();
295
+ }
296
+ function fillForm(id) {
297
+ const m = monitorCache.find((item) => item.id === id);
298
+ if (!m) return;
299
+ editingId = m.id;
300
+ byId('form-name').value = m.name;
301
+ byId('form-kind').value = m.kind;
302
+ byId('form-url').value = m.url || '';
303
+ byId('form-host').value = m.host || '';
304
+ byId('form-port').value = m.port || '';
305
+ byId('form-interval').value = m.intervalSeconds;
306
+ byId('form-timeout').value = m.timeoutMs;
307
+ byId('form-submit').textContent = 'Save';
308
+ byId('form-status').textContent = ['Editing', m.name].join(' ');
309
+ }
310
+ function resetForm() {
311
+ editingId = null;
312
+ byId('monitor-form').reset();
313
+ byId('form-submit').textContent = 'Add';
314
+ byId('form-status').textContent = '';
315
+ }
316
+ byId('monitor-form').addEventListener('submit', async (event) => {
317
+ event.preventDefault();
318
+ const kind = byId('form-kind').value;
319
+ const body = {
320
+ name: byId('form-name').value,
321
+ kind,
322
+ intervalSeconds: Number(byId('form-interval').value || 60),
323
+ timeoutMs: Number(byId('form-timeout').value || 5000),
324
+ };
325
+ if (kind === 'http') body.url = byId('form-url').value;
326
+ else {
327
+ body.host = byId('form-host').value;
328
+ body.port = Number(byId('form-port').value);
329
+ }
330
+ const response = await fetch(editingId ? '/api/monitors/' + encodeURIComponent(editingId) : '/api/monitors', {
331
+ method: editingId ? 'PATCH' : 'POST',
332
+ headers: { 'content-type': 'application/json' },
333
+ body: JSON.stringify(body),
334
+ });
335
+ const payload = await response.json();
336
+ byId('form-status').textContent = response.ok ? [editingId ? 'Saved' : 'Added', payload.name].join(' ') : payload.error;
337
+ if (response.ok) resetForm();
338
+ await load();
339
+ });
340
+ byId('form-cancel').addEventListener('click', resetForm);
341
+ byId('refresh').addEventListener('click', load);
342
+ byId('check-all').addEventListener('click', async () => {
343
+ await fetch('/api/check-all', { method: 'POST' });
344
+ await load();
345
+ });
346
+ load();
347
+ </script>
348
+ </body>
349
+ </html>`;
350
+ }
351
+ export {
352
+ dashboardHtml
353
+ };
@@ -0,0 +1,7 @@
1
+ export { createUptimeClient, UptimeService } from "./service.js";
2
+ export { UptimeStore } from "./store.js";
3
+ export { runMonitorCheck, runHttpCheck, runTcpCheck } from "./checks.js";
4
+ export { createApiHandler, serveUptime } from "./api.js";
5
+ export { uptimeHome, uptimeDbPath, ensureUptimeHome } from "./paths.js";
6
+ export type { CheckAttemptResult, CheckResult, CheckStatus, CreateMonitorInput, Incident, IncidentStatus, ListResultsOptions, Monitor, MonitorKind, MonitorStatus, MonitorSummary, SchedulerHandle, UpdateMonitorInput, UptimeSummary, } from "./types.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACxE,YAAY,EACV,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,cAAc,EACd,kBAAkB,EAClB,OAAO,EACP,WAAW,EACX,aAAa,EACb,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,aAAa,GACd,MAAM,YAAY,CAAC"}