Spaces:
Running
Running
refactor: update default allowed domains, unify domain filtering logic, and remove UptimeRobot setup UI from health server
Browse files- .env.example +6 -6
- README.md +5 -10
- cloudflare-proxy-setup.py +31 -4
- cloudflare-proxy.js +25 -5
- health-server.js +61 -453
- setup-uptimerobot.sh +9 -2
- start.sh +5 -1
.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 |
-
#
|
| 183 |
-
#
|
|
|
|
|
|
|
| 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 |
-
#
|
| 208 |
-
#
|
| 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:**
|
| 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 |
-
|
| 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:**
|
| 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 =
|
| 211 |
-
|
| 212 |
-
|
| 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
|
| 27 |
-
|
| 28 |
-
.
|
| 29 |
-
.
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 24 |
-
|
| 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
|
| 200 |
try {
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 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
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
<
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
<
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
<div
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
<
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 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 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 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…</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
|
| 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:-
|
| 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 |
|