somratpro commited on
Commit
93a1830
·
1 Parent(s): 1b1f2ba

refactor: update default allowed domains, unify domain filtering logic, and remove UptimeRobot setup UI from health server

Browse files
.env.example CHANGED
@@ -179,8 +179,10 @@ TELEGRAM_USER_ID=123456789
179
  # CLOUDFLARE_PROXY_URL=https://your-proxy.workers.dev
180
  # CLOUDFLARE_PROXY_SECRET=your_proxy_secret_here
181
 
182
- # Domains to proxy (default: *)
183
- # CLOUDFLARE_PROXY_DOMAINS=*
 
 
184
 
185
  # ── OPTIONAL: Workspace Backup to HF Dataset ──
186
  HF_USERNAME=your_hf_username
@@ -204,10 +206,8 @@ SYNC_INTERVAL=180
204
  # Webhooks: Standard POST notifications for lifecycle events
205
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
206
 
207
- # Optional: external keep-alive via UptimeRobot
208
- # Use the Main API key from UptimeRobot -> Integrations.
209
- # Do not use the Read-only API key or a Monitor-specific API key.
210
- # Run setup-uptimerobot.sh once from your own terminal to create the monitor.
211
  # UPTIMEROBOT_API_KEY=ur_your_api_key_here
212
 
213
  # Trusted proxies (comma-separated IPs)
 
179
  # CLOUDFLARE_PROXY_URL=https://your-proxy.workers.dev
180
  # CLOUDFLARE_PROXY_SECRET=your_proxy_secret_here
181
 
182
+ # Extra domains to proxy, merged with built-in defaults (Telegram, Discord, WhatsApp,
183
+ # Facebook, Google). Comma-separated. Set to "*" to proxy ALL external traffic.
184
+ # Leave unset to proxy only the built-in default domains.
185
+ # CLOUDFLARE_PROXY_DOMAINS=api.sendgrid.com,slack.com
186
 
187
  # ── OPTIONAL: Workspace Backup to HF Dataset ──
188
  HF_USERNAME=your_hf_username
 
206
  # Webhooks: Standard POST notifications for lifecycle events
207
  # WEBHOOK_URL=https://your-webhook-endpoint.com/log
208
 
209
+ # UptimeRobot keep-alive: add your Main API key (NOT Read-only or Monitor-specific).
210
+ # Monitor is created automatically at boot. Status shown on the dashboard.
 
 
211
  # UPTIMEROBOT_API_KEY=ur_your_api_key_here
212
 
213
  # Trusted proxies (comma-separated IPs)
README.md CHANGED
@@ -16,6 +16,8 @@ secrets:
16
  description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
17
  - name: CLOUDFLARE_WORKERS_TOKEN
18
  description: "Cloudflare API token — auto-creates a Worker proxy for Telegram, WhatsApp, and Google APIs."
 
 
19
  ---
20
 
21
  <!-- Badges -->
@@ -54,7 +56,7 @@ secrets:
54
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
55
  - 🌐 **Cloudflare Outbound Proxy:** HuggingClaw can automatically provision a Cloudflare Worker proxy for blocked outbound traffic such as Telegram API requests.
56
  - 💾 **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub`, preserving data automatically without storing your HF token in a git remote.
57
- - ⏰ **External Keep-Alive:** Set up a one-time UptimeRobot monitor from the dashboard to help keep free HF Spaces awake.
58
  - 👥 **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
59
  - 📊 **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
60
  - 🔔 **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
@@ -166,14 +168,7 @@ HuggingClaw automatically syncs your workspace (chats, settings, sessions) to a
166
 
167
  ## 💓 Staying Alive *(Recommended on Free HF Spaces)*
168
 
169
- To help keep your Space awake, set up an external [UptimeRobot](https://uptimerobot.com/) monitor directly from the dashboard UI.
170
-
171
- 1. Open your Space's dashboard (`/`).
172
- 2. Find the **Keep Space Awake** section.
173
- 3. Paste your UptimeRobot **Main API key**.
174
- 4. Click **Create Monitor**.
175
-
176
- HuggingClaw will automatically create a monitor for your Space's `/health` endpoint.
177
 
178
  ## 🔔 Webhooks *(Optional)*
179
 
@@ -313,7 +308,7 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
313
  - **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings → Secrets**.
314
  - **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `📱 Enabling Telegram`.
315
  - **Backup restore failing:** Make sure `HF_TOKEN` is valid and has write access to your HF account dataset. Set `HF_USERNAME` only if auto-detection is not available in your environment.
316
- - **Space keeps sleeping:** Open `/` and use `Keep Space Awake` to create the external monitor.
317
  - **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
318
  - **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`.
319
  - **WhatsApp lost its session after restart:** Make sure `HF_TOKEN` is configured so the hidden session backup can be restored on boot.
 
16
  description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
17
  - name: CLOUDFLARE_WORKERS_TOKEN
18
  description: "Cloudflare API token — auto-creates a Worker proxy for Telegram, WhatsApp, and Google APIs."
19
+ - name: UPTIMEROBOT_API_KEY
20
+ description: UptimeRobot API key for automatic monitor setup.
21
  ---
22
 
23
  <!-- Badges -->
 
56
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
57
  - 🌐 **Cloudflare Outbound Proxy:** HuggingClaw can automatically provision a Cloudflare Worker proxy for blocked outbound traffic such as Telegram API requests.
