Spaces:
Running
Running
refactor: move UptimeRobot configuration from client-side UI to server-side environment secret
Browse files- .env.example +9 -1
- README.md +6 -18
- cloudflare-proxy-setup.py +31 -4
- cloudflare-proxy.js +25 -5
- health-server.js +32 -322
- setup-uptimerobot.sh +10 -3
- start.sh +6 -0
.env.example
CHANGED
|
@@ -73,6 +73,11 @@ OPENCODE_ALLOW_ALL_MODELS=true
|
|
| 73 |
# Get it from: https://dash.cloudflare.com/profile/api-tokens
|
| 74 |
# CLOUDFLARE_WORKERS_TOKEN=xxx
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
# ============================================================================
|
| 77 |
# Database Backup Configuration
|
| 78 |
# ============================================================================
|
|
@@ -104,8 +109,11 @@ BETTER_AUTH_SECRET=your-random-secret-here-minimum-32-characters
|
|
| 104 |
# Monitoring & Uptime
|
| 105 |
# ============================================================================
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# Optional: Webhook URL for restart/failure alerts
|
| 108 |
-
# Useful for UptimeRobot or similar monitoring services
|
| 109 |
# WEBHOOK_URL=https://uptime-robot-webhook-url
|
| 110 |
|
| 111 |
# ============================================================================
|
|
|
|
| 73 |
# Get it from: https://dash.cloudflare.com/profile/api-tokens
|
| 74 |
# CLOUDFLARE_WORKERS_TOKEN=xxx
|
| 75 |
|
| 76 |
+
# Extra domains to proxy, merged with built-in defaults (Telegram, Discord, WhatsApp,
|
| 77 |
+
# Facebook, Google). Comma-separated. Set to "*" to proxy ALL external traffic.
|
| 78 |
+
# Leave unset to proxy only the built-in default domains.
|
| 79 |
+
# CLOUDFLARE_PROXY_DOMAINS=api.sendgrid.com,slack.com
|
| 80 |
+
|
| 81 |
# ============================================================================
|
| 82 |
# Database Backup Configuration
|
| 83 |
# ============================================================================
|
|
|
|
| 109 |
# Monitoring & Uptime
|
| 110 |
# ============================================================================
|
| 111 |
|
| 112 |
+
# UptimeRobot keep-alive: add your Main API key (NOT Read-only or Monitor-specific).
|
| 113 |
+
# Monitor is created automatically at boot. Status shown on the dashboard.
|
| 114 |
+
# UPTIMEROBOT_API_KEY=ur_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 115 |
+
|
| 116 |
# Optional: Webhook URL for restart/failure alerts
|
|
|
|
| 117 |
# WEBHOOK_URL=https://uptime-robot-webhook-url
|
| 118 |
|
| 119 |
# ============================================================================
|
README.md
CHANGED
|
@@ -16,6 +16,8 @@ secrets:
|
|
| 16 |
description: Google Gemini API key for Gemini-powered agents.
|
| 17 |
- name: OPENAI_API_KEY
|
| 18 |
description: OpenAI API key for GPT-powered agents.
|
|
|
|
|
|
|
| 19 |
---
|
| 20 |
|
| 21 |
<!-- Badges -->
|
|
@@ -48,7 +50,7 @@ secrets:
|
|
| 48 |
- β‘ **One-click deploy:** Duplicate the Space and add your API key β nothing else needed to get started.
|
| 49 |
- πΎ **Persistent Database:** PostgreSQL database auto-backed up to a private HF Dataset and restored on every restart β no data loss.
|
| 50 |
- π **Visual Dashboard:** Real-time status dashboard at `/` with Paperclip service health, backup status, and uptime.
|
| 51 |
-
- β° **Keep-Alive:**
|
| 52 |
- π **Cloudflare Proxy:** Auto-provisions a Cloudflare Worker proxy for blocked outbound connections.
|
| 53 |
- π **Secure by Default:** Auth secrets randomly generated on first boot and persisted across restarts.
|
| 54 |
- π **100% HF-Native:** Runs entirely on Hugging Face's free infrastructure.
|
|
@@ -141,7 +143,7 @@ HuggingClip will:
|
|
| 141 |
| :--- | :--- | :--- |
|
| 142 |
| `CLOUDFLARE_WORKERS_TOKEN` | β | Cloudflare API token |
|
| 143 |
| `CLOUDFLARE_ACCOUNT_ID` | auto | Optional account ID override |
|
| 144 |
-
| `CLOUDFLARE_PROXY_DOMAINS` |
|
| 145 |
|
| 146 |
## πΎ Database Backup *(Optional)*
|
| 147 |
|
|
@@ -160,21 +162,7 @@ HuggingClip automatically backs up your Paperclip PostgreSQL database to a priva
|
|
| 160 |
|
| 161 |
## π Staying Alive *(Recommended on Free HF Spaces)*
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
Use the **Main API key** from UptimeRobot β not the Read-only key or a Monitor-specific key.
|
| 166 |
-
|
| 167 |
-
**Setup:**
|
| 168 |
-
|
| 169 |
-
1. Open the dashboard at `/`.
|
| 170 |
-
2. Find **Keep Space Awake**.
|
| 171 |
-
3. Paste your UptimeRobot **Main API key**.
|
| 172 |
-
4. Click **Create Monitor**.
|
| 173 |
-
|
| 174 |
-
HuggingClip creates a monitor for `https://your-space.hf.space/health`. UptimeRobot pings it from outside HF every 5 minutes.
|
| 175 |
-
|
| 176 |
-
> [!NOTE]
|
| 177 |
-
> This works for **public** Spaces only. Private Spaces cannot be reached by external monitors.
|
| 178 |
|
| 179 |
## π» Local Development
|
| 180 |
|
|
@@ -253,7 +241,7 @@ Verify `HF_TOKEN` is set and has write access. Check the dashboard backup status
|
|
| 253 |
`HF_TOKEN` is not set. Add it and the next restart will restore from backup. The backup also needs to have been run at least once before the restart.
|
| 254 |
|
| 255 |
**Space keeps sleeping**
|
| 256 |
-
|
| 257 |
|
| 258 |
**Paperclip unreachable (502 errors)**
|
| 259 |
Wait 60β90s after boot for Paperclip to initialize. If it stays unreachable, check logs for PostgreSQL connection errors or memory issues.
|
|
|
|
| 16 |
description: Google Gemini API key for Gemini-powered agents.
|
| 17 |
- name: OPENAI_API_KEY
|
| 18 |
description: OpenAI API key for GPT-powered agents.
|
| 19 |
+
- name: UPTIMEROBOT_API_KEY
|
| 20 |
+
description: UptimeRobot API key for automatic monitor setup.
|
| 21 |
---
|
| 22 |
|
| 23 |
<!-- Badges -->
|
|
|
|
| 50 |
- β‘ **One-click deploy:** Duplicate the Space and add your API key β nothing else needed to get started.
|
| 51 |
- πΎ **Persistent Database:** PostgreSQL database auto-backed up to a private HF Dataset and restored on every restart β no data loss.
|
| 52 |
- π **Visual Dashboard:** Real-time status dashboard at `/` with Paperclip service health, backup status, and uptime.
|
| 53 |
+
- β° **Keep-Alive:** Add `UPTIMEROBOT_API_KEY` as a Space secret and the monitor is created automatically at boot β no manual setup.
|
| 54 |
- π **Cloudflare Proxy:** Auto-provisions a Cloudflare Worker proxy for blocked outbound connections.
|
| 55 |
- π **Secure by Default:** Auth secrets randomly generated on first boot and persisted across restarts.
|
| 56 |
- π **100% HF-Native:** Runs entirely on Hugging Face's free infrastructure.
|
|
|
|
| 143 |
| :--- | :--- | :--- |
|
| 144 |
| `CLOUDFLARE_WORKERS_TOKEN` | β | Cloudflare API token |
|
| 145 |
| `CLOUDFLARE_ACCOUNT_ID` | auto | Optional account ID override |
|
| 146 |
+
| `CLOUDFLARE_PROXY_DOMAINS` | β | Extra domains to proxy, merged with built-in defaults. Set to `*` to proxy all external traffic. |
|
| 147 |
|
| 148 |
## πΎ Database Backup *(Optional)*
|
| 149 |
|
|
|
|
| 162 |
|
| 163 |
## π Staying Alive *(Recommended on Free HF Spaces)*
|
| 164 |
|
| 165 |
+
Add your [UptimeRobot](https://uptimerobot.com/) **Main API key** (not the Read-only or Monitor-specific key) as a Space secret named `UPTIMEROBOT_API_KEY`. HuggingClip will automatically create a monitor for `https://your-space.hf.space/health` at boot. The dashboard shows the current status (configured, setting up, or failed).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
## π» Local Development
|
| 168 |
|
|
|
|
| 241 |
`HF_TOKEN` is not set. Add it and the next restart will restore from backup. The backup also needs to have been run at least once before the restart.
|
| 242 |
|
| 243 |
**Space keeps sleeping**
|
| 244 |
+
Add `UPTIMEROBOT_API_KEY` as a Space secret to enable automatic keep-awake monitoring.
|
| 245 |
|
| 246 |
**Paperclip unreachable (502 errors)**
|
| 247 |
Wait 60β90s after boot for Paperclip to initialize. If it stays unreachable, check logs for PostgreSQL connection errors or memory issues.
|
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 Paperclip.
|
| 2 |
const http = require("http");
|
| 3 |
-
const https = require("https");
|
| 4 |
const fs = require("fs");
|
| 5 |
const net = require("net");
|
| 6 |
|
|
@@ -12,13 +11,8 @@ const startTime = Date.now();
|
|
| 12 |
const HF_BACKUP_ENABLED = !!process.env.HF_TOKEN;
|
| 13 |
const SYNC_INTERVAL = process.env.SYNC_INTERVAL || "86400";
|
| 14 |
|
| 15 |
-
const
|
| 16 |
-
|
| 17 |
-
const UPTIMEROBOT_RATE_WINDOW_MS = 60 * 1000;
|
| 18 |
-
const UPTIMEROBOT_RATE_MAX = Number(process.env.UPTIMEROBOT_RATE_LIMIT_PER_MINUTE || 5);
|
| 19 |
-
const SPACE_VISIBILITY_TTL_MS = 10 * 60 * 1000;
|
| 20 |
-
const spaceVisibilityCache = new Map();
|
| 21 |
-
const uptimerobotRateMap = new Map();
|
| 22 |
|
| 23 |
// ============================================================================
|
| 24 |
// URL helpers
|
|
@@ -33,169 +27,20 @@ function parseRequestUrl(url) {
|
|
| 33 |
}
|
| 34 |
|
| 35 |
function isLocalRoute(pathname) {
|
| 36 |
-
return
|
| 37 |
-
pathname === "/health" ||
|
| 38 |
-
pathname === "/status" ||
|
| 39 |
-
pathname === "/uptimerobot/setup"
|
| 40 |
-
);
|
| 41 |
}
|
| 42 |
|
| 43 |
// ============================================================================
|
| 44 |
// UptimeRobot helpers
|
| 45 |
// ============================================================================
|
| 46 |
|
| 47 |
-
function
|
| 48 |
-
const forwarded = req.headers["x-forwarded-for"];
|
| 49 |
-
if (typeof forwarded === "string") return forwarded.split(",")[0].trim();
|
| 50 |
-
if (Array.isArray(forwarded) && forwarded.length > 0) return String(forwarded[0]).split(",")[0].trim();
|
| 51 |
-
return req.socket.remoteAddress || "unknown";
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
function isRateLimited(req) {
|
| 55 |
-
const now = Date.now();
|
| 56 |
-
const ip = getRequesterIp(req);
|
| 57 |
-
const bucket = uptimerobotRateMap.get(ip) || [];
|
| 58 |
-
const recent = bucket.filter((ts) => now - ts < UPTIMEROBOT_RATE_WINDOW_MS);
|
| 59 |
-
recent.push(now);
|
| 60 |
-
uptimerobotRateMap.set(ip, recent);
|
| 61 |
-
return recent.length > UPTIMEROBOT_RATE_MAX;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
setInterval(() => {
|
| 65 |
-
const cutoff = Date.now() - UPTIMEROBOT_RATE_WINDOW_MS;
|
| 66 |
-
for (const [ip, timestamps] of uptimerobotRateMap) {
|
| 67 |
-
if (timestamps.every((ts) => ts < cutoff)) uptimerobotRateMap.delete(ip);
|
| 68 |
-
}
|
| 69 |
-
}, 5 * 60 * 1000).unref();
|
| 70 |
-
|
| 71 |
-
function isAllowedUptimeSetupOrigin(req) {
|
| 72 |
-
const host = String(req.headers.host || "").toLowerCase();
|
| 73 |
-
const origin = String(req.headers.origin || "").toLowerCase();
|
| 74 |
-
const referer = String(req.headers.referer || "").toLowerCase();
|
| 75 |
-
if (!host) return false;
|
| 76 |
-
if (origin && !origin.includes(host)) return false;
|
| 77 |
-
if (referer && !referer.includes(host)) return false;
|
| 78 |
-
return true;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
function isValidUptimeApiKey(key) {
|
| 82 |
-
return /^[A-Za-z0-9_-]{20,128}$/.test(String(key || ""));
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
function decodeJwtPayload(token) {
|
| 86 |
-
try {
|
| 87 |
-
const parts = String(token || "").split(".");
|
| 88 |
-
if (parts.length < 2) return null;
|
| 89 |
-
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
| 90 |
-
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
| 91 |
-
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
| 92 |
-
} catch {
|
| 93 |
-
return null;
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
function getSpaceRef(parsedUrl) {
|
| 98 |
-
const signedToken = parsedUrl.searchParams.get("__sign");
|
| 99 |
-
if (!signedToken) return null;
|
| 100 |
-
const payload = decodeJwtPayload(signedToken);
|
| 101 |
-
const subject = payload && payload.sub;
|
| 102 |
-
const match =
|
| 103 |
-
typeof subject === "string"
|
| 104 |
-
? subject.match(/^\/spaces\/([^/]+)\/([^/]+)$/)
|
| 105 |
-
: null;
|
| 106 |
-
if (!match) return null;
|
| 107 |
-
return { owner: match[1], repo: match[2] };
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
function fetchStatusCode(url) {
|
| 111 |
-
return new Promise((resolve, reject) => {
|
| 112 |
-
const req = https.get(
|
| 113 |
-
url,
|
| 114 |
-
{ headers: { "user-agent": "HuggingClip/1.0", accept: "application/json" } },
|
| 115 |
-
(res) => { res.resume(); resolve(res.statusCode || 0); },
|
| 116 |
-
);
|
| 117 |
-
req.on("error", reject);
|
| 118 |
-
req.setTimeout(5000, () => req.destroy(new Error("timeout")));
|
| 119 |
-
});
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
async function resolveSpaceIsPrivate(parsedUrl) {
|
| 123 |
-
const ref = getSpaceRef(parsedUrl);
|
| 124 |
-
if (!ref) return false;
|
| 125 |
-
const cacheKey = `${ref.owner}/${ref.repo}`;
|
| 126 |
-
const cached = spaceVisibilityCache.get(cacheKey);
|
| 127 |
-
if (cached && Date.now() - cached.timestamp < SPACE_VISIBILITY_TTL_MS) return cached.isPrivate;
|
| 128 |
try {
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
if (cached) return cached.isPrivate;
|
| 135 |
-
return false;
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
function postUptimeRobot(path, form) {
|
| 140 |
-
const body = new URLSearchParams(form).toString();
|
| 141 |
-
return new Promise((resolve, reject) => {
|
| 142 |
-
const request = https.request(
|
| 143 |
-
{
|
| 144 |
-
hostname: "api.uptimerobot.com",
|
| 145 |
-
port: 443,
|
| 146 |
-
method: "POST",
|
| 147 |
-
path,
|
| 148 |
-
headers: {
|
| 149 |
-
"Content-Type": "application/x-www-form-urlencoded",
|
| 150 |
-
"Content-Length": Buffer.byteLength(body),
|
| 151 |
-
},
|
| 152 |
-
},
|
| 153 |
-
(response) => {
|
| 154 |
-
let raw = "";
|
| 155 |
-
response.setEncoding("utf8");
|
| 156 |
-
response.on("data", (chunk) => { raw += chunk; });
|
| 157 |
-
response.on("end", () => {
|
| 158 |
-
try { resolve(JSON.parse(raw)); }
|
| 159 |
-
catch { reject(new Error("Unexpected response from UptimeRobot")); }
|
| 160 |
-
});
|
| 161 |
-
},
|
| 162 |
-
);
|
| 163 |
-
request.on("error", reject);
|
| 164 |
-
request.write(body);
|
| 165 |
-
request.end();
|
| 166 |
-
});
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
async function createUptimeRobotMonitor(apiKey, host) {
|
| 170 |
-
const cleanHost = String(host || "").replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
| 171 |
-
if (!cleanHost) throw new Error("Missing Space host.");
|
| 172 |
-
|
| 173 |
-
const monitorUrl = `https://${cleanHost}/health`;
|
| 174 |
-
const existing = await postUptimeRobot("/v2/getMonitors", {
|
| 175 |
-
api_key: apiKey, format: "json", logs: "0",
|
| 176 |
-
response_times: "0", response_times_limit: "1",
|
| 177 |
-
});
|
| 178 |
-
|
| 179 |
-
const existingMonitor = Array.isArray(existing.monitors)
|
| 180 |
-
? existing.monitors.find((m) => m.url === monitorUrl)
|
| 181 |
-
: null;
|
| 182 |
-
|
| 183 |
-
if (existingMonitor) {
|
| 184 |
-
return { created: false, message: `Monitor already exists for ${monitorUrl}` };
|
| 185 |
-
}
|
| 186 |
-
|
| 187 |
-
const created = await postUptimeRobot("/v2/newMonitor", {
|
| 188 |
-
api_key: apiKey, format: "json", type: "1",
|
| 189 |
-
friendly_name: `HuggingClip ${cleanHost}`,
|
| 190 |
-
url: monitorUrl, interval: "300",
|
| 191 |
-
});
|
| 192 |
-
|
| 193 |
-
if (created.stat !== "ok") {
|
| 194 |
-
const message = created?.error?.message || created?.message || "Failed to create UptimeRobot monitor.";
|
| 195 |
-
throw new Error(message);
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
return { created: true, message: `Monitor created for ${monitorUrl}` };
|
| 199 |
}
|
| 200 |
|
| 201 |
// ============================================================================
|
|
@@ -255,27 +100,25 @@ function formatUptime(seconds) {
|
|
| 255 |
// ============================================================================
|
| 256 |
|
| 257 |
function renderDashboard(initialData) {
|
| 258 |
-
const
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
</div>
|
| 278 |
-
<div id="uptimerobot-result" class="helper-result"></div>`;
|
| 279 |
|
| 280 |
const syncStatus = initialData.sync;
|
| 281 |
const hasBackup = HF_BACKUP_ENABLED;
|
|
@@ -443,9 +286,12 @@ function renderDashboard(initialData) {
|
|
| 443 |
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
| 444 |
background: rgba(255,255,255,0.03); color: var(--text-dim);
|
| 445 |
font-size: 0.9rem; line-height: 1.5;
|
|
|
|
| 446 |
}
|
| 447 |
.helper-summary strong { color: var(--text); }
|
|
|
|
| 448 |
.helper-summary.success { background: rgba(16,185,129,0.08); }
|
|
|
|
| 449 |
.helper-toggle {
|
| 450 |
margin-top: 14px; display: inline-flex; align-items: center; justify-content: center;
|
| 451 |
background: rgba(255,255,255,0.04); color: var(--text);
|
|
@@ -549,10 +395,6 @@ function renderDashboard(initialData) {
|
|
| 549 |
</div>
|
| 550 |
|
| 551 |
<script>
|
| 552 |
-
const KEEP_AWAKE_PRIVATE = ${initialData.spacePrivate ? "true" : "false"};
|
| 553 |
-
const KEEP_AWAKE_SETUP_ENABLED = ${UPTIMEROBOT_SETUP_ENABLED ? "true" : "false"};
|
| 554 |
-
const monitorStateKey = 'huggingclip_uptimerobot_v1';
|
| 555 |
-
|
| 556 |
function getCurrentSearch() { return window.location.search || ''; }
|
| 557 |
|
| 558 |
function renderSyncBadge(status, lastSyncTime, lastError) {
|
|
@@ -599,98 +441,13 @@ function renderDashboard(initialData) {
|
|
| 599 |
}
|
| 600 |
}
|
| 601 |
|
| 602 |
-
function setMonitorUiState(isConfigured) {
|
| 603 |
-
const summary = document.getElementById('uptimerobot-summary');
|
| 604 |
-
const shell = document.getElementById('uptimerobot-shell');
|
| 605 |
-
const toggle = document.getElementById('uptimerobot-toggle');
|
| 606 |
-
if (!summary || !shell || !toggle) return;
|
| 607 |
-
if (isConfigured) {
|
| 608 |
-
summary.classList.add('success');
|
| 609 |
-
summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
|
| 610 |
-
shell.classList.add('hidden');
|
| 611 |
-
toggle.textContent = 'Set Up Again';
|
| 612 |
-
} else {
|
| 613 |
-
summary.classList.remove('success');
|
| 614 |
-
summary.innerHTML = 'One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.';
|
| 615 |
-
toggle.textContent = 'Set Up Monitor';
|
| 616 |
-
}
|
| 617 |
-
}
|
| 618 |
-
|
| 619 |
-
function restoreMonitorUiState() {
|
| 620 |
-
try { setMonitorUiState(window.localStorage.getItem(monitorStateKey) === 'done'); }
|
| 621 |
-
catch { setMonitorUiState(false); }
|
| 622 |
-
}
|
| 623 |
-
|
| 624 |
-
async function setupUptimeRobot() {
|
| 625 |
-
const input = document.getElementById('uptimerobot-key');
|
| 626 |
-
const button = document.getElementById('uptimerobot-btn');
|
| 627 |
-
const result = document.getElementById('uptimerobot-result');
|
| 628 |
-
const apiKey = input.value.trim();
|
| 629 |
-
if (!apiKey) {
|
| 630 |
-
result.className = 'helper-result error';
|
| 631 |
-
result.textContent = 'Paste your UptimeRobot Main API key first.';
|
| 632 |
-
return;
|
| 633 |
-
}
|
| 634 |
-
button.disabled = true;
|
| 635 |
-
button.textContent = 'Creating...';
|
| 636 |
-
result.className = 'helper-result';
|
| 637 |
-
result.textContent = '';
|
| 638 |
-
try {
|
| 639 |
-
const res = await fetch('/uptimerobot/setup' + getCurrentSearch(), {
|
| 640 |
-
method: 'POST',
|
| 641 |
-
headers: { 'Content-Type': 'application/json' },
|
| 642 |
-
body: JSON.stringify({ apiKey }),
|
| 643 |
-
});
|
| 644 |
-
const data = await res.json();
|
| 645 |
-
if (!res.ok) throw new Error(data.message || 'Failed to create monitor.');
|
| 646 |
-
result.className = 'helper-result ok';
|
| 647 |
-
result.textContent = data.message || 'UptimeRobot monitor is ready.';
|
| 648 |
-
input.value = '';
|
| 649 |
-
try { window.localStorage.setItem(monitorStateKey, 'done'); } catch {}
|
| 650 |
-
setMonitorUiState(true);
|
| 651 |
-
document.getElementById('uptimerobot-shell').classList.add('hidden');
|
| 652 |
-
} catch (error) {
|
| 653 |
-
result.className = 'helper-result error';
|
| 654 |
-
result.textContent = error.message || 'Failed to create monitor.';
|
| 655 |
-
} finally {
|
| 656 |
-
button.disabled = false;
|
| 657 |
-
button.textContent = 'Create Monitor';
|
| 658 |
-
}
|
| 659 |
-
}
|
| 660 |
-
|
| 661 |
updateStatus();
|
| 662 |
setInterval(updateStatus, 30000);
|
| 663 |
-
|
| 664 |
-
if (KEEP_AWAKE_SETUP_ENABLED && !KEEP_AWAKE_PRIVATE) {
|
| 665 |
-
restoreMonitorUiState();
|
| 666 |
-
const toggleBtn = document.getElementById('uptimerobot-toggle');
|
| 667 |
-
const createBtn = document.getElementById('uptimerobot-btn');
|
| 668 |
-
if (toggleBtn) toggleBtn.addEventListener('click', () => {
|
| 669 |
-
document.getElementById('uptimerobot-shell').classList.toggle('hidden');
|
| 670 |
-
});
|
| 671 |
-
if (createBtn) createBtn.addEventListener('click', setupUptimeRobot);
|
| 672 |
-
}
|
| 673 |
</script>
|
| 674 |
</body>
|
| 675 |
</html>`;
|
| 676 |
}
|
| 677 |
|
| 678 |
-
// ============================================================================
|
| 679 |
-
// Request body reader
|
| 680 |
-
// ============================================================================
|
| 681 |
-
|
| 682 |
-
function readRequestBody(req) {
|
| 683 |
-
return new Promise((resolve, reject) => {
|
| 684 |
-
let body = "";
|
| 685 |
-
req.on("data", (chunk) => {
|
| 686 |
-
body += chunk;
|
| 687 |
-
if (body.length > 64 * 1024) { reject(new Error("Request too large")); req.destroy(); }
|
| 688 |
-
});
|
| 689 |
-
req.on("end", () => resolve(body));
|
| 690 |
-
req.on("error", reject);
|
| 691 |
-
});
|
| 692 |
-
}
|
| 693 |
-
|
| 694 |
// ============================================================================
|
| 695 |
// HTTP Proxy helpers
|
| 696 |
// ============================================================================
|
|
@@ -803,61 +560,14 @@ const server = http.createServer((req, res) => {
|
|
| 803 |
return;
|
| 804 |
}
|
| 805 |
|
| 806 |
-
// ββ UptimeRobot setup endpoint βββββββββββββββββββββββββββββββββββββββββββββ
|
| 807 |
-
if (pathname === "/uptimerobot/setup") {
|
| 808 |
-
if (req.method !== "POST") {
|
| 809 |
-
res.writeHead(405, { "Content-Type": "application/json" });
|
| 810 |
-
res.end(JSON.stringify({ message: "Method not allowed" }));
|
| 811 |
-
return;
|
| 812 |
-
}
|
| 813 |
-
void (async () => {
|
| 814 |
-
try {
|
| 815 |
-
if (!UPTIMEROBOT_SETUP_ENABLED) {
|
| 816 |
-
res.writeHead(403, { "Content-Type": "application/json" });
|
| 817 |
-
res.end(JSON.stringify({ message: "Uptime setup is disabled." }));
|
| 818 |
-
return;
|
| 819 |
-
}
|
| 820 |
-
if (isRateLimited(req)) {
|
| 821 |
-
res.writeHead(429, { "Content-Type": "application/json" });
|
| 822 |
-
res.end(JSON.stringify({ message: "Too many requests." }));
|
| 823 |
-
return;
|
| 824 |
-
}
|
| 825 |
-
if (!isAllowedUptimeSetupOrigin(req)) {
|
| 826 |
-
res.writeHead(403, { "Content-Type": "application/json" });
|
| 827 |
-
res.end(JSON.stringify({ message: "Invalid request origin." }));
|
| 828 |
-
return;
|
| 829 |
-
}
|
| 830 |
-
const body = await readRequestBody(req);
|
| 831 |
-
const parsed = JSON.parse(body || "{}");
|
| 832 |
-
const apiKey = String(parsed.apiKey || "").trim();
|
| 833 |
-
if (!isValidUptimeApiKey(apiKey)) {
|
| 834 |
-
res.writeHead(400, { "Content-Type": "application/json" });
|
| 835 |
-
res.end(JSON.stringify({ message: "A valid API key is required." }));
|
| 836 |
-
return;
|
| 837 |
-
}
|
| 838 |
-
const result = await createUptimeRobotMonitor(apiKey, req.headers.host);
|
| 839 |
-
res.writeHead(200, { "Content-Type": "application/json" });
|
| 840 |
-
res.end(JSON.stringify(result));
|
| 841 |
-
} catch (error) {
|
| 842 |
-
res.writeHead(400, { "Content-Type": "application/json" });
|
| 843 |
-
res.end(JSON.stringify({ message: error?.message || "Failed to create UptimeRobot monitor." }));
|
| 844 |
-
}
|
| 845 |
-
})();
|
| 846 |
-
return;
|
| 847 |
-
}
|
| 848 |
-
|
| 849 |
// ββ Dashboard (root) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 850 |
if (pathname === "/" || pathname === "") {
|
| 851 |
void (async () => {
|
| 852 |
-
const
|
| 853 |
-
checkPaperclipHealth(),
|
| 854 |
-
resolveSpaceIsPrivate(parsedUrl),
|
| 855 |
-
]);
|
| 856 |
const initialData = {
|
| 857 |
paperclipRunning: paperclipStatus.status === "running",
|
| 858 |
sync: readSyncStatus(),
|
| 859 |
inviteUrl: readInviteUrl(),
|
| 860 |
-
spacePrivate,
|
| 861 |
};
|
| 862 |
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
| 863 |
res.end(renderDashboard(initialData));
|
|
|
|
| 1 |
// Single public entrypoint for HF Spaces: local dashboard + reverse proxy to Paperclip.
|
| 2 |
const http = require("http");
|
|
|
|
| 3 |
const fs = require("fs");
|
| 4 |
const net = require("net");
|
| 5 |
|
|
|
|
| 11 |
const HF_BACKUP_ENABLED = !!process.env.HF_TOKEN;
|
| 12 |
const SYNC_INTERVAL = process.env.SYNC_INTERVAL || "86400";
|
| 13 |
|
| 14 |
+
const UPTIMEROBOT_STATUS_FILE = "/tmp/huggingclip-uptimerobot-status.json";
|
| 15 |
+
const UPTIMEROBOT_API_KEY_SET = !!process.env.UPTIMEROBOT_API_KEY;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
// ============================================================================
|
| 18 |
// URL helpers
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
function isLocalRoute(pathname) {
|
| 30 |
+
return pathname === "/health" || pathname === "/status";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
// ============================================================================
|
| 34 |
// UptimeRobot helpers
|
| 35 |
// ============================================================================
|
| 36 |
|
| 37 |
+
function getUptimeRobotStatus() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
try {
|
| 39 |
+
if (fs.existsSync(UPTIMEROBOT_STATUS_FILE)) {
|
| 40 |
+
return JSON.parse(fs.readFileSync(UPTIMEROBOT_STATUS_FILE, "utf8"));
|
| 41 |
+
}
|
| 42 |
+
} catch {}
|
| 43 |
+
return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
// ============================================================================
|
|
|
|
| 100 |
// ============================================================================
|
| 101 |
|
| 102 |
function renderDashboard(initialData) {
|
| 103 |
+
const uptimerobotStatus = getUptimeRobotStatus();
|
| 104 |
+
let keepAwakeHtml;
|
| 105 |
+
if (uptimerobotStatus?.configured) {
|
| 106 |
+
keepAwakeHtml = `<div class="helper-summary success">
|
| 107 |
+
<span class="status-badge status-online"><div class="pulse"></div>Configured</span>
|
| 108 |
+
<span>UptimeRobot monitor active for <code>${uptimerobotStatus.url || "your /health endpoint"}</code>.</span>
|
| 109 |
+
</div>`;
|
| 110 |
+
} else if (uptimerobotStatus?.configured === false) {
|
| 111 |
+
keepAwakeHtml = `<div class="helper-summary error">
|
| 112 |
+
<span class="status-badge status-error">Failed</span>
|
| 113 |
+
<span>Monitor setup failed. Check Space logs.</span>
|
| 114 |
+
</div>`;
|
| 115 |
+
} else if (UPTIMEROBOT_API_KEY_SET) {
|
| 116 |
+
keepAwakeHtml = `<div class="helper-summary"><span class="status-badge status-syncing"><div class="pulse" style="background:#3b82f6"></div>Setting up</span> Setting up UptimeRobot monitor...</div>`;
|
| 117 |
+
} else {
|
| 118 |
+
keepAwakeHtml = `<div class="helper-summary">
|
| 119 |
+
<strong>Not configured.</strong> Add <code>UPTIMEROBOT_API_KEY</code> to Space secrets to enable keep-awake monitoring.
|
| 120 |
+
</div>`;
|
| 121 |
+
}
|
|
|
|
|
|
|
| 122 |
|
| 123 |
const syncStatus = initialData.sync;
|
| 124 |
const hasBackup = HF_BACKUP_ENABLED;
|
|
|
|
| 286 |
margin-top: 14px; padding: 12px 14px; border-radius: 12px;
|
| 287 |
background: rgba(255,255,255,0.03); color: var(--text-dim);
|
| 288 |
font-size: 0.9rem; line-height: 1.5;
|
| 289 |
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
| 290 |
}
|
| 291 |
.helper-summary strong { color: var(--text); }
|
| 292 |
+
.helper-summary code { background: rgba(255,255,255,0.07); padding: 1px 6px; border-radius: 4px; font-size: 0.85em; color: var(--text); }
|
| 293 |
.helper-summary.success { background: rgba(16,185,129,0.08); }
|
| 294 |
+
.helper-summary.error { background: rgba(239,68,68,0.08); }
|
| 295 |
.helper-toggle {
|
| 296 |
margin-top: 14px; display: inline-flex; align-items: center; justify-content: center;
|
| 297 |
background: rgba(255,255,255,0.04); color: var(--text);
|
|
|
|
| 395 |
</div>
|
| 396 |
|
| 397 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
function getCurrentSearch() { return window.location.search || ''; }
|
| 399 |
|
| 400 |
function renderSyncBadge(status, lastSyncTime, lastError) {
|
|
|
|
| 441 |
}
|
| 442 |
}
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
updateStatus();
|
| 445 |
setInterval(updateStatus, 30000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
</script>
|
| 447 |
</body>
|
| 448 |
</html>`;
|
| 449 |
}
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
// ============================================================================
|
| 452 |
// HTTP Proxy helpers
|
| 453 |
// ============================================================================
|
|
|
|
| 560 |
return;
|
| 561 |
}
|
| 562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
// ββ Dashboard (root) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 564 |
if (pathname === "/" || pathname === "") {
|
| 565 |
void (async () => {
|
| 566 |
+
const paperclipStatus = await checkPaperclipHealth();
|
|
|
|
|
|
|
|
|
|
| 567 |
const initialData = {
|
| 568 |
paperclipRunning: paperclipStatus.status === "running",
|
| 569 |
sync: readSyncStatus(),
|
| 570 |
inviteUrl: readInviteUrl(),
|
|
|
|
| 571 |
};
|
| 572 |
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
| 573 |
res.end(renderDashboard(initialData));
|
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."
|
|
@@ -34,8 +35,8 @@ 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:-
|
| 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/huggingclip-uptimerobot-status.json"
|
| 19 |
|
| 20 |
if [ -z "$API_KEY" ]; then
|
| 21 |
echo "Missing UPTIMEROBOT_API_KEY."
|
|
|
|
| 35 |
SPACE_HOST_CLEAN="${SPACE_HOST_CLEAN%%/*}"
|
| 36 |
|
| 37 |
MONITOR_URL="https://${SPACE_HOST_CLEAN}/health"
|
| 38 |
+
MONITOR_NAME="${UPTIMEROBOT_MONITOR_NAME:-HuggingClip ${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
|
@@ -208,6 +208,12 @@ fi
|
|
| 208 |
# ββ Health server βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 209 |
node /app/health-server.js &
|
| 210 |
HEALTH_PID=$!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
sleep 2
|
| 212 |
|
| 213 |
# ββ Paperclip instance config βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 208 |
# ββ Health server βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 209 |
node /app/health-server.js &
|
| 210 |
HEALTH_PID=$!
|
| 211 |
+
|
| 212 |
+
if [ -n "${UPTIMEROBOT_API_KEY:-}" ] && [ -n "${SPACE_HOST:-}" ]; then
|
| 213 |
+
echo "Setting up UptimeRobot monitor..."
|
| 214 |
+
bash /app/setup-uptimerobot.sh "${SPACE_HOST}" || true
|
| 215 |
+
fi
|
| 216 |
+
|
| 217 |
sleep 2
|
| 218 |
|
| 219 |
# ββ Paperclip instance config βββββββββββββββββββββββββββββββββββββββββββββββββ
|