somratpro commited on
Commit
78e2c48
Β·
1 Parent(s): 9528925

feat: add UptimeRobot monitor setup UI and backend integration to prevent Space sleep

Browse files
Files changed (9) hide show
  1. .env.example +6 -0
  2. CHANGELOG.md +19 -1
  3. CONTRIBUTING.md +1 -1
  4. Dockerfile +1 -2
  5. README.md +25 -16
  6. health-server.js +388 -1
  7. keep-alive.sh +0 -34
  8. setup-uptimerobot.sh +84 -0
  9. start.sh +2 -9
.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)
CHANGELOG.md CHANGED
@@ -2,9 +2,25 @@
2
 
3
  All notable changes to this project will be documented in this file.
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ## [1.1.0] - 2026-03-31
6
 
7
  ### Added
 
8
  - **Pre-built Docker image** β€” uses `ghcr.io/openclaw/openclaw:latest` multi-stage build for much faster builds (minutes instead of 30+)
9
  - **Python huggingface_hub sync** β€” `workspace-sync.py` uses the `huggingface_hub` library for more reliable HF Dataset sync (handles auth, LFS, retries). Falls back to git-based sync automatically
10
  - **Password auth** β€” `OPENCLAW_PASSWORD` for simpler login (optional alternative to token)
@@ -14,6 +30,7 @@ All notable changes to this project will be documented in this file.
14
  - **OpenCode Zen/Go** β€” support for OpenCode's tested model service
15
 
16
  ### Changed
 
17
  - Provider detection now uses `case` statement (cleaner, faster) with correct OpenClaw provider IDs
18
  - Model IDs now sourced from OpenClaw docs (not OpenRouter) for accuracy
19
  - Google API key env var corrected to `GEMINI_API_KEY`
@@ -23,6 +40,7 @@ All notable changes to this project will be documented in this file.
23
  ### πŸŽ‰ Initial Release
24
 
25
  #### Features
 
26
  - **Any LLM provider** β€” Anthropic (Claude), OpenAI (GPT-4), Google (Gemini)
27
  - **Telegram integration** β€” connect via @BotFather, supports multiple users
28
  - **Built-in keep-alive** β€” self-pings to prevent HF Spaces 48h sleep
@@ -36,8 +54,8 @@ All notable changes to this project will be documented in this file.
36
  - **Zero-config defaults** β€” just 2 secrets to get started
37
 
38
  #### Architecture
 
39
  - `start.sh` β€” config generator + validation + orchestrator
40
- - `keep-alive.sh` β€” self-ping background service
41
  - `workspace-sync.sh` β€” periodic workspace backup
42
  - `health-server.js` β€” lightweight health endpoint
43
  - `dns-fix.js` β€” DNS override for HF network restrictions
 
2
 
3
  All notable changes to this project will be documented in this file.
4
 
5
+ ## [1.2.0] - 2026-04-03
6
+
7
+ ### Added
8
+
9
+ - **Dashboard-based UptimeRobot setup** β€” users can now paste their UptimeRobot Main API key directly in the dashboard and create an external uptime monitor
10
+ - **Optional WhatsApp mode** β€” WhatsApp now stays fully disabled unless `WHATSAPP_ENABLED=true`
11
+
12
+ ### Changed
13
+
14
+ - **Documentation simplified** β€” README now explains the simple dashboard flow for external keep-alive, which key to use, and where to paste it
15
+
16
+ ### Removed
17
+
18
+ - **Internal self-ping keep-alive** β€” removed `keep-alive.sh` and all startup wiring because internal self-pings do not reliably prevent free-tier HF Space sleep
19
+
20
  ## [1.1.0] - 2026-03-31
21
 
22
  ### Added
23
+
24
  - **Pre-built Docker image** β€” uses `ghcr.io/openclaw/openclaw:latest` multi-stage build for much faster builds (minutes instead of 30+)
25
  - **Python huggingface_hub sync** β€” `workspace-sync.py` uses the `huggingface_hub` library for more reliable HF Dataset sync (handles auth, LFS, retries). Falls back to git-based sync automatically
26
  - **Password auth** β€” `OPENCLAW_PASSWORD` for simpler login (optional alternative to token)
 
30
  - **OpenCode Zen/Go** β€” support for OpenCode's tested model service
