somratpro commited on
Commit
dbb4ecf
·
1 Parent(s): bd3ae30

feat: redesign UptimeRobot setup UI with improved status tracking and configuration state management

Browse files
Files changed (1) hide show
  1. health-server.js +312 -107
health-server.js CHANGED
@@ -47,17 +47,41 @@ function renderDashboard(data) {
47
  };
48
 
49
  const keepAwakeHtml = data.isPrivate
50
- ? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
 
 
 
 
51
  : `
52
- <div id="uptimerobot-flow">
53
- <p class="helper-text">Setup a free monitor to prevent this Space from sleeping.</p>
54
- <div class="input-group">
55
- <input type="password" id="ur-key" placeholder="UptimeRobot Main API Key">
56
- <button id="ur-btn" onclick="setupMonitor()">Set Up Monitor</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </div>
58
- <p id="ur-status"></p>
59
- </div>
60
- `;
61
 
62
  return `
63
  <!DOCTYPE html>
@@ -176,29 +200,109 @@ function renderDashboard(data) {
176
  text-align: left;
177
  }
178
  .keep-alive h3 { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
179
- .helper-text { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 16px; }
180
- .input-group { display: flex; gap: 8px; }
181
- input {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  flex: 1;
183
- background: #0f172a;
184
- border: 1px solid rgba(255,255,255,0.1);
185
- padding: 12px;
 
186
  border-radius: 12px;
187
- color: white;
188
- font-family: inherit;
189
  }
190
- button#ur-btn {
191
- background: rgba(255,255,255,0.05);
192
- border: none;
193
- color: white;
194
- padding: 0 16px;
 
 
195
  border-radius: 12px;
196
- cursor: pointer;
 
197
  font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  font-size: 0.85rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  }
200
- button#ur-btn:hover { background: rgba(255,255,255,0.1); }
201
- #ur-status { font-size: 0.8rem; margin-top: 8px; }
202
  </style>
203
  </head>
204
  <body>
@@ -228,38 +332,105 @@ function renderDashboard(data) {
228
 
229
  <a href="/app/" target="_blank" class="btn-primary">Open n8n Editor</a>
230
 
231
- <div class="keep-alive">
232
- <h3>Keep Alive</h3>
233
  ${keepAwakeHtml}
 
234
  </div>
235
  </div>
236
 
237
  <script>
238
- async function setupMonitor() {
239
- const key = document.getElementById('ur-key').value;
240
- const btn = document.getElementById('ur-btn');
241
- const status = document.getElementById('ur-status');
242
- if (!key) return alert('Please enter an API key');
243
-
244
- btn.disabled = true;
245
- btn.innerText = 'Setting up...';
246
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  try {
248
- const res = await fetch('/uptimerobot/setup', {
249
  method: 'POST',
250
- body: JSON.stringify({ apiKey: key })
 
251
  });
252
  const data = await res.json();
253
- status.innerText = data.message;
254
- status.style.color = res.ok ? '#22c55e' : '#ef4444';
255
- } catch (e) {
256
- status.innerText = 'Connection error.';
257
- status.style.color = '#ef4444';
 
 
 
 
 
 
 
 
 
 
 
258
  } finally {
259
- btn.disabled = false;
260
- btn.innerText = 'Set Up Monitor';
261
  }
262
  }
 
 
 
 
 
 
263
  </script>
264
  </body>
265
  </html>`;
@@ -302,6 +473,89 @@ async function resolveSpaceIsPrivate(req) {
302
  }