58
  - 💾 **Workspace Backup:** Chats, settings, and WhatsApp session state sync to a private HF Dataset via the `huggingface_hub`, preserving data automatically without storing your HF token in a git remote.
59
+ - ⏰ **External Keep-Alive:** Add `UPTIMEROBOT_API_KEY` as a Space secret and the monitor is created automatically at boot no manual setup.
60
  - 👥 **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
61
  - 📊 **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
62
  - 🔔 **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
 
168
 
169
  ## 💓 Staying Alive *(Recommended on Free HF Spaces)*
170
 
171
+ Add your [UptimeRobot](https://uptimerobot.com/) **Main API key** as a Space secret named `UPTIMEROBOT_API_KEY`. HuggingClaw will automatically create a monitor for your Space's `/health` endpoint at boot. The dashboard shows the current status (configured, setting up, or failed).
 
 
 
 
 
 
 
172
 
173
  ## 🔔 Webhooks *(Optional)*
174
 
 
308
  - **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings → Secrets**.
309
  - **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `📱 Enabling Telegram`.
310
  - **Backup restore failing:** Make sure `HF_TOKEN` is valid and has write access to your HF account dataset. Set `HF_USERNAME` only if auto-detection is not available in your environment.
311
+ - **Space keeps sleeping:** Add `UPTIMEROBOT_API_KEY` as a Space secret to enable automatic keep-awake monitoring.
312
  - **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
313
  - **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`.
314
  - **WhatsApp lost its session after restart:** Make sure `HF_TOKEN` is configured so the hidden session backup can be restored on boot.
cloudflare-proxy-setup.py CHANGED
@@ -12,13 +12,33 @@ from pathlib import Path
12
  API_BASE = "https://api.cloudflare.com/client/v4"
13
  ENV_FILE = Path("/tmp/huggingclaw-cloudflare-proxy.env")
14
  DEFAULT_ALLOWED = [
 
15
  "api.telegram.org",
16
  "discord.com",
17
  "discordapp.com",
18
  "gateway.discord.gg",
19
  "status.discord.com",
20
  "web.whatsapp.com",
 
21
  "graph.facebook.com",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  "googleapis.com",
23
  "google.com",
24
  "googleusercontent.com",
@@ -207,10 +227,17 @@ def main() -> int:
207
 
208
  worker_name = derive_worker_name()
209
  allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
210
- allow_proxy_all = not allowed_raw or allowed_raw == "*"
211
- allowed_targets = DEFAULT_ALLOWED if not allowed_raw or allow_proxy_all else [
212
- value.strip() for value in allowed_raw.split(",") if value.strip()
213
- ]
 
 
 
 
 
 
 
214
  proxy_secret = existing_secret or secrets.token_urlsafe(24)
215
  worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
216
 
 
12
  API_BASE = "https://api.cloudflare.com/client/v4"
13
  ENV_FILE = Path("/tmp/huggingclaw-cloudflare-proxy.env")
14
  DEFAULT_ALLOWED = [
15
+ # Messaging
16
  "api.telegram.org",
17
  "discord.com",
18
  "discordapp.com",
19
  "gateway.discord.gg",
20
  "status.discord.com",
21
  "web.whatsapp.com",
22
+ # Social — confirmed/likely blocked by HF firewall
23
  "graph.facebook.com",
24
+ "graph.instagram.com",
25
+ "api.twitter.com",
26
+ "api.x.com",
27
+ "upload.twitter.com",
28
+ "api.linkedin.com",
29
+ "www.linkedin.com",
30
+ "open.tiktokapis.com",
31
+ "oauth.reddit.com",
32
+ # Video
33
+ "youtube.com",
34
+ "www.youtube.com",
35
+ # AI APIs
36
+ "api.openai.com",
37
+ # Email HTTP APIs (SMTP ports are blocked; use these instead)
38
+ "api.resend.com",
39
+ "api.sendgrid.com",
40
+ "api.mailgun.net",
41
+ # Google
42
  "googleapis.com",
43
  "google.com",
44
  "googleusercontent.com",
 
227
 
228
  worker_name = derive_worker_name()
229
  allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
230
+ allow_proxy_all = allowed_raw == "*"
231
+ if allow_proxy_all:
232
+ allowed_targets = DEFAULT_ALLOWED
233
+ else:
234
+ extra = [v.strip() for v in allowed_raw.split(",") if v.strip()]
235
+ seen = set(DEFAULT_ALLOWED)
236
+ allowed_targets = list(DEFAULT_ALLOWED)
237
+ for domain in extra:
238
+ if domain not in seen:
239
+ allowed_targets.append(domain)
240
+ seen.add(domain)
241
  proxy_secret = existing_secret or secrets.token_urlsafe(24)
242
  worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
243
 
cloudflare-proxy.js CHANGED
@@ -23,11 +23,31 @@ if (
23
 
24
  const DEBUG = process.env.CLOUDFLARE_PROXY_DEBUG === "true";
25
  const PROXY_SHARED_SECRET = (process.env.CLOUDFLARE_PROXY_SECRET || "").trim();
26
- const PROXY_DOMAINS = process.env.CLOUDFLARE_PROXY_DOMAINS || "*";
27
- const BLOCKED_DOMAINS = PROXY_DOMAINS.split(",")
28
- .map((domain) => domain.trim())
29
- .filter(Boolean);
30
- const PROXY_ALL = PROXY_DOMAINS === "*";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  if (PROXY_URL) {
33
  try {
 
23
 
24
  const DEBUG = process.env.CLOUDFLARE_PROXY_DEBUG === "true";
25
  const PROXY_SHARED_SECRET = (process.env.CLOUDFLARE_PROXY_SECRET || "").trim();
26
+ const DEFAULT_PROXY_DOMAINS = [
27
+ "api.telegram.org", "discord.com", "discordapp.com",
28
+ "gateway.discord.gg", "status.discord.com", "web.whatsapp.com",
29
+ "graph.facebook.com", "graph.instagram.com",
30
+ "api.twitter.com", "api.x.com", "upload.twitter.com",
31
+ "api.linkedin.com", "www.linkedin.com",
32
+ "open.tiktokapis.com", "oauth.reddit.com",
33
+ "youtube.com", "www.youtube.com",
34
+ "api.openai.com",
35
+ "api.resend.com", "api.sendgrid.com", "api.mailgun.net",
36
+ "googleapis.com", "google.com", "googleusercontent.com", "gstatic.com",
37
+ ];
38
+ const PROXY_DOMAINS_RAW = (process.env.CLOUDFLARE_PROXY_DOMAINS || "").trim();
39
+ const PROXY_ALL = PROXY_DOMAINS_RAW === "*";
40
+ let BLOCKED_DOMAINS;
41
+ if (PROXY_ALL) {
42
+ BLOCKED_DOMAINS = [];
43
+ } else {
44
+ const extra = PROXY_DOMAINS_RAW.split(",").map((d) => d.trim()).filter(Boolean);
45
+ const seen = new Set(DEFAULT_PROXY_DOMAINS);
46
+ BLOCKED_DOMAINS = [...DEFAULT_PROXY_DOMAINS];
47
+ for (const d of extra) {
48
+ if (!seen.has(d)) { BLOCKED_DOMAINS.push(d); seen.add(d); }
49
+ }
50
+ }
51
 
52
  if (PROXY_URL) {
53
  try {
health-server.js CHANGED
@@ -1,6 +1,5 @@
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
 
@@ -17,19 +16,10 @@ const SYNC_INTERVAL = process.env.SYNC_INTERVAL || "180";
17
  const DASHBOARD_BASE = "/dashboard";
18
  const DASHBOARD_STATUS_PATH = `${DASHBOARD_BASE}/status`;
19
  const DASHBOARD_HEALTH_PATH = `${DASHBOARD_BASE}/health`;
20
- const DASHBOARD_UPTIMEROBOT_PATH = `${DASHBOARD_BASE}/uptimerobot/setup`;
21
  const DASHBOARD_APP_BASE = `${DASHBOARD_BASE}/app`;
22
  const APP_BASE = "/app";
23
- const UPTIMEROBOT_SETUP_ENABLED =
24
- String(process.env.UPTIMEROBOT_SETUP_ENABLED || "true").toLowerCase() ===
25
- "true";
26
- const UPTIMEROBOT_RATE_WINDOW_MS = 60 * 1000;
27
- const UPTIMEROBOT_RATE_MAX = Number(
28
- process.env.UPTIMEROBOT_RATE_LIMIT_PER_MINUTE || 5,
29
- );
30
- const SPACE_VISIBILITY_TTL_MS = 10 * 60 * 1000;
31
- const spaceVisibilityCache = new Map();
32
- const uptimerobotRateMap = new Map();
33
 
34
  function parseRequestUrl(url) {
35
  try {
@@ -62,10 +52,8 @@ function isLocalRoute(pathname) {
62
  return (
63
  pathname === "/health" ||
64
  pathname === "/status" ||
65
- pathname === "/uptimerobot/setup" ||
66
  pathname === DASHBOARD_HEALTH_PATH ||
67
- pathname === DASHBOARD_STATUS_PATH ||
68
- pathname === DASHBOARD_UPTIMEROBOT_PATH
69
  );
70
  }
71
 
@@ -125,38 +113,6 @@ function getRequesterIp(req) {
125
  );
126
  }
127
 
128
- function isRateLimited(req) {
129
- const now = Date.now();
130
- const ip = getRequesterIp(req);
131
- const bucket = uptimerobotRateMap.get(ip) || [];
132
- const recent = bucket.filter((ts) => now - ts < UPTIMEROBOT_RATE_WINDOW_MS);
133
- recent.push(now);
134
- uptimerobotRateMap.set(ip, recent);
135
- return recent.length > UPTIMEROBOT_RATE_MAX;
136
- }
137
-
138
- // Prune stale rate-limit buckets every 5 minutes to prevent unbounded growth.
139
- setInterval(() => {
140
- const cutoff = Date.now() - UPTIMEROBOT_RATE_WINDOW_MS;
141
- for (const [ip, timestamps] of uptimerobotRateMap) {
142
- if (timestamps.every((ts) => ts < cutoff)) uptimerobotRateMap.delete(ip);
143
- }
144
- }, 5 * 60 * 1000).unref();
145
-
146
- function isAllowedUptimeSetupOrigin(req) {
147
- const host = String(req.headers.host || "").toLowerCase();
148
- const origin = String(req.headers.origin || "").toLowerCase();
149
- const referer = String(req.headers.referer || "").toLowerCase();
150
- if (!host) return false;
151
- if (origin && !origin.includes(host)) return false;
152
- if (referer && !referer.includes(host)) return false;
153
- return true;
154
- }
155
-
156
- function isValidUptimeApiKey(key) {
157
- return /^[A-Za-z0-9_-]{20,128}$/.test(String(key || ""));
158
- }
159
-
160
  function readSyncStatus() {
161
  try {
162
  if (fs.existsSync("/tmp/sync-status.json")) {
@@ -196,76 +152,13 @@ function readGuardianStatus() {
196
  return { configured: true, connected: false, pairing: false };
197
  }
198
 
199
- function decodeJwtPayload(token) {
200
  try {
201
- const parts = String(token || "").split(".");
202
- if (parts.length < 2) return null;
203
- const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
204
- const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
205
- return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
206
- } catch {
207
- return null;
208
- }
209
- }
210
-
211
- function getSpaceRef(parsedUrl) {
212
- const signedToken = parsedUrl.searchParams.get("__sign");
213
- if (!signedToken) return null;
214
-
215
- const payload = decodeJwtPayload(signedToken);
216
- const subject = payload && payload.sub;
217
- const match =
218
- typeof subject === "string"
219
- ? subject.match(/^\/spaces\/([^/]+)\/([^/]+)$/)
220
- : null;
221
-
222
- if (!match) return null;
223
- return { owner: match[1], repo: match[2] };
224
- }
225
-
226
- function fetchStatusCode(url) {
227
- return new Promise((resolve, reject) => {
228
- const req = https.get(
229
- url,
230
- {
231
- headers: {
232
- "user-agent": "HuggingClaw/1.0",
233
- accept: "application/json",
234
- },
235
- },
236
- (res) => {
237
- res.resume();
238
- resolve(res.statusCode || 0);
239
- },
240
- );
241
- req.on("error", reject);
242
- req.setTimeout(5000, () => {
243
- req.destroy(new Error("Request timed out"));
244
- });
245
- });
246
- }
247
-
248
- async function resolveSpaceIsPrivate(parsedUrl) {
249
- const ref = getSpaceRef(parsedUrl);
250
- if (!ref) return false;
251
-
252
- const cacheKey = `${ref.owner}/${ref.repo}`;
253
- const cached = spaceVisibilityCache.get(cacheKey);
254
- if (cached && Date.now() - cached.timestamp < SPACE_VISIBILITY_TTL_MS) {
255
- return cached.isPrivate;
256
- }
257
-
258
- try {
259
- const statusCode = await fetchStatusCode(
260
- `https://huggingface.co/api/spaces/${ref.owner}/${ref.repo}`,
261
- );
262
- const isPrivate = statusCode === 401 || statusCode === 403 || statusCode === 404;
263
- spaceVisibilityCache.set(cacheKey, { isPrivate, timestamp: Date.now() });
264
- return isPrivate;
265
- } catch {
266
- if (cached) return cached.isPrivate;
267
- return false;
268
- }
269
  }