31
 
32
  ### Changed
33
+
34
  - Provider detection now uses `case` statement (cleaner, faster) with correct OpenClaw provider IDs
35
  - Model IDs now sourced from OpenClaw docs (not OpenRouter) for accuracy
36
  - Google API key env var corrected to `GEMINI_API_KEY`
 
40
  ### πŸŽ‰ Initial Release
41
 
42
  #### Features
43
+
44
  - **Any LLM provider** β€” Anthropic (Claude), OpenAI (GPT-4), Google (Gemini)
45
  - **Telegram integration** β€” connect via @BotFather, supports multiple users
46
  - **Built-in keep-alive** β€” self-pings to prevent HF Spaces 48h sleep
 
54
  - **Zero-config defaults** β€” just 2 secrets to get started
55
 
56
  #### Architecture
57
+
58
  - `start.sh` β€” config generator + validation + orchestrator
 
59
  - `workspace-sync.sh` β€” periodic workspace backup
60
  - `health-server.js` β€” lightweight health endpoint
61
  - `dns-fix.js` β€” DNS override for HF network restrictions
CONTRIBUTING.md CHANGED
@@ -30,7 +30,7 @@ Thanks for your interest in contributing! 🦞
30
  - Test with at least one LLM provider (Anthropic, OpenAI, or Google)
31
  - Test with and without Telegram enabled
32
  - Test with and without workspace backup enabled
33
- - Verify keep-alive and auto-sync work
34
 
35
  ## Development Setup
36
 
 
30
  - Test with at least one LLM provider (Anthropic, OpenAI, or Google)
31
  - Test with and without Telegram enabled
32
  - Test with and without workspace backup enabled
33
+ - Verify dashboard setup and auto-sync work
34
 
35
  ## Development Setup
36
 
Dockerfile CHANGED
@@ -39,10 +39,9 @@ COPY --chown=1000:1000 dns-fix.js /opt/dns-fix.js
39
  COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
40
  COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
41
  COPY --chown=1000:1000 start.sh /home/node/app/start.sh
42
- COPY --chown=1000:1000 keep-alive.sh /home/node/app/keep-alive.sh
43
  COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
44
  COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
45
- RUN chmod +x /home/node/app/start.sh /home/node/app/keep-alive.sh
46
 
47
  USER node
48
 
 
39
  COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
40
  COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
41
  COPY --chown=1000:1000 start.sh /home/node/app/start.sh
 
42
  COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
43
  COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
44
+ RUN chmod +x /home/node/app/start.sh
45
 
46
  USER node
47
 
README.md CHANGED
@@ -44,7 +44,7 @@ license: mit
44
  - ⚑ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) – no other setup needed.
45
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
46
  - πŸ’Ύ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
47
- - πŸ’“ **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
48
  - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
49
  - πŸ“Š **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
50
  - πŸ”” **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
@@ -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:
@@ -232,7 +254,6 @@ openclaw channels login --gateway https://YOUR_SPACE_NAME.hf.space
232
  HuggingClaw/
233
  β”œβ”€β”€ Dockerfile # Multi-stage build using pre-built OpenClaw image
234
  β”œβ”€β”€ start.sh # Config generator, validator, and orchestrator
235
- β”œβ”€β”€ keep-alive.sh # Self-ping to prevent HF Space sleep
236
  β”œβ”€β”€ workspace-sync.py # Syncs workspace to HF Datasets (with Git fallback)
237
  β”œβ”€β”€ health-server.js # /health endpoint for uptime checks
238
  β”œβ”€β”€ dns-fix.js # DNS-over-HTTPS fallback (for blocked domains)
@@ -246,29 +267,17 @@ HuggingClaw/
246
  4. Restore workspace from HF Dataset.
247
  5. Generate `openclaw.json` from environment variables.
248
  6. Print startup summary.
249
- 7. Launch background tasks (keep-alive, auto-sync).
250
  8. Launch the OpenClaw gateway (start listening).
251
  9. On `SIGTERM`, save workspace and exit cleanly.