303
  }
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  const server = http.createServer(async (req, res) => {
306
  const url = parseRequestUrl(req.url);
307
  const pathname = url.pathname;
@@ -321,71 +575,22 @@ const server = http.createServer(async (req, res) => {
321
  );
322
  }
323
  if (pathname === "/uptimerobot/setup" && req.method === "POST") {
324
- let body = "";
325
- req.on("data", (c) => (body += c));
326
- req.on("end", async () => {
327
  try {
328
- const { apiKey } = JSON.parse(body);
329
- const host = req.headers.host;
330
- const monitorUrl = `https://${host}/health`;
331
- const post = (p, d) =>
332
- new Promise((res, rej) => {
333
- const r = https.request(
334
- {
335
- hostname: "api.uptimerobot.com",
336
- port: 443,
337
- path: p,
338
- method: "POST",
339
- headers: {
340
- "Content-Type": "application/x-www-form-urlencoded",
341
- },
342
- },
343
- (r) => {
344
- let b = "";
345
- r.on("data", (c) => (b += c));
346
- r.on("end", () => res(JSON.parse(b)));
347
- },
348
- );
349
- r.on("error", rej);
350
- r.write(new URLSearchParams(d).toString());
351
- r.end();
352
- });
353
-
354
- const existing = await post("/v2/getMonitors", {
355
- api_key: apiKey,
356
- format: "json",
357
- });
358
- if (existing.monitors?.some((m) => m.url === monitorUrl)) {
359
- res.writeHead(200);
360
- return res.end(
361
- JSON.stringify({ message: "Monitor already exists." }),
362
- );
363
- }
364
- const created = await post("/v2/newMonitor", {
365
- api_key: apiKey,
366
- format: "json",
367
- type: "1",
368
- friendly_name: `Hugging8n ${host}`,
369
- url: monitorUrl,
370
- interval: "300",
371
- });
372
- if (created.stat === "ok") {
373
- res.writeHead(200);
374
- return res.end(
375
- JSON.stringify({ message: "Monitor created successfully!" }),
376
- );
377
- }
378
- res.writeHead(400);
379
- res.end(
380
- JSON.stringify({
381
- message: created.error?.message || "Failed to create monitor.",
382
- }),
383
- );
384
  } catch (e) {
385
- res.writeHead(400);
386
- res.end(JSON.stringify({ message: "Invalid request." }));
387
  }
388
- });
389
  return;
390
  }
391
  if (pathname === "/" || pathname === "/dashboard") {
 
47
  };
48
 
49
  const keepAwakeHtml = data.isPrivate
50
+ ? `
51
+ <div id="uptimerobot-private-note" class="helper-summary">
52
+ <strong>This Space is private.</strong> External monitors cannot reliably access private HF health URLs, so keep-awake setup is only available on public Spaces.
53
+ </div>
54
+ `
55
  : `
56
+ <div id="uptimerobot-public-flow">
57
+ <div id="uptimerobot-summary" class="helper-summary">
58
+ One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.
59
+ </div>
60
+ <button id="uptimerobot-toggle" class="helper-toggle" type="button">
61
+ Set Up Monitor
62
+ </button>
63
+ <div id="uptimerobot-shell" class="helper-shell hidden">
64
+ <div class="helper-copy">
65
+ Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
66
+ </div>
67
+ <div class="helper-row">
68
+ <input
69
+ id="uptimerobot-key"
70
+ class="helper-input"
71
+ type="password"
72
+ placeholder="Paste your UptimeRobot Main API key"
73
+ autocomplete="off"
74
+ />
75
+ <button id="uptimerobot-btn" class="helper-button" type="button">
76
+ Create Monitor
77
+ </button>
78
+ </div>
79
+ <div class="helper-note">
80
+ One-time setup. Your key is only used to create the monitor for this Space.
81
+ </div>
82
+ </div>
83
  </div>
84
+ `;
 
 
85
 
86
  return `
87
  <!DOCTYPE html>
 
200
  text-align: left;
201
  }
202
  .keep-alive h3 { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
203
+
204
+ .helper-card {
205
+ width: 100%;
206
+ }
207
+ .helper-copy {
208
+ color: var(--text-muted);
209
+ font-size: 0.85rem;
210
+ line-height: 1.6;
211
+ margin-top: 10px;
212
+ }
213
+ .helper-copy strong {
214
+ color: var(--text);
215
+ }
216
+ .helper-row {
217
+ display: flex;
218
+ gap: 10px;
219
+ margin-top: 16px;
220
+ flex-wrap: wrap;
221
+ }
222
+ .helper-input {
223
  flex: 1;
224
+ min-width: 240px;
225
+ background: rgba(255, 255, 255, 0.04);
226
+ border: 1px solid rgba(255, 255, 255, 0.08);
227
+ color: var(--text);
228
  border-radius: 12px;
229
+ padding: 12px 16px;
230
+ font: inherit;
231
  }
232
+ .helper-input::placeholder {
233
+ color: var(--text-muted);
234
+ }
235
+ .helper-button {
236
+ background: var(--accent);
237
+ color: #fff;
238
+ border: 0;
239
  border-radius: 12px;
240
+ padding: 12px 18px;
241
+ font: inherit;
242
  font-weight: 600;
243
+ cursor: pointer;
244
+ }
245
+ .helper-button:disabled {
246
+ opacity: 0.6;
247
+ cursor: wait;
248
+ }
249
+ .hidden {
250
+ display: none !important;
251
+ }
252
+ .helper-note {
253
+ margin-top: 10px;
254
+ font-size: 0.82rem;
255
+ color: var(--text-muted);
256
+ }
257
+ .helper-result {
258
+ margin-top: 14px;
259
+ padding: 12px 14px;
260
+ border-radius: 12px;
261
+ font-size: 0.9rem;
262
+ display: none;
263
+ }
264
+ .helper-result.ok {
265
+ display: block;
266
+ background: rgba(34, 197, 94, 0.1);
267
+ color: var(--success);
268
+ }
269
+ .helper-result.error {
270
+ display: block;
271
+ background: rgba(239, 68, 68, 0.1);
272
+ color: var(--error);
273
+ }
274
+ .helper-shell {
275
+ margin-top: 12px;
276
+ }
277
+ .helper-summary {
278
+ margin-top: 14px;
279
+ padding: 12px 14px;
280
+ border-radius: 12px;
281
+ background: rgba(255, 255, 255, 0.03);
282
+ color: var(--text-muted);
283
  font-size: 0.85rem;
284
+ line-height: 1.5;
285
+ }
286
+ .helper-summary strong {
287
+ color: var(--text);
288
+ }
289
+ .helper-summary.success {
290
+ background: rgba(34, 197, 94, 0.08);
291
+ }
292
+ .helper-toggle {
293
+ margin-top: 14px;
294
+ display: inline-flex;
295
+ align-items: center;
296
+ justify-content: center;
297
+ background: rgba(255, 255, 255, 0.04);
298
+ color: var(--text);
299
+ border: 1px solid rgba(255, 255, 255, 0.08);
300
+ border-radius: 12px;
301
+ padding: 10px 16px;
302
+ font: inherit;
303
+ font-weight: 600;
304
+ cursor: pointer;
305
  }
 
 
306
  </style>
307
  </head>
308
  <body>
 
332
 
333
  <a href="/app/" target="_blank" class="btn-primary">Open n8n Editor</a>
334
 
335
+ <div class="keep-alive helper-card">
336
+ <span class="stat-label">Keep Space Awake</span>
337
  ${keepAwakeHtml}
338
+ <div id="uptimerobot-result" class="helper-result"></div>
339
  </div>
340
  </div>
341
 
342
  <script>
343
+ function getCurrentSearch() {
344
+ return window.location.search || '';
345
+ }
346
+
347
+ const monitorStateKey = 'hugging8n_uptimerobot_setup_v1';
348
+ const KEEP_AWAKE_PRIVATE = ${data.isPrivate ? "true" : "false"};
349
+
350
+ function setMonitorUiState(isConfigured) {
351
+ const summary = document.getElementById('uptimerobot-summary');
352
+ const shell = document.getElementById('uptimerobot-shell');
353
+ const toggle = document.getElementById('uptimerobot-toggle');
354
+
355
+ if (!summary || !shell || !toggle) return;
356
+
357
+ if (isConfigured) {
358
+ summary.classList.add('success');
359
+ summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
360
+ shell.classList.add('hidden');
361
+ toggle.textContent = 'Set Up Again';
362
+ } else {
363
+ summary.classList.remove('success');
364
+ summary.innerHTML = 'One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.';
365
+ toggle.textContent = 'Set Up Monitor';
366
+ }
367
+ }
368
+
369
+ function restoreMonitorUiState() {
370
+ try {
371
+ const value = window.localStorage.getItem(monitorStateKey);
372
+ setMonitorUiState(value === 'done');
373
+ } catch {
374
+ setMonitorUiState(false);
375
+ }
376
+ }
377
+
378
+ function toggleMonitorSetup() {
379
+ const shell = document.getElementById('uptimerobot-shell');
380
+ shell.classList.toggle('hidden');
381
+ }
382
+
383
+ async function setupUptimeRobot() {
384
+ const input = document.getElementById('uptimerobot-key');
385
+ const button = document.getElementById('uptimerobot-btn');
386
+ const result = document.getElementById('uptimerobot-result');
387
+ const apiKey = input.value.trim();
388
+
389
+ if (!apiKey) {
390
+ result.className = 'helper-result error';
391
+ result.textContent = 'Paste your UptimeRobot Main API key first.';
392
+ return;
393
+ }
394
+
395
+ button.disabled = true;
396
+ button.textContent = 'Creating...';
397
+ result.className = 'helper-result';
398
+ result.textContent = '';
399
+
400
  try {
401
+ const res = await fetch('/uptimerobot/setup' + getCurrentSearch(), {
402
  method: 'POST',
403
+ headers: { 'Content-Type': 'application/json' },
404
+ body: JSON.stringify({ apiKey })
405
  });
406
  const data = await res.json();
407
+
408
+ if (!res.ok) {
409
+ throw new Error(data.message || 'Failed to create monitor.');
410
+ }
411
+
412
+ result.className = 'helper-result ok';
413
+ result.textContent = data.message || 'UptimeRobot monitor is ready.';
414
+ input.value = '';
415
+ try {
416
+ window.localStorage.setItem(monitorStateKey, 'done');
417
+ } catch {}
418
+ setMonitorUiState(true);
419
+ document.getElementById('uptimerobot-shell').classList.add('hidden');
420
+ } catch (error) {
421
+ result.className = 'helper-result error';
422
+ result.textContent = error.message || 'Failed to create monitor.';
423
  } finally {
424
+ button.disabled = false;
425
+ button.textContent = 'Create Monitor';
426
  }
427
  }
428
+
429
+ if (!KEEP_AWAKE_PRIVATE) {
430
+ restoreMonitorUiState();
431
+ document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
432
+ document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
433
+ }
434
  </script>
435
  </body>
436
  </html>`;
 
473
  }
474
  }
