somratpro commited on
Commit
bb5c703
·
1 Parent(s): 38b85f9

refactor: migrate UptimeRobot configuration from client-side dynamic form to environment variable-based monitoring

Browse files
.env.example CHANGED
@@ -45,12 +45,14 @@ CLOUDFLARE_PROXY_URL=
45
  # If unset, proxy still works but without app-to-worker auth
46
  # Generate with: openssl rand -hex 24
47
  CLOUDFLARE_PROXY_SECRET=
48
- # Comma-separated list of domains to proxy. Use "*" to proxy everything.
49
- CLOUDFLARE_PROXY_DOMAINS=api.telegram.org,discord.com,discordapp.com
50
-
51
- # Dashboard helper hardening
52
- UPTIMEROBOT_SETUP_ENABLED=true
53
- UPTIMEROBOT_RATE_LIMIT_PER_MINUTE=5
 
 
54
 
55
  # Max seconds to wait for n8n readiness at startup
56
  N8N_STARTUP_TIMEOUT=180
 
45
  # If unset, proxy still works but without app-to-worker auth
46
  # Generate with: openssl rand -hex 24
47
  CLOUDFLARE_PROXY_SECRET=
48
+ # Extra domains to proxy, merged with built-in defaults (Telegram, Discord, WhatsApp,
49
+ # Facebook, Google). Comma-separated. Set to "*" to proxy ALL external traffic.
50
+ # Leave unset to proxy only the built-in default domains.
51
+ # CLOUDFLARE_PROXY_DOMAINS=api.sendgrid.com,smtp.sendgrid.net,slack.com
52
+
53
+ # UptimeRobot keep-alive: add your Main API key (NOT Read-only or Monitor-specific).
54
+ # Monitor is created automatically at boot. Status shown on the dashboard.
55
+ # UPTIMEROBOT_API_KEY=ur_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
56
 
57
  # Max seconds to wait for n8n readiness at startup
58
  N8N_STARTUP_TIMEOUT=180
README.md CHANGED
@@ -12,6 +12,8 @@ secrets:
12
  description: HuggingFace token with write access. Used for automatic workspace backup.
13
  - name: CLOUDFLARE_WORKERS_TOKEN
14
  description: Cloudflare API token for automatic Worker proxy setup.
 
 
15
  ---
16
 
17
  <!-- Badges -->
@@ -45,7 +47,7 @@ secrets:
45
  - 🔐 **Secure by Default:** Uses n8n's native user management and restricted file permissions (`umask 0077`).
46
  - 🌐 **Built-in Connectivity:** Includes transparent outbound proxying via Cloudflare Workers for Telegram, WhatsApp-related APIs, Google APIs, Discord, and other external services.
47
  - 📊 **Premium Dashboard:** Beautiful Web UI at `/` for real-time monitoring of uptime, sync health, and n8n status.
48
- - ⏰ **Easy Keep-Alive:** Set up a one-time UptimeRobot monitor directly from the dashboard to keep your free Space awake.
49
  - 🐳 **Optimized Infrastructure:** Minimal resource usage with clean startup logs and production-ready proxying.
50
 
51
  ## 🎥 Video Tutorial
@@ -124,14 +126,7 @@ Hugging8n automatically creates a private dataset named `hugging8n-backup` in yo
124
 
125
  ## 💓 Staying Alive *(Recommended on Free HF Spaces)*
126
 
