somratpro commited on
Commit
8236b3f
·
1 Parent(s): 0defb22

feat: add UptimeRobot integration to prevent free Space sleep with automated monitor creation

Browse files
Files changed (4) hide show
  1. .env.example +6 -0
  2. README.md +22 -0
  3. health-server.js +388 -1
  4. setup-uptimerobot.sh +84 -0
.env.example CHANGED
@@ -168,6 +168,12 @@ SYNC_INTERVAL=600
168
  # Webhooks: Standard POST notifications for lifecycle events
169
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
170
 
 
 
 
 
 
 
171
  # Trusted proxies (comma-separated IPs)
172
  # Fixes "Proxy headers detected from untrusted address" behind reverse proxies
173
  # Only set if you see pairing/auth errors. Find IPs in Space logs (remote=x.x.x.x)
 
168
  # Webhooks: Standard POST notifications for lifecycle events
169
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
170
 
171
+ # Optional: external keep-alive via UptimeRobot
172
+ # Use the Main API key from UptimeRobot -> Integrations.
173
+ # Do not use the Read-only API key or a Monitor-specific API key.
174
+ # Run setup-uptimerobot.sh once from your own terminal to create the monitor.
175
+ # UPTIMEROBOT_API_KEY=ur_your_api_key_here
176
+
177
  # Trusted proxies (comma-separated IPs)
178
  # Fixes "Proxy headers detected from untrusted address" behind reverse proxies
179
  # Only set if you see pairing/auth errors. Find IPs in Space logs (remote=x.x.x.x)
README.md CHANGED
@@ -127,6 +127,28 @@ For persistent chat history and configuration, HuggingClaw can sync your workspa
127
  > [!TIP]
128
  > This backup also stores a hidden copy of your WhatsApp session credentials, allowing paired logins to survive Space restarts automatically.
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  ## 🔔 Webhooks *(Optional)*
131
 
132
  Get notified when your Space restarts or if a backup fails:
 
127
  > [!TIP]
128
  > This backup also stores a hidden copy of your WhatsApp session credentials, allowing paired logins to survive Space restarts automatically.
129
 
130
+ ## ⏰ External Keep-Alive *(Recommended on Free HF Spaces)*
131
+
132
+ Free Hugging Face Spaces can still sleep. To keep your Space awake, set up an external UptimeRobot monitor from the dashboard.
133
+
134
+ Use the **Main API key** from UptimeRobot.
135
+ Do **not** use the `Read-only API key` or a `Monitor-specific API key`.
136
+
137
+ Setup:
138
+
139
+ 1. Open `/dashboard`.
140
+ 2. Find **Keep Space Awake**.
141
+ 3. Paste your UptimeRobot **Main API key**.
142
+ 4. Click **Create Monitor**.
143
+
144
+ What happens next:
145
+
146
+ - HuggingClaw creates a monitor for `https://your-space.hf.space/health`
147
+ - UptimeRobot keeps pinging it from outside Hugging Face
148
+ - You only need to do this once
149
+
150
+ You do **not** need to add this key to Hugging Face Space Secrets.
151
+
152
  ## 🔔 Webhooks *(Optional)*
153
 
154
  Get notified when your Space restarts or if a backup fails:
health-server.js CHANGED
@@ -1,5 +1,6 @@
1
  // Single public entrypoint for HF Spaces: local dashboard + reverse proxy to OpenClaw.
2
  const http = require("http");
 
3
  const fs = require("fs");
4
  const net = require("net");
5
 
@@ -25,7 +26,12 @@ function isDashboardRoute(pathname) {
25
  }
26
 
27
  function isLocalRoute(pathname) {
28
- return pathname === "/health" || pathname === "/status" || isDashboardRoute(pathname);
 
 
 
 
 
29
  }
30
 