475
 
476
+ function readRequestBody(req) {
477
+ return new Promise((resolve, reject) => {
478
+ let body = "";
479
+ req.on("data", (chunk) => {
480
+ body += chunk;
481
+ if (body.length > 1024 * 64) {
482
+ reject(new Error("Request too large"));
483
+ req.destroy();
484
+ }
485
+ });
486
+ req.on("end", () => resolve(body));
487
+ req.on("error", reject);
488
+ });
489
+ }
490
+
491
+ function postUptimeRobot(path, form) {
492
+ const body = new URLSearchParams(form).toString();
493
+ return new Promise((resolve, reject) => {
494
+ const request = https.request(
495
+ {
496
+ hostname: "api.uptimerobot.com",
497
+ port: 443,
498
+ method: "POST",
499
+ path,
500
+ headers: {
501
+ "Content-Type": "application/x-www-form-urlencoded",
502
+ "Content-Length": Buffer.byteLength(body),
503
+ },
504
+ },
505
+ (response) => {
506
+ let raw = "";
507
+ response.setEncoding("utf8");
508
+ response.on("data", (chunk) => {
509
+ raw += chunk;
510
+ });
511
+ response.on("end", () => {
512
+ try {
513
+ resolve(JSON.parse(raw));
514
+ } catch {
515
+ reject(new Error("Unexpected response from UptimeRobot"));
516
+ }
517
+ });
518
+ },
519
+ );
520
+ request.on("error", reject);
521
+ request.write(body);
522
+ request.end();
523
+ });
524
+ }
525
+
526
+ async function createUptimeRobotMonitor(apiKey, host) {
527
+ const cleanHost = String(host || "")
528
+ .replace(/^https?:\/\//, "")
529
+ .replace(/\/.*$/, "");
530
+ if (!cleanHost) throw new Error("Missing Space host.");
531
+ const monitorUrl = `https://${cleanHost}/health`;
532
+ const existing = await postUptimeRobot("/v2/getMonitors", {
533
+ api_key: apiKey,
534
+ format: "json",
535
+ logs: "0",
536
+ response_times: "0",
537
+ response_times_limit: "1",
538
+ });
539
+ const existingMonitor = Array.isArray(existing.monitors)
540
+ ? existing.monitors.find((m) => m.url === monitorUrl)
541
+ : null;
542
+ if (existingMonitor) {
543
+ return { created: false, message: `Monitor already exists for ${monitorUrl}` };
544
+ }
545
+ const created = await postUptimeRobot("/v2/newMonitor", {
546
+ api_key: apiKey,
547
+ format: "json",
548
+ type: "1",
549
+ friendly_name: `Hugging8n ${cleanHost}`,
550
+ url: monitorUrl,
551
+ interval: "300",
552
+ });
553
+ if (created.stat !== "ok") {
554
+ throw new Error(created?.error?.message || "Failed to create monitor.");
555
+ }
556
+ return { created: true, message: `Monitor created for ${monitorUrl}` };
557
+ }
558
+
559
  const server = http.createServer(async (req, res) => {
560
  const url = parseRequestUrl(req.url);
561
  const pathname = url.pathname;
 
575
  );
576
  }
577
  if (pathname === "/uptimerobot/setup" && req.method === "POST") {
578
+ void (async () => {
 
 
579
  try {
580
+ const body = await readRequestBody(req);
581
+ const { apiKey } = JSON.parse(body || "{}");
582
+ if (!apiKey) {
583
+ res.writeHead(400, { "Content-Type": "application/json" });
584
+ return res.end(JSON.stringify({ message: "API key is required." }));
585
+ }
586
+ const result = await createUptimeRobotMonitor(apiKey, req.headers.host);
587
+ res.writeHead(200, { "Content-Type": "application/json" });
588
+ res.end(JSON.stringify(result));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  } catch (e) {
590
+ res.writeHead(400, { "Content-Type": "application/json" });
591
+ res.end(JSON.stringify({ message: e.message || "Invalid request." }));
592
  }
593
+ })();
594
  return;
595
  }
596
  if (pathname === "/" || pathname === "/dashboard") {