127
- To help keep your Space awake, set up an external [UptimeRobot](https://uptimerobot.com/) monitor directly from the dashboard UI.
128
-
129
- 1. Open your Space's dashboard (`/`).
130
- 2. Find the **Keep Space Awake** section.
131
- 3. Paste your UptimeRobot **Main API key**.
132
- 4. Click **Create Monitor**.
133
-
134
- Hugging8n will automatically create a monitor for your Space's `/health` endpoint.
135
 
136
  ## 🔐 Security & Advanced *(Optional)*
137
 
@@ -142,14 +137,13 @@ Customize your instance with these environment variables:
142
  | `GENERIC_TIMEZONE` | `UTC` | Timezone for your n8n instance |
143
  | `N8N_LOG_LEVEL` | `error` | Set to `info` or `debug` for more details |
144
  | `CLOUDFLARE_WORKERS_TOKEN` | — | Cloudflare API token for automatic Worker setup |
145
- | `CLOUDFLARE_PROXY_DOMAINS` | `*` | Comma-separated domains to proxy (or `*` for all external traffic) |
146
  | `CLOUDFLARE_PROXY_SECRET` | — | Optional shared secret for proxy authentication |
147
  | `CLOUDFLARE_WORKER_NAME` | auto | Custom name for the automatically created Worker |
148
  | `CLOUDFLARE_ACCOUNT_ID` | auto | Optional Cloudflare account ID override |
149
  | `SPACE_HOST_OVERRIDE` | — | Override detected host for custom domains |
150
  | `N8N_STARTUP_TIMEOUT` | `180` | Max seconds to wait for n8n readiness |
151
- | `UPTIMEROBOT_SETUP_ENABLED` | `true` | Enable/disable dashboard helper endpoint |
152
- | `UPTIMEROBOT_RATE_LIMIT_PER_MINUTE` | `5` | Rate limit for monitor creation |
153
 
154
  ## 💻 Local Development
155
 
@@ -177,9 +171,9 @@ docker run -p 7861:7861 --env-file .env hugging8n
177
 
178
  ## 🐛 Troubleshooting
179
 
180
- - **Telegram/Google/WhatsApp not connecting:** Ensure `CLOUDFLARE_WORKERS_TOKEN` or `CLOUDFLARE_PROXY_URL` is set correctly, or keep `CLOUDFLARE_PROXY_DOMAINS=*`.
181
  - **Workflows not saving:** Check if `HF_TOKEN` has **Write** access to your account.
182
- - **Space keeps sleeping:** Use the dashboard to set up an UptimeRobot monitor.
183
  - **Authentication errors:** n8n v2 uses its own internal users; ensure you created the owner account on first run.
184
 
185
  ## 🌟 More Projects
@@ -202,9 +196,11 @@ Similar projects by [@somratpro](https://github.com/somratpro) — all free, one
202
  If Hugging8n saves you time, consider buying me a coffee to keep the projects alive!
203
 
204
  **USDT (TRC-20 / TRON network only)**
 
205
  ```
206
  TELx8TJz1W1h7n6SgpgGNNGZXpJCEUZrdB
207
  ```
 
208
  > [!WARNING]
209
  > Send **USDT on TRC-20 network only**. Sending other tokens or using a different network will result in permanent loss.
210
 
 
12
  description: HuggingFace token with write access. Used for automatic workspace backup.
13
  - name: CLOUDFLARE_WORKERS_TOKEN
14
  description: Cloudflare API token for automatic Worker proxy setup.
15
+ - name: UPTIMEROBOT_API_KEY
16
+ description: UptimeRobot API key for automatic monitor setup.
17
  ---
18
 
19
  <!-- Badges -->
 
47
  - 🔐 **Secure by Default:** Uses n8n's native user management and restricted file permissions (`umask 0077`).
48
  - 🌐 **Built-in Connectivity:** Includes transparent outbound proxying via Cloudflare Workers for Telegram, WhatsApp-related APIs, Google APIs, Discord, and other external services.
49
  - 📊 **Premium Dashboard:** Beautiful Web UI at `/` for real-time monitoring of uptime, sync health, and n8n status.
50
+ - ⏰ **Easy Keep-Alive:** Add `UPTIMEROBOT_API_KEY` as a Space secret and the monitor is created automatically at boot no manual setup.
51
  - 🐳 **Optimized Infrastructure:** Minimal resource usage with clean startup logs and production-ready proxying.
52
 
53
  ## 🎥 Video Tutorial
 
126
 
127
  ## 💓 Staying Alive *(Recommended on Free HF Spaces)*
128
 
129
+ Add your [UptimeRobot](https://uptimerobot.com/) **Main API key** as a Space secret named `UPTIMEROBOT_API_KEY`. Hugging8n will automatically create a monitor for your Space's `/health` endpoint at boot. The dashboard shows the current status (configured, setting up, or failed).
 
 
 
 
 
 
 
130
 
131
  ## 🔐 Security & Advanced *(Optional)*
132
 
 
137
  | `GENERIC_TIMEZONE` | `UTC` | Timezone for your n8n instance |
138
  | `N8N_LOG_LEVEL` | `error` | Set to `info` or `debug` for more details |
139
  | `CLOUDFLARE_WORKERS_TOKEN` | — | Cloudflare API token for automatic Worker setup |
140
+ | `CLOUDFLARE_PROXY_DOMAINS` | | Extra domains to proxy, merged with built-in defaults. Set to `*` to proxy all external traffic. Leave unset to use defaults only. |
141
  | `CLOUDFLARE_PROXY_SECRET` | — | Optional shared secret for proxy authentication |
142
  | `CLOUDFLARE_WORKER_NAME` | auto | Custom name for the automatically created Worker |
143
  | `CLOUDFLARE_ACCOUNT_ID` | auto | Optional Cloudflare account ID override |
144
  | `SPACE_HOST_OVERRIDE` | — | Override detected host for custom domains |
145
  | `N8N_STARTUP_TIMEOUT` | `180` | Max seconds to wait for n8n readiness |
146
+ | `UPTIMEROBOT_API_KEY` | | UptimeRobot Main API key. When set, a monitor is created automatically at boot. |
 
147
 
148
  ## 💻 Local Development
149
 
 
171
 
172
  ## 🐛 Troubleshooting
173
 
174
+ - **Telegram/Google/WhatsApp not connecting:** Ensure `CLOUDFLARE_WORKERS_TOKEN` or `CLOUDFLARE_PROXY_URL` is set. Use `CLOUDFLARE_PROXY_DOMAINS=*` to proxy all external traffic.
175
  - **Workflows not saving:** Check if `HF_TOKEN` has **Write** access to your account.
176
+ - **Space keeps sleeping:** Add `UPTIMEROBOT_API_KEY` as a Space secret to enable automatic keep-awake monitoring.
177
  - **Authentication errors:** n8n v2 uses its own internal users; ensure you created the owner account on first run.
178
 
179
  ## 🌟 More Projects
 
196
  If Hugging8n saves you time, consider buying me a coffee to keep the projects alive!
197
 
198
  **USDT (TRC-20 / TRON network only)**
199
+
200
  ```
201
  TELx8TJz1W1h7n6SgpgGNNGZXpJCEUZrdB
202
  ```
203
+
204
  > [!WARNING]
205
  > Send **USDT on TRC-20 network only**. Sending other tokens or using a different network will result in permanent loss.
206
 
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/hugging8n-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",
@@ -210,10 +230,17 @@ def main() -> int:
210
 
211
  worker_name = derive_worker_name()
212
  allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
213
- allow_proxy_all = not allowed_raw or allowed_raw == "*"
214
- allowed_targets = DEFAULT_ALLOWED if allow_proxy_all else [
215
- value.strip() for value in allowed_raw.split(",") if value.strip()
216
- ]
 
 
 
 
 
 
 
217
  proxy_secret = existing_secret or secrets.token_urlsafe(24)
218
  worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
219
 
 
12
  API_BASE = "https://api.cloudflare.com/client/v4"
13
  ENV_FILE = Path("/tmp/hugging8n-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",
 
230
 
231
  worker_name = derive_worker_name()
232
  allowed_raw = os.environ.get("CLOUDFLARE_PROXY_DOMAINS", "").strip()
233
+ allow_proxy_all = allowed_raw == "*"
234
+ if allow_proxy_all:
235
+ allowed_targets = DEFAULT_ALLOWED
236
+ else:
237
+ extra = [v.strip() for v in allowed_raw.split(",") if v.strip()]
238
+ seen = set(DEFAULT_ALLOWED)
239
+ allowed_targets = list(DEFAULT_ALLOWED)
240
+ for domain in extra:
241
+ if domain not in seen:
242
+ allowed_targets.append(domain)
243
+ seen.add(domain)
244
  proxy_secret = existing_secret or secrets.token_urlsafe(24)
245
  worker_source = render_worker(proxy_secret, allowed_targets, allow_proxy_all)
246
 
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,5 +1,4 @@
1
  const http = require("http");
2
- const https = require("https");
3
  const fs = require("fs");
4
  const net = require("net");
5
 
@@ -7,15 +6,9 @@ const PORT = Number(process.env.PUBLIC_PORT || 7861);
7
  const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
8
  const TARGET_HOST = "127.0.0.1";
9
  const SYNC_STATUS_FILE = "/tmp/hugging8n-sync-status.json";
 
 
10
  const startTime = Date.now();
11
- const UPTIMEROBOT_SETUP_ENABLED =
12
- String(process.env.UPTIMEROBOT_SETUP_ENABLED || "true").toLowerCase() ===
13
- "true";
14
- const UPTIMEROBOT_RATE_WINDOW_MS = 60 * 1000;
15
- const UPTIMEROBOT_RATE_MAX = Number(
16
- process.env.UPTIMEROBOT_RATE_LIMIT_PER_MINUTE || 5,
17
- );
18
- const uptimerobotRateMap = new Map();
19
 
20
  function parseRequestUrl(url) {
21
  try {
@@ -38,6 +31,15 @@ function getStatus() {
38
  };
39
  }
40
 
 
 
 
 
 
 
 
 
 
41
  function probeN8nHealth(timeoutMs = 1500) {
42
  return new Promise((resolve) => {
43
  const request = http.get(
@@ -60,46 +62,6 @@ function probeN8nHealth(timeoutMs = 1500) {
60
  });
61
  }
62
 
63
- function getRequesterIp(req) {
64
- return (
65
- req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
66
- req.socket.remoteAddress ||
67
- "unknown"
68
- );
69
- }
70
-
71
- function isRateLimited(req) {
72
- const now = Date.now();
73
- const ip = getRequesterIp(req);
74
- const bucket = uptimerobotRateMap.get(ip) || [];
75
- const recent = bucket.filter((ts) => now - ts < UPTIMEROBOT_RATE_WINDOW_MS);
76
- recent.push(now);
77
- uptimerobotRateMap.set(ip, recent);
78
- return recent.length > UPTIMEROBOT_RATE_MAX;
79
- }
80
-
81
- // Prune stale rate-limit buckets every 5 minutes to prevent unbounded growth.
82
- setInterval(() => {
83
- const cutoff = Date.now() - UPTIMEROBOT_RATE_WINDOW_MS;
84
- for (const [ip, timestamps] of uptimerobotRateMap) {
85
- if (timestamps.every((ts) => ts < cutoff)) uptimerobotRateMap.delete(ip);
86
- }
87
- }, 5 * 60 * 1000).unref();
88
-
89
- function isAllowedUptimeSetupOrigin(req) {
90
- const host = String(req.headers.host || "").toLowerCase();
91
- const origin = String(req.headers.origin || "").toLowerCase();
92
- const referer = String(req.headers.referer || "").toLowerCase();
93
- if (!host) return false;
94
- if (origin && !origin.includes(host)) return false;
95
- if (referer && !referer.includes(host)) return false;
96
- return true;
97
- }
98
-
99
- function isValidUptimeApiKey(key) {
100
- return /^[A-Za-z0-9_-]{20,128}$/.test(String(key || ""));
101
- }
102
-
103
  function renderDashboard(data) {
104
  const { status } = data.sync;
105
  const getBadge = (status) => {
@@ -115,42 +77,31 @@ function renderDashboard(data) {
115
  return `<div class="status-badge ${cls}">${cls === "status-online" ? '<div class="pulse"></div>' : ""}${String(status).toUpperCase()}</div>`;
116
  };
117
 
118
- const keepAwakeHtml = data.isPrivate
119
- ? `
120
- <div id="uptimerobot-private-note" class="helper-summary">
121
- <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.
122
- </div>
123
- `
124
- : `
125
- <div id="uptimerobot-public-flow">
126
- <div id="uptimerobot-summary" class="helper-summary">
127
- One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.
128
- </div>
129
- <button id="uptimerobot-toggle" class="helper-toggle" type="button">
130
- Set Up Monitor
131
- </button>
132
- <div id="uptimerobot-shell" class="helper-shell hidden">
133
- <div class="helper-copy">
134
- Do <strong>not</strong> use the Read-only API key or a Monitor-specific API key.
135
- </div>
136
- <div class="helper-row">
137
- <input
138
- id="uptimerobot-key"
139
- class="helper-input"
140
- type="password"
141
- placeholder="Paste your UptimeRobot Main API key"
142
- autocomplete="off"
143
- />
144
- <button id="uptimerobot-btn" class="helper-button" type="button">
145
- Create Monitor
146
- </button>
147
- </div>
148
- <div class="helper-note">
149
- One-time setup. Your key is only used to create the monitor for this Space.
150
- </div>
151
- </div>
152
- </div>
153
- `;
154
 
155
  return `
156
  <!DOCTYPE html>
@@ -196,7 +147,7 @@ function renderDashboard(data) {
196
  }
197
  h1 { font-size: 2.5rem; margin-bottom: 8px; letter-spacing: -1px; }
198
  .subtitle { color: var(--text-muted); margin-bottom: 32px; font-weight: 300; }
199
-
200
  .stats {
201
  display: grid;
202
  grid-template-columns: 1fr 1fr;
@@ -233,7 +184,7 @@ function renderDashboard(data) {
233
  .status-online { background: rgba(34, 197, 94, 0.2); color: var(--success); }
234
  .status-syncing { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
235
  .status-offline { background: rgba(239, 68, 68, 0.2); color: var(--error); }
236
-
237
  .pulse {
238
  width: 8px;
239
  height: 8px;
@@ -269,80 +220,7 @@ function renderDashboard(data) {
269
  text-align: left;
270
  }
271
  .keep-alive h3 { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
272
-
273
- .helper-card {
274
- width: 100%;
275
- }
276
- .helper-copy {
277
- color: var(--text-muted);
278
- font-size: 0.85rem;
279
- line-height: 1.6;
280
- margin-top: 10px;
281
- }
282
- .helper-copy strong {
283
- color: var(--text);
284
- }
285
- .helper-row {
286
- display: flex;
287
- gap: 10px;
288
- margin-top: 16px;
289
- flex-wrap: wrap;
290
- }
291
- .helper-input {
292
- flex: 1;
293
- min-width: 240px;
294
- background: rgba(255, 255, 255, 0.04);
295
- border: 1px solid rgba(255, 255, 255, 0.08);
296
- color: var(--text);
297
- border-radius: 12px;
298
- padding: 12px 16px;
299
- font: inherit;
300
- }
301
- .helper-input::placeholder {
302
- color: var(--text-muted);
303
- }
304
- .helper-button {
305
- background: var(--accent);
306
- color: #fff;
307
- border: 0;
308
- border-radius: 12px;
309
- padding: 12px 18px;
310
- font: inherit;
311
- font-weight: 600;
312
- cursor: pointer;
313
- }
314
- .helper-button:disabled {
315
- opacity: 0.6;
316
- cursor: wait;
317
- }
318
- .hidden {
319
- display: none !important;
320
- }
321
- .helper-note {
322
- margin-top: 10px;
323
- font-size: 0.82rem;
324
- color: var(--text-muted);
325
- }
326
- .helper-result {
327
- margin-top: 14px;
328
- padding: 12px 14px;
329
- border-radius: 12px;
330
- font-size: 0.9rem;
331
- display: none;
332
- }
333
- .helper-result.ok {
334
- display: block;
335
- background: rgba(34, 197, 94, 0.1);
336
- color: var(--success);
337
- }
338
- .helper-result.error {
339
- display: block;
340
- background: rgba(239, 68, 68, 0.1);
341
- color: var(--error);
342
- }
343
- .helper-shell {
344
- margin-top: 12px;
345
- }
346
  .helper-summary {
347
  margin-top: 14px;
348
  padding: 12px 14px;
@@ -351,34 +229,28 @@ function renderDashboard(data) {
351
  color: var(--text-muted);
352
  font-size: 0.85rem;
353
  line-height: 1.5;
354
- }
355
- .helper-summary strong {
356
- color: var(--text);
357
- }
358
- .helper-summary.success {
359
- background: rgba(34, 197, 94, 0.08);
360
- }
361
- .helper-toggle {
362
- margin-top: 14px;
363
- display: inline-flex;
364
  align-items: center;
365
- justify-content: center;
366
- background: rgba(255, 255, 255, 0.04);
 
 
 
 
 
 
 
367
  color: var(--text);
368
- border: 1px solid rgba(255, 255, 255, 0.08);
369
- border-radius: 12px;
370
- padding: 10px 16px;
371
- font: inherit;
372
- font-weight: 600;
373
- cursor: pointer;
374
  }
 
 
375
  </style>
376
  </head>
377
  <body>
378
  <div class="dashboard">
379
  <h1>🔗 Hugging8n</h1>
380
  <p class="subtitle">Workflow Automation Space</p>
381
-
382
  <div class="stats">
383
  <div class="stat-card">
384
  <div class="stat-label">Uptime</div>
@@ -401,233 +273,15 @@ function renderDashboard(data) {
401
 
402
  <a href="/home/workflows" target="_blank" class="btn-primary">Open n8n Editor</a>
403
 
404
- <div class="keep-alive helper-card">
405
  <span class="stat-label">Keep Space Awake</span>
406
  ${keepAwakeHtml}
407
- <div id="uptimerobot-result" class="helper-result"></div>
408
  </div>
409
  </div>
410
-
411
- <script>
412
- function getCurrentSearch() {
413
- return window.location.search || '';
414
- }
415
-
416
- const monitorStateKey = 'hugging8n_uptimerobot_setup_v1';
417
- const KEEP_AWAKE_PRIVATE = ${data.isPrivate ? "true" : "false"};
418
-
419
- function setMonitorUiState(isConfigured) {
420
- const summary = document.getElementById('uptimerobot-summary');
421
- const shell = document.getElementById('uptimerobot-shell');
422
- const toggle = document.getElementById('uptimerobot-toggle');
423
-
424
- if (!summary || !shell || !toggle) return;
425
-
426
- if (isConfigured) {
427
- summary.classList.add('success');
428
- summary.innerHTML = '<strong>Already set up.</strong> Your UptimeRobot monitor should keep this public Space awake.';
429
- shell.classList.add('hidden');
430
- toggle.textContent = 'Set Up Again';
431
- } else {
432
- summary.classList.remove('success');
433
- summary.innerHTML = 'One-time setup for public Spaces. Paste your UptimeRobot <strong>Main API key</strong> to create the monitor.';
434
- toggle.textContent = 'Set Up Monitor';
435
- }
436
- }
437
-
438
- function restoreMonitorUiState() {
439
- try {
440
- const value = window.localStorage.getItem(monitorStateKey);
441
- setMonitorUiState(value === 'done');
442
- } catch {
443
- setMonitorUiState(false);
444
- }
445
- }
446
-
447
- function toggleMonitorSetup() {
448
- const shell = document.getElementById('uptimerobot-shell');
449
- shell.classList.toggle('hidden');
450
- }
451
-
452
- async function setupUptimeRobot() {
453
- const input = document.getElementById('uptimerobot-key');
454
- const button = document.getElementById('uptimerobot-btn');
455
- const result = document.getElementById('uptimerobot-result');
456
- const apiKey = input.value.trim();
457
-
458
- if (!apiKey) {
459
- result.className = 'helper-result error';
460
- result.textContent = 'Paste your UptimeRobot Main API key first.';
461
- return;
462
- }
463
-
464
- button.disabled = true;
465
- button.textContent = 'Creating...';
466
- result.className = 'helper-result';
467
- result.textContent = '';
468
-
469
- try {
470
- const res = await fetch('/uptimerobot/setup' + getCurrentSearch(), {
471
- method: 'POST',
472
- headers: { 'Content-Type': 'application/json' },
473
- body: JSON.stringify({ apiKey })
474
- });
475
- const data = await res.json();
476
-
477
- if (!res.ok) {
478
- throw new Error(data.message || 'Failed to create monitor.');
479
- }
480
-
481
- result.className = 'helper-result ok';
482
- result.textContent = data.message || 'UptimeRobot monitor is ready.';
483
- input.value = '';
484
- try {
485
- window.localStorage.setItem(monitorStateKey, 'done');
486
- } catch {}
487
- setMonitorUiState(true);
488
- document.getElementById('uptimerobot-shell').classList.add('hidden');
489
- } catch (error) {
490
- result.className = 'helper-result error';
491
- result.textContent = error.message || 'Failed to create monitor.';
492
- } finally {
493
- button.disabled = false;
494
- button.textContent = 'Create Monitor';
495
- }
496
- }
497
-
498
- if (!KEEP_AWAKE_PRIVATE) {
499
- restoreMonitorUiState();
500
- document.getElementById('uptimerobot-btn').addEventListener('click', setupUptimeRobot);
501
- document.getElementById('uptimerobot-toggle').addEventListener('click', toggleMonitorSetup);
502
- }
503
- </script>
504
  </body>
505
  </html>`;
506
  }
507
 
508
- async function resolveSpaceIsPrivate(req) {
509
- const host = (req.headers.host || "").split(":")[0];
510
- if (!host.endsWith(".hf.space")) return false;
511
-
512
- const params = new URLSearchParams(req.url.split("?")[1] || "");
513
- const token = params.get("__sign");
514
- if (!token) return false;
515
- try {
516
- const payload = JSON.parse(
517
- Buffer.from(token.split(".")[1], "base64").toString(),
518
- );
519
- const sub = payload.sub || "";
520
- const match_sub = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
521
- if (!match_sub) return false;
522
- return new Promise((resolve) => {
523
- https
524
- .get(
525
- `https://huggingface.co/api/spaces/${match_sub[1]}/${match_sub[2]}`,
526
- { headers: { "User-Agent": "Hugging8n" } },
527
- (res) => {
528
- resolve(
529
- res.statusCode === 401 ||
530
- res.statusCode === 403 ||
531
- res.statusCode === 404,
532
- );
533
- },
534
- )
535
- .on("error", () => resolve(false));
536
- });
537
- } catch {
538
- return false;
539
- }
540
- }
541
-
542
- function readRequestBody(req) {
543
- return new Promise((resolve, reject) => {
544
- let body = "";
545
- req.on("data", (chunk) => {
546
- body += chunk;
547
- if (body.length > 1024 * 64) {
548
- reject(new Error("Request too large"));
549
- req.destroy();
550
- }
551
- });
552
- req.on("end", () => resolve(body));
553
- req.on("error", reject);
554
- });
555
- }
556
-
557
- function postUptimeRobot(path, form) {
558
- const body = new URLSearchParams(form).toString();
559
- return new Promise((resolve, reject) => {
560
- const request = https.request(
561
- {
562
- hostname: "api.uptimerobot.com",
563
- port: 443,
564
- method: "POST",
565
- path,
566
- headers: {
567
- "Content-Type": "application/x-www-form-urlencoded",
568
- "Content-Length": Buffer.byteLength(body),
569
- },
570
- },
571
- (response) => {
572
- let raw = "";
573
- response.setEncoding("utf8");
574
- response.on("data", (chunk) => {
575
- raw += chunk;
576
- });
577
- response.on("end", () => {
578
- try {
579
- resolve(JSON.parse(raw));
580
- } catch {
581
- reject(new Error("Unexpected response from UptimeRobot"));
582
- }
583
- });
584
- },
585
- );
586
- request.on("error", reject);
587
- request.write(body);
588
- request.end();
589
- });
590
- }
591
-
592
- async function createUptimeRobotMonitor(apiKey, host) {
593
- const cleanHost = String(host || "")
594
- .replace(/^https?:\/\//, "")
595
- .replace(/\/.*$/, "");
596
- if (!cleanHost.endsWith(".hf.space")) {
597
- throw new Error("Uptime setup is only supported on .hf.space hosts.");
598
- }
599
- if (!cleanHost) throw new Error("Missing Space host.");
600
- const monitorUrl = `https://${cleanHost}/health`;
601
- const existing = await postUptimeRobot("/v2/getMonitors", {
602
- api_key: apiKey,
603
- format: "json",
604
- logs: "0",
605
- response_times: "0",
606
- response_times_limit: "1",
607
- });
608
- const existingMonitor = Array.isArray(existing.monitors)
609
- ? existing.monitors.find((m) => m.url === monitorUrl)
610
- : null;
611
- if (existingMonitor) {
612
- return {
613
- created: false,
614
- message: `Monitor already exists for ${monitorUrl}`,
615
- };
616
- }
617
- const created = await postUptimeRobot("/v2/newMonitor", {
618
- api_key: apiKey,
619
- format: "json",
620
- type: "1",
621
- friendly_name: `Hugging8n ${cleanHost}`,
622
- url: monitorUrl,
623
- interval: "300",
624
- });
625
- if (created.stat !== "ok") {
626
- throw new Error(created?.error?.message || "Failed to create monitor.");
627
- }
628
- return { created: true, message: `Monitor created for ${monitorUrl}` };
629
- }
630
-
631
  const server = http.createServer(async (req, res) => {
632
  const url = parseRequestUrl(req.url);
633
  const pathname = url.pathname;
@@ -655,53 +309,14 @@ const server = http.createServer(async (req, res) => {
655
  }),
656
  );
657
  }
658
- if (pathname === "/uptimerobot/setup" && req.method === "POST") {
659
- void (async () => {
660
- try {
661
- if (!UPTIMEROBOT_SETUP_ENABLED) {
662
- res.writeHead(403, { "Content-Type": "application/json" });
663
- return res.end(
664
- JSON.stringify({ message: "Uptime setup is disabled." }),
665
- );
666
- }
667
- if (isRateLimited(req)) {
668
- res.writeHead(429, { "Content-Type": "application/json" });
669
- return res.end(JSON.stringify({ message: "Too many requests." }));
670
- }
671
- if (!isAllowedUptimeSetupOrigin(req)) {
672
- res.writeHead(403, { "Content-Type": "application/json" });
673
- return res.end(
674
- JSON.stringify({ message: "Invalid request origin." }),
675
- );
676
- }
677
-
678
- const body = await readRequestBody(req);
679
- const { apiKey } = JSON.parse(body || "{}");
680
- if (!isValidUptimeApiKey(apiKey)) {
681
- res.writeHead(400, { "Content-Type": "application/json" });
682
- return res.end(
683
- JSON.stringify({ message: "A valid API key is required." }),
684
- );
685
- }
686
- const result = await createUptimeRobotMonitor(apiKey, req.headers.host);
687
- res.writeHead(200, { "Content-Type": "application/json" });
688
- res.end(JSON.stringify(result));
689
- } catch (e) {
690
- res.writeHead(400, { "Content-Type": "application/json" });
691
- res.end(JSON.stringify({ message: e.message || "Invalid request." }));
692
- }
693
- })();
694
- return;
695
- }
696
  if (pathname === "/" || pathname === "/dashboard") {
697
  const uptime = Math.floor((Date.now() - startTime) / 1000);
698
- const isPrivate = await resolveSpaceIsPrivate(req);
699
  res.writeHead(200, { "Content-Type": "text/html" });
700
  return res.end(
701
  renderDashboard({
702
  uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
703
  sync: getStatus(),
704
- isPrivate,
705
  }),
706
  );
707
  }
 
1
  const http = require("http");
 
2
  const fs = require("fs");
3
  const net = require("net");
4
 
 
6
  const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
7
  const TARGET_HOST = "127.0.0.1";
8
  const SYNC_STATUS_FILE = "/tmp/hugging8n-sync-status.json";
9
+ const UPTIMEROBOT_STATUS_FILE = "/tmp/hugging8n-uptimerobot-status.json";
10
+ const UPTIMEROBOT_API_KEY_SET = !!process.env.UPTIMEROBOT_API_KEY;
11
  const startTime = Date.now();
 
 
 
 
 
 
 
 
12
 
13
  function parseRequestUrl(url) {
14
  try {
 
31
  };
32
  }
33
 
34
+ function getUptimeRobotStatus() {
35
+ try {
36
+ if (fs.existsSync(UPTIMEROBOT_STATUS_FILE)) {
37
+ return JSON.parse(fs.readFileSync(UPTIMEROBOT_STATUS_FILE, "utf8"));
38
+ }
39
+ } catch {}
40
+ return null;
41
+ }
42
+
43
  function probeN8nHealth(timeoutMs = 1500) {
44
  return new Promise((resolve) => {
45
  const request = http.get(
 
62
  });
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  function renderDashboard(data) {
66
  const { status } = data.sync;
67
  const getBadge = (status) => {
 
77
  return `<div class="status-badge ${cls}">${cls === "status-online" ? '<div class="pulse"></div>' : ""}${String(status).toUpperCase()}</div>`;
78
  };
79
 
80
+ const urStatus = data.uptimerobotStatus;
81
+ let keepAwakeHtml;
82
+ if (urStatus?.configured) {
83
+ keepAwakeHtml = `
84
+ <div class="helper-summary success">
85
+ ${getBadge("configured")}
86
+ <span>UptimeRobot monitor active for <code>${urStatus.url || "your /health endpoint"}</code>.</span>
87
+ </div>`;
88
+ } else if (urStatus?.configured === false) {
89
+ keepAwakeHtml = `
90
+ <div class="helper-summary error">
91
+ ${getBadge("failed")}
92
+ <span>Monitor setup failed. Check Space logs for details.</span>
93
+ </div>`;
94
+ } else if (UPTIMEROBOT_API_KEY_SET) {
95
+ keepAwakeHtml = `
96
+ <div class="helper-summary">
97
+ ${getBadge("syncing")} Setting up UptimeRobot monitor&hellip;
98
+ </div>`;
99
+ } else {
100
+ keepAwakeHtml = `
101
+ <div class="helper-summary">
102
+ <strong>Not configured.</strong> Add <code>UPTIMEROBOT_API_KEY</code> to Space secrets to enable keep-awake monitoring.
103
+ </div>`;
104
+ }
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  return `
107
  <!DOCTYPE html>
 
147
  }
148
  h1 { font-size: 2.5rem; margin-bottom: 8px; letter-spacing: -1px; }
149
  .subtitle { color: var(--text-muted); margin-bottom: 32px; font-weight: 300; }
150
+
151
  .stats {
152
  display: grid;
153
  grid-template-columns: 1fr 1fr;
 
184
  .status-online { background: rgba(34, 197, 94, 0.2); color: var(--success); }
185
  .status-syncing { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
186
  .status-offline { background: rgba(239, 68, 68, 0.2); color: var(--error); }
187
+
188
  .pulse {
189
  width: 8px;
190
  height: 8px;
 
220
  text-align: left;
221
  }
222
  .keep-alive h3 { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
223
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  .helper-summary {
225
  margin-top: 14px;
226
  padding: 12px 14px;
 
229
  color: var(--text-muted);
230
  font-size: 0.85rem;
231
  line-height: 1.5;
232
+ display: flex;
 
 
 
 
 
 
 
 
 
233
  align-items: center;
234
+ gap: 10px;
235
+ flex-wrap: wrap;
236
+ }
237
+ .helper-summary strong { color: var(--text); }
238
+ .helper-summary code {
239
+ background: rgba(255,255,255,0.06);
240
+ padding: 2px 6px;
241
+ border-radius: 6px;
242
+ font-size: 0.82rem;
243
  color: var(--text);
 
 
 
 
 
 
244
  }
245
+ .helper-summary.success { background: rgba(34, 197, 94, 0.08); }
246
+ .helper-summary.error { background: rgba(239, 68, 68, 0.08); }
247
  </style>
248
  </head>
249
  <body>
250
  <div class="dashboard">
251
  <h1>🔗 Hugging8n</h1>
252
  <p class="subtitle">Workflow Automation Space</p>
253
+
254
  <div class="stats">
255
  <div class="stat-card">
256
  <div class="stat-label">Uptime</div>
 
273
 
274
  <a href="/home/workflows" target="_blank" class="btn-primary">Open n8n Editor</a>
275
 
276
+ <div class="keep-alive">
277
  <span class="stat-label">Keep Space Awake</span>
278
  ${keepAwakeHtml}
 
279
  </div>
280
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  </body>
282
  </html>`;
283
  }
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  const server = http.createServer(async (req, res) => {
286
  const url = parseRequestUrl(req.url);
287
  const pathname = url.pathname;
 
309
  }),
310
  );
311
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  if (pathname === "/" || pathname === "/dashboard") {
313
  const uptime = Math.floor((Date.now() - startTime) / 1000);
 
314
  res.writeHead(200, { "Content-Type": "text/html" });
315
  return res.end(
316
  renderDashboard({
317
  uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
318
  sync: getStatus(),
319
+ uptimerobotStatus: getUptimeRobotStatus(),
320
  }),
321
  );
322
  }
setup-uptimerobot.sh CHANGED
@@ -4,6 +4,7 @@ set -euo pipefail
4
  API_URL="https://api.uptimerobot.com/v2"
5
  API_KEY="${UPTIMEROBOT_API_KEY:-}"
6
  SPACE_HOST_INPUT="${1:-${SPACE_HOST:-}}"
 
7
 
8
  if [ -z "$API_KEY" ]; then
9
  echo "Missing UPTIMEROBOT_API_KEY."
@@ -22,7 +23,7 @@ SPACE_HOST_CLEAN="${SPACE_HOST_CLEAN%%/*}"
22
 
23
  MONITOR_URL="https://${SPACE_HOST_CLEAN}/health"
24
  MONITOR_NAME="${UPTIMEROBOT_MONITOR_NAME:-Hugging8n ${SPACE_HOST_CLEAN}}"
25
- INTERVAL="${UPTIMEROBOT_INTERVAL:-5}"
26
 
27
  MONITORS_RESPONSE=$(curl -sS -X POST "${API_URL}/getMonitors" \
28
  -d "api_key=${API_KEY}" \
@@ -36,6 +37,8 @@ MONITOR_ID=$(printf '%s' "$MONITORS_RESPONSE" | jq -r --arg url "$MONITOR_URL" '
36
  ')
37
 
38
  if [ -n "$MONITOR_ID" ]; then
 
 
39
  echo "Monitor already exists (id=${MONITOR_ID}) for ${MONITOR_URL}"
40
  exit 0
41
  fi
@@ -59,9 +62,13 @@ CREATE_RESPONSE=$(curl "${CURL_ARGS[@]}")
59
  CREATE_STATUS=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.stat // "fail"')
60
 
61
  if [ "$CREATE_STATUS" != "ok" ]; then
 
 
62
  printf '%s\n' "$CREATE_RESPONSE"
63
  exit 1
64
  fi
65
 
66
  NEW_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.monitor.id // empty')
 
 
67
  echo "Created UptimeRobot monitor ${NEW_ID:-"(id unavailable)"} for ${MONITOR_URL}"
 
4
  API_URL="https://api.uptimerobot.com/v2"
5
  API_KEY="${UPTIMEROBOT_API_KEY:-}"
6
  SPACE_HOST_INPUT="${1:-${SPACE_HOST:-}}"
7
+ STATUS_FILE="/tmp/hugging8n-uptimerobot-status.json"
8
 
9
  if [ -z "$API_KEY" ]; then
10
  echo "Missing UPTIMEROBOT_API_KEY."
 
23
 
24
  MONITOR_URL="https://${SPACE_HOST_CLEAN}/health"
25
  MONITOR_NAME="${UPTIMEROBOT_MONITOR_NAME:-Hugging8n ${SPACE_HOST_CLEAN}}"
26
+ INTERVAL="${UPTIMEROBOT_INTERVAL:-300}"
27
 
28
  MONITORS_RESPONSE=$(curl -sS -X POST "${API_URL}/getMonitors" \
29
  -d "api_key=${API_KEY}" \
 
37
  ')
38
 
39
  if [ -n "$MONITOR_ID" ]; then
40
+ printf '{"configured":true,"monitorId":"%s","url":"%s","alreadyExisted":true,"timestamp":"%s"}\n' \
41
+ "$MONITOR_ID" "$MONITOR_URL" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS_FILE"
42
  echo "Monitor already exists (id=${MONITOR_ID}) for ${MONITOR_URL}"
43
  exit 0
44
  fi
 
62
  CREATE_STATUS=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.stat // "fail"')
63
 
64
  if [ "$CREATE_STATUS" != "ok" ]; then
65
+ printf '{"configured":false,"error":"creation failed","timestamp":"%s"}\n' \
66
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS_FILE"
67
  printf '%s\n' "$CREATE_RESPONSE"
68
  exit 1
69
  fi
70
 
71
  NEW_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.monitor.id // empty')
72
+ printf '{"configured":true,"monitorId":"%s","url":"%s","timestamp":"%s"}\n' \
73
+ "${NEW_ID:-}" "$MONITOR_URL" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS_FILE"
74
  echo "Created UptimeRobot monitor ${NEW_ID:-"(id unavailable)"} for ${MONITOR_URL}"
start.sh CHANGED
@@ -77,7 +77,6 @@ CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}
77
  export CLOUDFLARE_WORKERS_TOKEN
78
  CF_PROXY_ENV_FILE="/tmp/hugging8n-cloudflare-proxy.env"
79
  if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
80
- export CLOUDFLARE_PROXY_DOMAINS="${CLOUDFLARE_PROXY_DOMAINS:-*}"
81
  export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"
82
  echo "Preparing Cloudflare outbound proxy..."
83
  python3 "$APP_DIR/cloudflare-proxy-setup.py" || true
@@ -119,6 +118,11 @@ fi
119
  node "$APP_DIR/health-server.js" &
120
  PROXY_PID=$!
121
 
 
 
 
 
 
122
  n8n start &
123
  N8N_PID=$!
124
 
 
77
  export CLOUDFLARE_WORKERS_TOKEN
78
  CF_PROXY_ENV_FILE="/tmp/hugging8n-cloudflare-proxy.env"
79
  if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
 
80
  export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"
81
  echo "Preparing Cloudflare outbound proxy..."
82
  python3 "$APP_DIR/cloudflare-proxy-setup.py" || true
 
118
  node "$APP_DIR/health-server.js" &
119
  PROXY_PID=$!
120
 
121
+ if [ -n "${UPTIMEROBOT_API_KEY:-}" ] && [ -n "${SPACE_HOST_DETECTED:-}" ]; then
122
+ echo "Setting up UptimeRobot monitor..."
123
+ bash "$APP_DIR/setup-uptimerobot.sh" "$SPACE_HOST_DETECTED" || true
124
+ fi
125
+
126
  n8n start &
127
  N8N_PID=$!
128