252
  ```
253
 
254
- ## πŸ’“ Staying Alive
255
-
256
- HuggingClaw keeps the Space awake without external cron tools:
257
-
258
- - **Self-ping:** It periodically sends HTTP requests to its own URL (default every 5 minutes).
259
- - **Health endpoint:** `/health` returns `200 OK` and uptime info.
260
- - **No external deps:** Fully managed within HF Spaces (no outside pingers or servers).
261
-
262
- | Variable | Default | Description |
263
- | :--- | :--- | :--- |
264
- | `KEEP_ALIVE_INTERVAL` | `300` | Self-ping interval in seconds (0 to disable) |
265
-
266
  ## πŸ› Troubleshooting
267
 
268
  - **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings β†’ Secrets**.
269
  - **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `πŸ“± Enabling Telegram`.
270
  - **Backup restore failing:** Make sure `HF_USERNAME` and `HF_TOKEN` are correct (token needs write access to your Dataset).
271
- - **Space keeps sleeping:** Check logs for `Keep-alive` messages. Ensure `KEEP_ALIVE_INTERVAL` isn’t set to `0`.
272
  - **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
273
  - **Control UI says too many failed authentication attempts:** Wait for the retry window to expire, then open the Space in an incognito window or clear site storage for your Space before logging in again with `GATEWAY_TOKEN`.
274
  - **WhatsApp lost its session after restart:** Make sure `HF_USERNAME` and `HF_TOKEN` are configured so the hidden session backup can be restored on boot.
 
44
  - ⚑ **Zero Config:** Duplicate this Space and set **just three** secrets (LLM_API_KEY, LLM_MODEL, GATEWAY_TOKEN) – no other setup needed.
45
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
46
  - πŸ’Ύ **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
47
+ - ⏰ **External Keep-Alive:** Set up a one-time UptimeRobot monitor from the dashboard to help keep free HF Spaces awake.
48
  - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
49
  - πŸ“Š **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
50
  - πŸ”” **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
 
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
+ ## πŸ’“ Staying Alive *(Recommended on Free HF Spaces)*
131
+
132
+ Free Hugging Face Spaces can still sleep. HuggingClaw does not rely on internal self-pings anymore. To help 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:
 
254
  HuggingClaw/
255
  β”œβ”€β”€ Dockerfile # Multi-stage build using pre-built OpenClaw image
256
  β”œβ”€β”€ start.sh # Config generator, validator, and orchestrator
 
257
  β”œβ”€β”€ workspace-sync.py # Syncs workspace to HF Datasets (with Git fallback)
258
  β”œβ”€β”€ health-server.js # /health endpoint for uptime checks
259
  β”œβ”€β”€ dns-fix.js # DNS-over-HTTPS fallback (for blocked domains)
 
267
  4. Restore workspace from HF Dataset.
268
  5. Generate `openclaw.json` from environment variables.
269
  6. Print startup summary.
270
+ 7. Launch background tasks (auto-sync and optional channel helpers).
271
  8. Launch the OpenClaw gateway (start listening).
272
  9. On `SIGTERM`, save workspace and exit cleanly.
273
  ```
274
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  ## πŸ› Troubleshooting
276
 
277
  - **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings β†’ Secrets**.
278
  - **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `πŸ“± Enabling Telegram`.
279
  - **Backup restore failing:** Make sure `HF_USERNAME` and `HF_TOKEN` are correct (token needs write access to your Dataset).
280
+ - **Space keeps sleeping:** Open `/dashboard` and use `Keep Space Awake` to create the external monitor.
281
  - **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
282
  - **Control UI says too many failed authentication attempts:** Wait for the retry window to expire, then open the Space in an incognito window or clear site storage for your Space before logging in again with `GATEWAY_TOKEN`.