270
 
271
  function renderChannelBadge(channel, configuredLabel) {
@@ -295,48 +188,32 @@ function renderSyncBadge(syncData) {
295
 
296
  function renderDashboard(initialData) {
297
  const controlUiHref = `${APP_BASE}/`;
298
- const keepAwakeHtml = !UPTIMEROBOT_SETUP_ENABLED
299
- ? `
300
- <div id="uptimerobot-private-note" class="helper-summary">
301
- UptimeRobot setup is disabled for this Space.
302
- </div>
303
- `
304
- : initialData.spacePrivate
305
- ? `
306
- <div id="uptimerobot-private-note" class="helper-summary">
307
- <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.
308
- </div>
309
- `
310
- : `
311
- <div id="uptimerobot-public-flow">
312
- <div id="uptimerobot-summary" class="helper-summary">
313
- One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.
314
- </div>
315
- <button id="uptimerobot-toggle" class="helper-toggle" type="button">
316
- Set Up Monitor
317
- </button>
318
- <div id="uptimerobot-shell" class="helper-shell hidden">
319
- <div class="helper-copy">
320
- Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
321
- </div>
322
- <div class="helper-row">
323
- <input
324
- id="uptimerobot-key"
325
- class="helper-input"
326
- type="password"
327
- placeholder="Paste your UptimeRobot Main API key"
328
- autocomplete="off"
329
- />
330
- <button id="uptimerobot-btn" class="helper-button" type="button">
331
- Create Monitor
332
- </button>
333
- </div>
334
- <div class="helper-note">
335
- One-time setup. Your key is only used to create the monitor for this Space.
336
- </div>
337
- </div>
338
- </div>
339
- `;
340
  return `
341
  <!DOCTYPE html>
342
  <html lang="en">
@@ -627,30 +504,21 @@ function renderDashboard(initialData) {
627
  color: var(--text-dim);
628
  font-size: 0.9rem;
629
  line-height: 1.5;
 
 
 
 
630
  }
631
 
632
- .helper-summary strong {
 
 
 
 
 
633
  color: var(--text);
634
  }
635
-
636
- .helper-summary.success {
637
- background: rgba(16, 185, 129, 0.08);
638
- }
639
-
640
- .helper-toggle {
641
- margin-top: 14px;
642
- display: inline-flex;
643
- align-items: center;
644
- justify-content: center;
645
- background: rgba(255, 255, 255, 0.04);
646
- color: var(--text);
647
- border: 1px solid rgba(255, 255, 255, 0.08);
648
- border-radius: 12px;
649
- padding: 12px 16px;
650
- font: inherit;
651
- font-weight: 600;
652
- cursor: pointer;
653
- }
654
 
655
  @media (max-width: 700px) {
656
  body {
@@ -744,7 +612,6 @@ function renderDashboard(initialData) {
744
  <div class="stat-card helper-card">
745
  <span class="stat-label">Keep Space Awake</span>
746
  ${keepAwakeHtml}
747
- <div id="uptimerobot-result" class="helper-result"></div>
748
  </div>
749
 
750
  <div class="footer">
@@ -807,210 +674,15 @@ function renderDashboard(initialData) {
807
  }
808
  }
809
 
810
- const monitorStateKey = 'huggingclaw_uptimerobot_setup_v1';
811
- const KEEP_AWAKE_PRIVATE = ${initialData.spacePrivate ? "true" : "false"};
812
- const KEEP_AWAKE_SETUP_ENABLED = ${UPTIMEROBOT_SETUP_ENABLED ? "true" : "false"};
813
-
814
- function setMonitorUiState(isConfigured) {
815
- const summary = document.getElementById('uptimerobot-summary');
816
- const shell = document.getElementById('uptimerobot-shell');
817
- const toggle = document.getElementById('uptimerobot-toggle');
818
-
819
- if (!summary || !shell || !toggle) {
820
- return;
821
- }
822
-
823
- if (isConfigured) {
824
- summary.classList.add('success');
825
- summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
826
- shell.classList.add('hidden');
827
- toggle.textContent = 'Set Up Again';
828
- } else {
829
- summary.classList.remove('success');
830
- summary.innerHTML = 'One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.';
831
- toggle.textContent = 'Set Up Monitor';
832
- }
833
- }
834
-
835
- function restoreMonitorUiState() {
836
- try {
837
- const value = window.localStorage.getItem(monitorStateKey);
838
- setMonitorUiState(value === 'done');
839
- } catch {
840
- setMonitorUiState(false);
841
- }
842
- }
843
-
844
- function toggleMonitorSetup() {
845
- const shell = document.getElementById('uptimerobot-shell');
846
- shell.classList.toggle('hidden');
847
- }
848
-
849
- async function setupUptimeRobot() {
850
- const input = document.getElementById('uptimerobot-key');
851
- const button = document.getElementById('uptimerobot-btn');
852
- const result = document.getElementById('uptimerobot-result');
853
- const apiKey = input.value.trim();
854
-
855
- if (!apiKey) {
856
- result.className = 'helper-result error';
857
- result.textContent = 'Paste your UptimeRobot Main API key first.';
858
- return;
859
- }
860
-
861
- button.disabled = true;
862
- button.textContent = 'Creating...';
863
- result.className = 'helper-result';
864
- result.textContent = '';
865
-
866
- try {
867
- const res = await fetch(getDashboardBase() + '/uptimerobot/setup' + getCurrentSearch(), {
868
- method: 'POST',
869
- headers: { 'Content-Type': 'application/json' },
870
- body: JSON.stringify({ apiKey })
871
- });
872
- const data = await res.json();
873
-
874
- if (!res.ok) {
875
- throw new Error(data.message || 'Failed to create monitor.');
876
- }
877
-
878
- result.className = 'helper-result ok';
879
- result.textContent = data.message || 'UptimeRobot monitor is ready.';
880
- input.value = '';
881
- try {
882
- window.localStorage.setItem(monitorStateKey, 'done');
883
- } catch {}
884
- setMonitorUiState(true);
885
- document.getElementById('uptimerobot-shell').classList.add('hidden');
886
- } catch (error) {
887
- result.className = 'helper-result error';
888
- result.textContent = error.message || 'Failed to create monitor.';
889
- } finally {
890
- button.disabled = false;
891
- button.textContent = 'Create Monitor';
892
- }
893
- }
894
-
895
  updateStats();
896
  setInterval(updateStats, 10000);
897
  document.getElementById('control-ui-link').setAttribute('href', getDashboardBase() + '/app/' + getCurrentSearch());
898
- if (KEEP_AWAKE_SETUP_ENABLED && !KEEP_AWAKE_PRIVATE) {
899
- restoreMonitorUiState();
900
- document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
901
- document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
902
- }
903
  </script>
904
  </body>
905
  </html>
906
  `;
907
  }
908
 
909
- function readRequestBody(req) {
910
- return new Promise((resolve, reject) => {
911
- let body = "";
912
-
913
- req.on("data", (chunk) => {
914
- body += chunk;
915
- if (body.length > 1024 * 64) {
916
- reject(new Error("Request too large"));
917
- req.destroy();
918
- }
919
- });
920
-
921
- req.on("end", () => resolve(body));
922
- req.on("error", reject);
923
- });
924
- }
925
-
926
- function postUptimeRobot(path, form) {
927
- const body = new URLSearchParams(form).toString();
928
-
929
- return new Promise((resolve, reject) => {
930
- const request = https.request(
931
- {
932
- hostname: "api.uptimerobot.com",
933
- port: 443,
934
- method: "POST",
935
- path,
936
- headers: {
937
- "Content-Type": "application/x-www-form-urlencoded",
938
- "Content-Length": Buffer.byteLength(body),
939
- },
940
- },
941
- (response) => {
942
- let raw = "";
943
- response.setEncoding("utf8");
944
- response.on("data", (chunk) => {
945
- raw += chunk;
946
- });
947
- response.on("end", () => {
948
- try {
949
- resolve(JSON.parse(raw));
950
- } catch {
951
- reject(new Error("Unexpected response from UptimeRobot"));
952
- }
953
- });
954
- },
955
- );
956
-
957
- request.on("error", reject);
958
- request.write(body);
959
- request.end();
960
- });
961
- }
962
-
963
- async function createUptimeRobotMonitor(apiKey, host) {
964
- const cleanHost = String(host || "")
965
- .replace(/^https?:\/\//, "")
966
- .replace(/\/.*$/, "");
967
-
968
- if (!cleanHost) {
969
- throw new Error("Missing Space host.");
970
- }
971
-
972
- const monitorUrl = `https://${cleanHost}/health`;
973
- const existing = await postUptimeRobot("/v2/getMonitors", {
974
- api_key: apiKey,
975
- format: "json",
976
- logs: "0",
977
- response_times: "0",
978
- response_times_limit: "1",
979
- });
980
-
981
- const existingMonitor = Array.isArray(existing.monitors)
982
- ? existing.monitors.find((monitor) => monitor.url === monitorUrl)
983
- : null;
984
-
985
- if (existingMonitor) {
986
- return {
987
- created: false,
988
- message: `Monitor already exists for ${monitorUrl}`,
989
- };
990
- }
991
-
992
- const created = await postUptimeRobot("/v2/newMonitor", {
993
- api_key: apiKey,
994
- format: "json",
995
- type: "1",
996
- friendly_name: `HuggingClaw ${cleanHost}`,
997
- url: monitorUrl,
998
- interval: "300",
999
- });
1000
-
1001
- if (created.stat !== "ok") {
1002
- const message =
1003
- created?.error?.message ||
1004
- created?.message ||
1005
- "Failed to create UptimeRobot monitor.";
1006
- throw new Error(message);
1007
- }
1008
-
1009
- return {
1010
- created: true,
1011
- message: `Monitor created for ${monitorUrl}`,
1012
- };
1013
- }
1014
 
1015
  function proxyHttp(req, res, proxyPath = req.url, proxyPort = GATEWAY_PORT) {
1016
  const clientIp = getForwardedClientIp(req);
@@ -1168,86 +840,22 @@ const server = http.createServer((req, res) => {
1168
  return;
1169
  }
1170
 
1171
- if (
1172
- pathname === "/uptimerobot/setup" ||
1173
- pathname === DASHBOARD_UPTIMEROBOT_PATH
1174
- ) {
1175
- if (req.method !== "POST") {
1176
- res.writeHead(405, { "Content-Type": "application/json" });
1177
- res.end(JSON.stringify({ message: "Method not allowed" }));
1178
- return;
1179
- }
1180
-
1181
- void (async () => {
1182
- try {
1183
- if (!UPTIMEROBOT_SETUP_ENABLED) {
1184
- res.writeHead(403, { "Content-Type": "application/json" });
1185
- res.end(JSON.stringify({ message: "Uptime setup is disabled." }));
1186
- return;
1187
- }
1188
-
1189
- if (isRateLimited(req)) {
1190
- res.writeHead(429, { "Content-Type": "application/json" });
1191
- res.end(JSON.stringify({ message: "Too many requests." }));
1192
- return;
1193
- }
1194
-
1195
- if (!isAllowedUptimeSetupOrigin(req)) {
1196
- res.writeHead(403, { "Content-Type": "application/json" });
1197
- res.end(JSON.stringify({ message: "Invalid request origin." }));
1198
- return;
1199
- }
1200
-
1201
- const body = await readRequestBody(req);
1202
- const parsed = JSON.parse(body || "{}");
1203
- const apiKey = String(parsed.apiKey || "").trim();
1204
-
1205
- if (!isValidUptimeApiKey(apiKey)) {
1206
- res.writeHead(400, { "Content-Type": "application/json" });
1207
- res.end(
1208
- JSON.stringify({
1209
- message: "A valid API key is required.",
1210
- }),
1211
- );
1212
- return;
1213
- }
1214
-
1215
- const result = await createUptimeRobotMonitor(apiKey, req.headers.host);
1216
- res.writeHead(200, { "Content-Type": "application/json" });
1217
- res.end(JSON.stringify(result));
1218
- } catch (error) {
1219
- res.writeHead(400, { "Content-Type": "application/json" });
1220
- res.end(
1221
- JSON.stringify({
1222
- message:
1223
- error && error.message
1224
- ? error.message
1225
- : "Failed to create UptimeRobot monitor.",
1226
- }),
1227
- );
1228
- }
1229
- })();
1230
- return;
1231
- }
1232
-
1233
  if (isDashboardRoute(pathname)) {
1234
- void (async () => {
1235
- const guardianStatus = readGuardianStatus();
1236
- const initialData = {
1237
- model: LLM_MODEL,
1238
- whatsapp: {
1239
- configured: guardianStatus.configured,
1240
- connected: guardianStatus.connected,
1241
- pairing: guardianStatus.pairing,
1242
- },
1243
- telegram: normalizeChannelStatus(null, TELEGRAM_ENABLED),
1244
- sync: readSyncStatus(),
1245
- uptime: uptimeHuman,
1246
- spacePrivate: await resolveSpaceIsPrivate(parsedUrl),
1247
- };
1248
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1249
- res.end(renderDashboard(initialData));
1250
- })();
1251
  return;
1252
  }
1253
 
 
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
 
 
16
  const DASHBOARD_BASE = "/dashboard";
17
  const DASHBOARD_STATUS_PATH = `${DASHBOARD_BASE}/status`;
18
  const DASHBOARD_HEALTH_PATH = `${DASHBOARD_BASE}/health`;
 
19
  const DASHBOARD_APP_BASE = `${DASHBOARD_BASE}/app`;
20
  const APP_BASE = "/app";
21
+ const UPTIMEROBOT_STATUS_FILE = "/tmp/huggingclaw-uptimerobot-status.json";
22
+ const UPTIMEROBOT_API_KEY_SET = !!process.env.UPTIMEROBOT_API_KEY;
 
 
 
 
 
 
 
 
23
 
24
  function parseRequestUrl(url) {
25
  try {
 
52
  return (
53
  pathname === "/health" ||
54
  pathname === "/status" ||
 
55
  pathname === DASHBOARD_HEALTH_PATH ||
56
+ pathname === DASHBOARD_STATUS_PATH
 
57
  );
58
  }
59
 
 
113
  );
114
  }
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  function readSyncStatus() {
117
  try {
118
  if (fs.existsSync("/tmp/sync-status.json")) {
 
152
  return { configured: true, connected: false, pairing: false };
153
  }
154
 
155
+ function getUptimeRobotStatus() {
156
  try {
157
+ if (fs.existsSync(UPTIMEROBOT_STATUS_FILE)) {
158
+ return JSON.parse(fs.readFileSync(UPTIMEROBOT_STATUS_FILE, "utf8"));
159
+ }
160
+ } catch {}
161
+ return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
 
164
  function renderChannelBadge(channel, configuredLabel) {
 
188
 
189
  function renderDashboard(initialData) {
190
  const controlUiHref = `${APP_BASE}/`;
191
+ const urStatus = initialData.uptimerobotStatus;
192
+ let keepAwakeHtml;
193
+ if (urStatus?.configured) {
194
+ keepAwakeHtml = `
195
+ <div class="helper-summary success">
196
+ <div class="status-badge status-online"><div class="pulse"></div>CONFIGURED</div>
197
+ <span>UptimeRobot monitor active for <code>${urStatus.url || "your /health endpoint"}</code>.</span>
198
+ </div>`;
199
+ } else if (urStatus?.configured === false) {
200
+ keepAwakeHtml = `
201
+ <div class="helper-summary" style="background:rgba(239,68,68,0.08);">
202
+ <div class="status-badge status-offline">FAILED</div>
203
+ <span>Monitor setup failed. Check Space logs for details.</span>
204
+ </div>`;
205
+ } else if (UPTIMEROBOT_API_KEY_SET) {
206
+ keepAwakeHtml = `
207
+ <div class="helper-summary">
208
+ <div class="status-badge status-syncing">SETTING UP&hellip;</div>
209
+ <span>UptimeRobot monitor is being configured.</span>
210
+ </div>`;
211
+ } else {
212
+ keepAwakeHtml = `
213
+ <div class="helper-summary">
214
+ <strong>Not configured.</strong> Add <code>UPTIMEROBOT_API_KEY</code> to Space secrets to enable keep-awake monitoring.
215
+ </div>`;
216
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  return `
218
  <!DOCTYPE html>
219
  <html lang="en">
 
504
  color: var(--text-dim);
505
  font-size: 0.9rem;
506
  line-height: 1.5;
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 10px;
510
+ flex-wrap: wrap;
511
  }
512
 
513
+ .helper-summary strong { color: var(--text); }
514
+ .helper-summary code {
515
+ background: rgba(255,255,255,0.06);
516
+ padding: 2px 6px;
517
+ border-radius: 6px;
518
+ font-size: 0.82rem;
519
  color: var(--text);
520
  }
521
+ .helper-summary.success { background: rgba(16, 185, 129, 0.08); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
 
523
  @media (max-width: 700px) {
524
  body {
 
612
  <div class="stat-card helper-card">
613
  <span class="stat-label">Keep Space Awake</span>
614
  ${keepAwakeHtml}
 
615
  </div>
616
 
617
  <div class="footer">
 
674
  }
675
  }
676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
  updateStats();
678
  setInterval(updateStats, 10000);
679
  document.getElementById('control-ui-link').setAttribute('href', getDashboardBase() + '/app/' + getCurrentSearch());
 
 
 
 
 
680
  </script>
681
  </body>
682
  </html>
683
  `;
684
  }
685
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
 
687
  function proxyHttp(req, res, proxyPath = req.url, proxyPort = GATEWAY_PORT) {
688
  const clientIp = getForwardedClientIp(req);
 
840
  return;
841
  }
842
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  if (isDashboardRoute(pathname)) {
844
+ const guardianStatus = readGuardianStatus();
845
+ const initialData = {
846
+ model: LLM_MODEL,
847
+ whatsapp: {
848
+ configured: guardianStatus.configured,
849
+ connected: guardianStatus.connected,
850
+ pairing: guardianStatus.pairing,
851
+ },
852
+ telegram: normalizeChannelStatus(null, TELEGRAM_ENABLED),
853
+ sync: readSyncStatus(),
854
+ uptime: uptimeHuman,
855
+ uptimerobotStatus: getUptimeRobotStatus(),
856
+ };
857
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
858
+ res.end(renderDashboard(initialData));
 
 
859
  return;
860
  }
861
 
setup-uptimerobot.sh CHANGED
@@ -10,11 +10,12 @@ set -euo pipefail
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."
@@ -35,7 +36,7 @@ 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" \
@@ -50,6 +51,8 @@ MONITOR_ID=$(printf '%s' "$MONITORS_RESPONSE" | jq -r --arg url "$MONITOR_URL" '
50
  ')
51
 
52
  if [ -n "$MONITOR_ID" ]; then
 
 
53
  echo "Monitor already exists (id=${MONITOR_ID}) for ${MONITOR_URL}"
54
  exit 0
55
  fi
@@ -75,10 +78,14 @@ 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}"
 
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 seconds (default: 300 = 5 min; min: 30)
14
 
15
  API_URL="https://api.uptimerobot.com/v2"
16
  API_KEY="${UPTIMEROBOT_API_KEY:-}"
17
  SPACE_HOST_INPUT="${1:-${SPACE_HOST:-}}"
18
+ STATUS_FILE="/tmp/huggingclaw-uptimerobot-status.json"
19
 
20
  if [ -z "$API_KEY" ]; then
21
  echo "Missing UPTIMEROBOT_API_KEY."
 
36
 
37
  MONITOR_URL="https://${SPACE_HOST_CLEAN}/health"
38
  MONITOR_NAME="${UPTIMEROBOT_MONITOR_NAME:-HuggingClaw ${SPACE_HOST_CLEAN}}"
39
+ INTERVAL="${UPTIMEROBOT_INTERVAL:-300}"
40
 
41
  echo "Checking existing UptimeRobot monitors for ${MONITOR_URL}..."
42
  MONITORS_RESPONSE=$(curl -sS -X POST "${API_URL}/getMonitors" \
 
51
  ')
52
 
53
  if [ -n "$MONITOR_ID" ]; then
54
+ printf '{"configured":true,"monitorId":"%s","url":"%s","alreadyExisted":true,"timestamp":"%s"}\n' \
55
+ "$MONITOR_ID" "$MONITOR_URL" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS_FILE"
56
  echo "Monitor already exists (id=${MONITOR_ID}) for ${MONITOR_URL}"
57
  exit 0
58
  fi
 
78
  CREATE_STATUS=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.stat // "fail"')
79
 
80
  if [ "$CREATE_STATUS" != "ok" ]; then
81
+ printf '{"configured":false,"error":"creation failed","timestamp":"%s"}\n' \
82
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS_FILE"
83
  echo "Failed to create monitor."
84
  printf '%s\n' "$CREATE_RESPONSE"
85
  exit 1
86
  fi
87
 
88
  NEW_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.monitor.id // empty')
89
+ printf '{"configured":true,"monitorId":"%s","url":"%s","timestamp":"%s"}\n' \
90
+ "${NEW_ID:-}" "$MONITOR_URL" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS_FILE"
91
  echo "Created UptimeRobot monitor ${NEW_ID:-"(id unavailable)"} for ${MONITOR_URL}"
start.sh CHANGED
@@ -145,7 +145,6 @@ CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}
145
  export CLOUDFLARE_WORKERS_TOKEN
146
  CF_PROXY_ENV_FILE="/tmp/huggingclaw-cloudflare-proxy.env"
147
  if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
148
- export CLOUDFLARE_PROXY_DOMAINS="${CLOUDFLARE_PROXY_DOMAINS:-api.telegram.org,web.whatsapp.com,googleapis.com}"
149
  # Default debug off for production. Set CLOUDFLARE_PROXY_DEBUG=true in HF
150
  # Space secrets to surface per-request "Redirecting" + error-cause logs.
151
  export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"
@@ -498,6 +497,11 @@ export LLM_MODEL="$LLM_MODEL"
498
  node /home/node/app/health-server.js &
499
  HEALTH_PID=$!
500
 
 
 
 
 
 
501
  # ── Launch gateway ──
502
  echo "Launching OpenClaw gateway on port 7860..."
503
 
 
145
  export CLOUDFLARE_WORKERS_TOKEN
146
  CF_PROXY_ENV_FILE="/tmp/huggingclaw-cloudflare-proxy.env"
147
  if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
 
148
  # Default debug off for production. Set CLOUDFLARE_PROXY_DEBUG=true in HF
149
  # Space secrets to surface per-request "Redirecting" + error-cause logs.
150
  export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"
 
497
  node /home/node/app/health-server.js &
498
  HEALTH_PID=$!
499
 
500
+ if [ -n "${UPTIMEROBOT_API_KEY:-}" ] && [ -n "${SPACE_HOST:-}" ]; then
501
+ echo "Setting up UptimeRobot monitor..."
502
+ bash /home/node/app/setup-uptimerobot.sh "${SPACE_HOST}" || true
503
+ fi
504
+
505
  # ── Launch gateway ──
506
  echo "Launching OpenClaw gateway on port 7860..."
507