31
  function appendForwarded(existingValue, nextValue) {
@@ -256,6 +262,124 @@ function renderDashboard() {
256
  }
257
 
258
  #sync-msg { color: var(--text); display: block; margin-top: 4px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  </style>
260
  </head>
261
  <body>
@@ -294,6 +418,42 @@ function renderDashboard() {
294
  </div>
295
  </div>
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  <div class="footer">
298
  Live updates every 10s
299
  </div>
@@ -343,14 +503,200 @@ function renderDashboard() {
343
  }
344
  }
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  updateStats();
347
  setInterval(updateStats, 10000);
 
 
 
348
  </script>
349
  </body>
350
  </html>
351
  `;
352
  }
353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  function proxyHttp(req, res) {
355
  const proxyReq = http.request(
356
  {
@@ -484,6 +830,47 @@ const server = http.createServer((req, res) => {
484
  return;
485
  }
486
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  if (isDashboardRoute(pathname)) {
488
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
489
  res.end(renderDashboard());
 
1
  // Single public entrypoint for HF Spaces: local dashboard + reverse proxy to OpenClaw.
2
  const http = require("http");
3
+ const https = require("https");
4
  const fs = require("fs");
5
  const net = require("net");
6
 
 
26
  }
27
 
28
  function isLocalRoute(pathname) {
29
+ return (
30
+ pathname === "/health" ||
31
+ pathname === "/status" ||
32
+ pathname === "/uptimerobot/setup" ||
33
+ isDashboardRoute(pathname)
34
+ );
35
  }
36
 
37
  function appendForwarded(existingValue, nextValue) {
 
262
  }
263
 
264
  #sync-msg { color: var(--text); display: block; margin-top: 4px; }
265
+
266
+ .helper-card {
267
+ width: 100%;
268
+ margin-top: 20px;
269
+ }
270
+
271
+ .helper-copy {
272
+ color: var(--text-dim);
273
+ font-size: 0.92rem;
274
+ line-height: 1.6;
275
+ margin-top: 10px;
276
+ }
277
+
278
+ .helper-copy strong {
279
+ color: var(--text);
280
+ }
281
+
282
+ .helper-row {
283
+ display: flex;
284
+ gap: 10px;
285
+ margin-top: 16px;
286
+ flex-wrap: wrap;
287
+ }
288
+
289
+ .helper-input {
290
+ flex: 1;
291
+ min-width: 240px;
292
+ background: rgba(255, 255, 255, 0.04);
293
+ border: 1px solid rgba(255, 255, 255, 0.08);
294
+ color: var(--text);
295
+ border-radius: 12px;
296
+ padding: 14px 16px;
297
+ font: inherit;
298
+ }
299
+
300
+ .helper-input::placeholder {
301
+ color: var(--text-dim);
302
+ }
303
+
304
+ .helper-button {
305
+ background: var(--accent);
306
+ color: #fff;
307
+ border: 0;
308
+ border-radius: 12px;
309
+ padding: 14px 18px;
310
+ font: inherit;
311
+ font-weight: 600;
312
+ cursor: pointer;
313
+ min-width: 180px;
314
+ }
315
+
316
+ .helper-button:disabled {
317
+ opacity: 0.6;
318
+ cursor: wait;
319
+ }
320
+
321
+ .helper-note {
322
+ margin-top: 10px;
323
+ font-size: 0.82rem;
324
+ color: var(--text-dim);
325
+ }
326
+
327
+ .helper-result {
328
+ margin-top: 14px;
329
+ padding: 12px 14px;
330
+ border-radius: 12px;
331
+ font-size: 0.9rem;
332
+ display: none;
333
+ }
334
+
335
+ .helper-result.ok {
336
+ display: block;
337
+ background: rgba(16, 185, 129, 0.1);
338
+ color: var(--success);
339
+ }
340
+
341
+ .helper-result.error {
342
+ display: block;
343
+ background: rgba(239, 68, 68, 0.1);
344
+ color: var(--error);
345
+ }
346
+
347
+ .helper-shell {
348
+ margin-top: 12px;
349
+ }
350
+
351
+ .helper-shell.hidden {
352
+ display: none;
353
+ }
354
+
355
+ .helper-summary {
356
+ margin-top: 14px;
357
+ padding: 12px 14px;
358
+ border-radius: 12px;
359
+ background: rgba(255, 255, 255, 0.03);
360
+ color: var(--text-dim);
361
+ font-size: 0.9rem;
362
+ line-height: 1.5;
363
+ }
364
+
365
+ .helper-summary strong {
366
+ color: var(--text);
367
+ }
368
+
369
+ .helper-toggle {
370
+ margin-top: 14px;
371
+ display: inline-flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ background: rgba(255, 255, 255, 0.04);
375
+ color: var(--text);
376
+ border: 1px solid rgba(255, 255, 255, 0.08);
377
+ border-radius: 12px;
378
+ padding: 12px 16px;
379
+ font: inherit;
380
+ font-weight: 600;
381
+ cursor: pointer;
382
+ }
383
  </style>
384
  </head>
385
  <body>
 
418
  </div>
419
  </div>
420
 
421
+ <div class="stat-card helper-card">
422
+ <span class="stat-label">Keep Space Awake</span>
423
+ <div class="helper-copy">
424
+ If you use a free Hugging Face Space, it can still sleep.
425
+ To keep it awake, create an external UptimeRobot monitor here.
426
+ Use your <strong>Main API key</strong>.
427
+ </div>
428
+ <div class="helper-copy">
429
+ Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
430
+ </div>
431
+ <div id="uptimerobot-summary" class="helper-summary">
432
+ Optional one-time setup. If you already created the monitor before, you do not need to paste the key again.
433
+ </div>
434
+ <button id="uptimerobot-toggle" class="helper-toggle" type="button">
435
+ Set Up Monitor
436
+ </button>
437
+ <div id="uptimerobot-shell" class="helper-shell hidden">
438
+ <div class="helper-row">
439
+ <input
440
+ id="uptimerobot-key"
441
+ class="helper-input"
442
+ type="password"
443
+ placeholder="Paste your UptimeRobot Main API key"
444
+ autocomplete="off"
445
+ />
446
+ <button id="uptimerobot-btn" class="helper-button" type="button">
447
+ Create Monitor
448
+ </button>
449
+ </div>
450
+ <div class="helper-note">
451
+ One-time setup. Your key is only used to create the monitor for this Space.
452
+ </div>
453
+ </div>
454
+ <div id="uptimerobot-result" class="helper-result"></div>
455
+ </div>
456
+
457
  <div class="footer">
458
  Live updates every 10s
459
  </div>
 
503
  }
504
  }
505
 
506
+ const monitorStateKey = 'huggingclaw_uptimerobot_setup_v1';
507
+
508
+ function setMonitorUiState(isConfigured) {
509
+ const summary = document.getElementById('uptimerobot-summary');
510
+ const shell = document.getElementById('uptimerobot-shell');
511
+ const toggle = document.getElementById('uptimerobot-toggle');
512
+
513
+ if (isConfigured) {
514
+ summary.innerHTML = '<strong>Already set up.</strong> Your monitor should keep running from UptimeRobot even after this Space restarts.';
515
+ shell.classList.add('hidden');
516
+ toggle.textContent = 'Set Up Again';
517
+ } else {
518
+ summary.innerHTML = 'Optional one-time setup. If you already created the monitor before, you do not need to paste the key again.';
519
+ toggle.textContent = 'Set Up Monitor';
520
+ }
521
+ }
522
+
523
+ function restoreMonitorUiState() {
524
+ try {
525
+ const value = window.localStorage.getItem(monitorStateKey);
526
+ setMonitorUiState(value === 'done');
527
+ } catch {
528
+ setMonitorUiState(false);
529
+ }
530
+ }
531
+
532
+ function toggleMonitorSetup() {
533
+ const shell = document.getElementById('uptimerobot-shell');
534
+ shell.classList.toggle('hidden');
535
+ }
536
+
537
+ async function setupUptimeRobot() {
538
+ const input = document.getElementById('uptimerobot-key');
539
+ const button = document.getElementById('uptimerobot-btn');
540
+ const result = document.getElementById('uptimerobot-result');
541
+ const apiKey = input.value.trim();
542
+
543
+ if (!apiKey) {
544
+ result.className = 'helper-result error';
545
+ result.textContent = 'Paste your UptimeRobot Main API key first.';
546
+ return;
547
+ }
548
+
549
+ button.disabled = true;
550
+ button.textContent = 'Creating...';
551
+ result.className = 'helper-result';
552
+ result.textContent = '';
553
+
554
+ try {
555
+ const res = await fetch('/uptimerobot/setup', {
556
+ method: 'POST',
557
+ headers: { 'Content-Type': 'application/json' },
558
+ body: JSON.stringify({ apiKey })
559
+ });
560
+ const data = await res.json();
561
+
562
+ if (!res.ok) {
563
+ throw new Error(data.message || 'Failed to create monitor.');
564
+ }
565
+
566
+ result.className = 'helper-result ok';
567
+ result.textContent = data.message || 'UptimeRobot monitor is ready.';
568
+ input.value = '';
569
+ try {
570
+ window.localStorage.setItem(monitorStateKey, 'done');
571
+ } catch {}
572
+ setMonitorUiState(true);
573
+ document.getElementById('uptimerobot-shell').classList.add('hidden');
574
+ } catch (error) {
575
+ result.className = 'helper-result error';
576
+ result.textContent = error.message || 'Failed to create monitor.';
577
+ } finally {
578
+ button.disabled = false;
579
+ button.textContent = 'Create Monitor';
580
+ }
581
+ }
582
+
583
  updateStats();
584
  setInterval(updateStats, 10000);
585
+ restoreMonitorUiState();
586
+ document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
587
+ document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
588
  </script>
589
  </body>
590
  </html>
591
  `;
592
  }