283
  - **WhatsApp lost its session after restart:** Make sure `HF_USERNAME` and `HF_TOKEN` are configured so the hidden session backup can be restored on boot.
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());
keep-alive.sh DELETED
@@ -1,34 +0,0 @@
1
- #!/bin/bash
2
- # Self-ping keep-alive for HF Spaces
3
- # HF Spaces sleeps after 48h of inactivity (no HTTP requests)
4
- # This script pings the Space's own URL to prevent that
5
- #
6
- # HF provides SPACE_HOST env var automatically (e.g., "username-spacename.hf.space")
7
- # Runs as a background process alongside the gateway
8
-
9
- INTERVAL="${KEEP_ALIVE_INTERVAL:-300}" # Default: every 5 minutes
10
-
11
- if [ "$INTERVAL" = "0" ]; then
12
- echo "⏸️ Keep-alive: disabled (KEEP_ALIVE_INTERVAL=0)"
13
- exit 0
14
- fi
15
-
16
- if [ -z "$SPACE_HOST" ]; then
17
- echo "⏸️ Keep-alive: SPACE_HOST not set (not on HF Spaces?), skipping."
18
- exit 0
19
- fi
20
-
21
- # Ping the health endpoint so we keep the Space warm without touching the gateway
22
- PING_URL="https://${SPACE_HOST}/health"
23
-
24
- echo "πŸ’“ Keep-alive started: pinging ${PING_URL} every ${INTERVAL}s"
25
-
26
- while true; do
27
- sleep "$INTERVAL"
28
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$PING_URL" 2>/dev/null)
29
- if [ "$HTTP_CODE" = "000" ]; then
30
- echo "πŸ’“ Keep-alive: ping failed (network error), retrying next cycle..."
31
- else
32
- echo "πŸ’“ Keep-alive: OK"
33
- fi
34
- done
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}"
start.sh CHANGED
@@ -331,11 +331,8 @@ else
331
  printf " β”‚ %-40s β”‚\n" "Auth: πŸ” token"
332
  fi
333
  if [ -n "$SPACE_HOST" ]; then
334
- printf " β”‚ %-40s β”‚\n" "Keep-alive: βœ… every ${KEEP_ALIVE_INTERVAL:-300}s"
335
  printf " β”‚ %-40s β”‚\n" "Control UI: https://${SPACE_HOST}"
336
  printf " β”‚ %-40s β”‚\n" "Dashboard: https://${SPACE_HOST}/dashboard"
337
- else
338
- printf " β”‚ %-40s β”‚\n" "Keep-alive: ⏸️ local mode"
339
  fi
340
  SYNC_STATUS="❌ disabled"
341
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
@@ -388,10 +385,6 @@ export LLM_MODEL="$LLM_MODEL"
388
  node /home/node/app/health-server.js &
389
  HEALTH_PID=$!
390
 
391
- # 11. Start HF keep-alive
392
- /home/node/app/keep-alive.sh &
393
- KEEP_ALIVE_PID=$!
394
-
395
  # ── Launch gateway ──
396
  echo "πŸš€ Launching OpenClaw gateway on port 7860..."
397
  echo ""
@@ -415,14 +408,14 @@ if ! kill -0 $GATEWAY_PID 2>/dev/null; then
415
  exit 1
416
  fi
417
 
418
- # 12. Start WhatsApp Guardian after the gateway is accepting connections
419
  if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
420
  node /home/node/app/wa-guardian.js &
421
  GUARDIAN_PID=$!
422
  echo "πŸ›‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
423
  fi
424
 
425
- # 13. Start Workspace Sync after startup settles
426
  python3 -u /home/node/app/workspace-sync.py &
427
 
428
  # Wait for gateway (allows trap to fire)
 
331
  printf " β”‚ %-40s β”‚\n" "Auth: πŸ” token"
332
  fi
333
  if [ -n "$SPACE_HOST" ]; then
 
334
  printf " β”‚ %-40s β”‚\n" "Control UI: https://${SPACE_HOST}"
335
  printf " β”‚ %-40s β”‚\n" "Dashboard: https://${SPACE_HOST}/dashboard"
 
 
336
  fi
337
  SYNC_STATUS="❌ disabled"
338
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
 
385
  node /home/node/app/health-server.js &
386
  HEALTH_PID=$!
387
 
 
 
 
 
388
  # ── Launch gateway ──
389
  echo "πŸš€ Launching OpenClaw gateway on port 7860..."
390
  echo ""
 
408
  exit 1
409
  fi
410
 
411
+ # 11. Start WhatsApp Guardian after the gateway is accepting connections
412
  if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
413
  node /home/node/app/wa-guardian.js &
414
  GUARDIAN_PID=$!
415
  echo "πŸ›‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
416
  fi
417
 
418
+ # 12. Start Workspace Sync after startup settles
419
  python3 -u /home/node/app/workspace-sync.py &
420
 
421
  # Wait for gateway (allows trap to fire)