593
 
594
+ function readRequestBody(req) {
595
+ return new Promise((resolve, reject) => {
596
+ let body = "";
597
+
598
+ req.on("data", (chunk) => {
599
+ body += chunk;
600
+ if (body.length > 1024 * 64) {
601
+ reject(new Error("Request too large"));
602
+ req.destroy();
603
+ }
604
+ });
605
+
606
+ req.on("end", () => resolve(body));
607
+ req.on("error", reject);
608
+ });
609
+ }
610
+
611
+ function postUptimeRobot(path, form) {
612
+ const body = new URLSearchParams(form).toString();
613
+
614
+ return new Promise((resolve, reject) => {
615
+ const request = https.request(
616
+ {
617
+ hostname: "api.uptimerobot.com",
618
+ port: 443,
619
+ method: "POST",
620
+ path,
621
+ headers: {
622
+ "Content-Type": "application/x-www-form-urlencoded",
623
+ "Content-Length": Buffer.byteLength(body),
624
+ },
625
+ },
626
+ (response) => {
627
+ let raw = "";
628
+ response.setEncoding("utf8");
629
+ response.on("data", (chunk) => {
630
+ raw += chunk;
631
+ });
632
+ response.on("end", () => {
633
+ try {
634
+ resolve(JSON.parse(raw));
635
+ } catch {
636
+ reject(new Error("Unexpected response from UptimeRobot"));
637
+ }
638
+ });
639
+ },
640
+ );
641
+
642
+ request.on("error", reject);
643
+ request.write(body);
644
+ request.end();
645
+ });
646
+ }
647
+
648
+ async function createUptimeRobotMonitor(apiKey, host) {
649
+ const cleanHost = String(host || "")
650
+ .replace(/^https?:\/\//, "")
651
+ .replace(/\/.*$/, "");
652
+
653
+ if (!cleanHost) {
654
+ throw new Error("Missing Space host.");
655
+ }
656
+
657
+ const monitorUrl = `https://${cleanHost}/health`;
658
+ const existing = await postUptimeRobot("/v2/getMonitors", {
659
+ api_key: apiKey,
660
+ format: "json",
661
+ logs: "0",
662
+ response_times: "0",
663
+ response_times_limit: "1",
664
+ });
665
+
666
+ const existingMonitor = Array.isArray(existing.monitors)
667
+ ? existing.monitors.find((monitor) => monitor.url === monitorUrl)
668
+ : null;
669
+
670
+ if (existingMonitor) {
671
+ return {
672
+ created: false,
673
+ message: `Monitor already exists for ${monitorUrl}`,
674
+ };
675
+ }
676
+
677
+ const created = await postUptimeRobot("/v2/newMonitor", {
678
+ api_key: apiKey,
679
+ format: "json",
680
+ type: "1",
681
+ friendly_name: `HuggingClaw ${cleanHost}`,
682
+ url: monitorUrl,
683
+ interval: "5",
684
+ });
685
+
686
+ if (created.stat !== "ok") {
687
+ const message =
688
+ created?.error?.message ||
689
+ created?.message ||
690
+ "Failed to create UptimeRobot monitor.";
691
+ throw new Error(message);
692
+ }
693
+
694
+ return {
695
+ created: true,
696
+ message: `Monitor created for ${monitorUrl}`,
697
+ };
698
+ }
699
+
700
  function proxyHttp(req, res) {
701
  const proxyReq = http.request(
702
  {
 
830
  return;
831
  }
832
 
833
+ if (pathname === "/uptimerobot/setup") {
834
+ if (req.method !== "POST") {
835
+ res.writeHead(405, { "Content-Type": "application/json" });
836
+ res.end(JSON.stringify({ message: "Method not allowed" }));
837
+ return;
838
+ }
839
+
840
+ void (async () => {
841
+ try {
842
+ const body = await readRequestBody(req);
843
+ const parsed = JSON.parse(body || "{}");
844
+ const apiKey = String(parsed.apiKey || "").trim();
845
+
846
+ if (!apiKey) {
847
+ res.writeHead(400, { "Content-Type": "application/json" });
848
+ res.end(
849
+ JSON.stringify({
850
+ message: "Paste your UptimeRobot Main API key first.",
851
+ }),
852
+ );
853
+ return;
854
+ }
855
+
856
+ const result = await createUptimeRobotMonitor(apiKey, req.headers.host);
857
+ res.writeHead(200, { "Content-Type": "application/json" });
858
+ res.end(JSON.stringify(result));
859
+ } catch (error) {
860
+ res.writeHead(400, { "Content-Type": "application/json" });
861
+ res.end(
862
+ JSON.stringify({
863
+ message:
864
+ error && error.message
865
+ ? error.message
866
+ : "Failed to create UptimeRobot monitor.",
867
+ }),
868
+ );
869
+ }
870
+ })();
871
+ return;
872
+ }
873
+
874
  if (isDashboardRoute(pathname)) {
875
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
876
  res.end(renderDashboard());
setup-uptimerobot.sh ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Create or update a UptimeRobot monitor for this Hugging Face Space.
5
+ #
6
+ # Requirements:
7
+ # - UPTIMEROBOT_API_KEY: Main API key from UptimeRobot
8
+ # - SPACE_HOST or first CLI arg: your HF Space host, e.g. "user-space.hf.space"
9
+ #
10
+ # Optional:
11
+ # - UPTIMEROBOT_MONITOR_NAME: friendly name for the monitor
12
+ # - UPTIMEROBOT_ALERT_CONTACTS: dash-separated alert contact IDs, e.g. "123456-789012"
13
+ # - UPTIMEROBOT_INTERVAL: monitoring interval in minutes (subject to account limits)
14
+
15
+ API_URL="https://api.uptimerobot.com/v2"
16
+ API_KEY="${UPTIMEROBOT_API_KEY:-}"
17
+ SPACE_HOST_INPUT="${1:-${SPACE_HOST:-}}"
18
+
19
+ if [ -z "$API_KEY" ]; then
20
+ echo "Missing UPTIMEROBOT_API_KEY."
21
+ echo "Use the Main API key from UptimeRobot -> Integrations."
22
+ echo "Do not use the Read-only API key or a Monitor-specific API key."
23
+ exit 1
24
+ fi
25
+
26
+ if [ -z "$SPACE_HOST_INPUT" ]; then
27
+ echo "Missing Space host."
28
+ echo "Usage: UPTIMEROBOT_API_KEY=... ./setup-uptimerobot.sh your-space.hf.space"
29
+ exit 1
30
+ fi
31
+
32
+ SPACE_HOST_CLEAN="${SPACE_HOST_INPUT#https://}"
33
+ SPACE_HOST_CLEAN="${SPACE_HOST_CLEAN#http://}"
34
+ SPACE_HOST_CLEAN="${SPACE_HOST_CLEAN%%/*}"
35
+
36
+ MONITOR_URL="https://${SPACE_HOST_CLEAN}/health"
37
+ MONITOR_NAME="${UPTIMEROBOT_MONITOR_NAME:-HuggingClaw ${SPACE_HOST_CLEAN}}"
38
+ INTERVAL="${UPTIMEROBOT_INTERVAL:-5}"
39
+
40
+ echo "Checking existing UptimeRobot monitors for ${MONITOR_URL}..."
41
+ MONITORS_RESPONSE=$(curl -sS -X POST "${API_URL}/getMonitors" \
42
+ -d "api_key=${API_KEY}" \
43
+ -d "format=json" \
44
+ -d "logs=0" \
45
+ -d "response_times=0" \
46
+ -d "response_times_limit=1")
47
+
48
+ MONITOR_ID=$(printf '%s' "$MONITORS_RESPONSE" | jq -r --arg url "$MONITOR_URL" '
49
+ (.monitors // []) | map(select(.url == $url)) | first | .id // empty
50
+ ')
51
+
52
+ if [ -n "$MONITOR_ID" ]; then
53
+ echo "Monitor already exists (id=${MONITOR_ID}) for ${MONITOR_URL}"
54
+ exit 0
55
+ fi
56
+
57
+ echo "Creating new UptimeRobot monitor for ${MONITOR_URL}..."
58
+
59
+ CURL_ARGS=(
60
+ -sS
61
+ -X POST "${API_URL}/newMonitor"
62
+ -d "api_key=${API_KEY}"
63
+ -d "format=json"
64
+ -d "type=1"
65
+ -d "friendly_name=${MONITOR_NAME}"
66
+ -d "url=${MONITOR_URL}"
67
+ -d "interval=${INTERVAL}"
68
+ )
69
+
70
+ if [ -n "${UPTIMEROBOT_ALERT_CONTACTS:-}" ]; then
71
+ CURL_ARGS+=(-d "alert_contacts=${UPTIMEROBOT_ALERT_CONTACTS}")
72
+ fi
73
+
74
+ CREATE_RESPONSE=$(curl "${CURL_ARGS[@]}")
75
+ CREATE_STATUS=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.stat // "fail"')
76
+
77
+ if [ "$CREATE_STATUS" != "ok" ]; then
78
+ echo "Failed to create monitor."
79
+ printf '%s\n' "$CREATE_RESPONSE"
80
+ exit 1
81
+ fi
82
+
83
+ NEW_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.monitor.id // empty')
84
+ echo "Created UptimeRobot monitor ${NEW_ID:-"(id unavailable)"} for ${MONITOR_URL}"