somratpro commited on
Commit
bd3ae30
·
1 Parent(s): 9818fe3

refactor: reconfigure n8n for root path hosting and overhaul dashboard UI components

Browse files
Files changed (2) hide show
  1. health-server.js +238 -136
  2. start.sh +4 -4
health-server.js CHANGED
@@ -32,7 +32,8 @@ function getStatus() {
32
  }
33
 
34
  function renderDashboard(data) {
35
- const syncBadge = (status) => {
 
36
  let cls = "status-offline";
37
  if (
38
  status === "success" ||
@@ -49,12 +50,12 @@ function renderDashboard(data) {
49
  ? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
50
  : `
51
  <div id="uptimerobot-flow">
52
- <div class="helper-summary" id="uptimerobot-summary">Setup a free monitor to prevent this Space from sleeping.</div>
53
- <button id="uptimerobot-toggle" class="helper-toggle">Set Up Monitor</button>
54
- <div id="uptimerobot-shell" class="hidden" style="margin-top:12px">
55
- <input id="uptimerobot-key" class="helper-input" type="password" placeholder="UptimeRobot Main API Key">
56
- <button id="uptimerobot-btn" class="helper-button">Create Monitor</button>
57
  </div>
 
58
  </div>
59
  `;
60
 
@@ -65,34 +66,139 @@ function renderDashboard(data) {
65
  <meta charset="UTF-8">
66
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
67
  <title>Hugging8n Dashboard</title>
 
 
68
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
69
  <style>
70
- :root { --bg: #0f172a; --card: rgba(30, 41, 59, 0.7); --accent: linear-gradient(135deg, #3b82f6, #8b5cf6); --text: #f8fafc; --text-dim: #94a3b8; --success: #10b981; --error: #ef4444; }
 
 
 
 
 
 
 
 
 
71
  * { box-sizing: border-box; margin: 0; padding: 0; }
72
- body { font-family: 'Outfit', sans-serif; background: var(--bg); color: var(--text); display: flex; justify-content: center; min-height: 100vh; padding: 40px 20px; background-image: radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%), radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%); }
73
- .dashboard { width: 100%; max-width: 600px; background: var(--card); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 24px; padding: 40px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
74
- h1 { font-size: 2.2rem; margin-bottom: 8px; background: var(--accent); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; }
75
- .subtitle { color: var(--text-dim); text-align: center; font-size: 0.9rem; margin-bottom: 40px; text-transform: uppercase; letter-spacing: 1px; }
76
- .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
77
- .card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05); padding: 20px; border-radius: 16px; }
78
- .label { color: var(--text-dim); font-size: 0.75rem; text-transform: uppercase; margin-bottom: 8px; display: block; }
79
- .value { font-size: 1.1rem; font-weight: 600; }
80
- .btn { display: block; background: var(--accent); color: #fff; padding: 16px; border-radius: 16px; text-align: center; text-decoration: none; font-weight: 600; margin-top: 8px; transition: transform 0.2s; box-shadow: 0 10px 20px -5px rgba(59, 130, 246, 0.4); }
81
- .btn:hover { transform: scale(1.02); }
82
- .status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }
83
- .status-online { background: rgba(16, 185, 129, 0.1); color: var(--success); }
84
- .status-syncing { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
85
- .status-offline { background: rgba(239, 68, 68, 0.1); color: var(--error); }
86
- .pulse { width: 8px; height: 8px; border-radius: 50%; background: currentColor; animation: pulse 2s infinite; }
87
- @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
88
- .helper-input { width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 12px; border-radius: 12px; margin-bottom: 12px; }
89
- .helper-button { background: var(--accent); color: #fff; border: 0; padding: 12px; border-radius: 12px; cursor: pointer; width: 100%; font-weight: 600; }
90
- .helper-toggle { background: rgba(255,255,255,0.05); color: #fff; border: 1px solid rgba(255,255,255,0.1); padding: 8px 16px; border-radius: 12px; cursor: pointer; font-size: 0.85rem; }
91
- .hidden { display: none; }
92
- .helper-summary { background: rgba(255,255,255,0.03); padding: 12px; border-radius: 12px; font-size: 0.85rem; color: var(--text-dim); margin-bottom: 12px; }
93
- .helper-result { margin-top: 12px; font-size: 0.85rem; padding: 10px; border-radius: 8px; display: none; }
94
- .helper-result.ok { display: block; background: rgba(16, 185, 129, 0.1); color: var(--success); }
95
- .helper-result.error { display: block; background: rgba(239, 68, 68, 0.1); color: var(--error); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </style>
97
  </head>
98
  <body>
@@ -101,67 +207,58 @@ function renderDashboard(data) {
101
  <p class="subtitle">Workflow Automation Space</p>
102
 
103
  <div class="stats">
104
- <div class="card"><span class="label">Uptime</span><span class="value" id="uptime">${data.uptimeHuman}</span></div>
105
- <div class="card"><span class="label">n8n Port</span><span class="value">${TARGET_PORT}</span></div>
 
 
 
 
 
 
106
  </div>
107
 
108
- <div class="card" style="margin-bottom:16px">
109
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px">
110
- <span class="label" style="margin-bottom:0">Sync Status</span>
111
- <div id="sync-badge">${syncBadge(data.sync.status)}</div>
112
- </div>
113
- <div style="font-size:0.85rem; color:var(--text-dim)">
114
- Last Activity: <span id="sync-time" style="color:var(--text)">${data.sync.timestamp}</span>
115
- <div id="sync-msg" style="margin-top:4px">${data.sync.message}</div>
116
  </div>
 
 
117
  </div>
118
 
119
- <a href="/app/" class="btn" target="_blank" rel="noopener noreferrer">Open n8n Editor</a>
120
 
121
- <div class="card" style="margin-top:24px">
122
- <span class="label">Keep Alive</span>
123
  ${keepAwakeHtml}
124
- <div id="uptimerobot-result" class="helper-result"></div>
125
  </div>
126
  </div>
127
 
128
  <script>
129
- async function refresh() {
 
 
 
 
 
 
 
 
130
  try {
131
- const res = await fetch('/status' + window.location.search);
132
- const d = await res.json();
133
- document.getElementById('uptime').textContent = d.uptime;
134
- document.getElementById('sync-time').textContent = d.sync.timestamp;
135
- document.getElementById('sync-msg').textContent = d.sync.message;
136
-
137
- const s = d.sync.status;
138
- let cls = "status-offline";
139
- if (s === "success" || s === "configured" || s === "restored") cls = "status-online";
140
- if (s === "syncing" || s === "restoring") cls = "status-syncing";
141
- document.getElementById('sync-badge').innerHTML = '<div class="status-badge ' + cls + '">' + (cls === 'status-online' ? '<div class="pulse"></div>' : '') + s.toUpperCase() + '</div>';
142
- } catch (e) {}
143
- }
144
- setInterval(refresh, 5000);
145
-
146
- const toggle = document.getElementById('uptimerobot-toggle');
147
- if (toggle) {
148
- toggle.onclick = () => document.getElementById('uptimerobot-shell').classList.toggle('hidden');
149
- document.getElementById('uptimerobot-btn').onclick = async () => {
150
- const key = document.getElementById('uptimerobot-key').value;
151
- const res = document.getElementById('uptimerobot-result');
152
- if (!key) return;
153
- res.className = 'helper-result'; res.textContent = 'Creating...'; res.style.display = 'block';
154
- try {
155
- const r = await fetch('/uptimerobot/setup' + window.location.search, {
156
- method: 'POST',
157
- headers: { 'Content-Type': 'application/json' },
158
- body: JSON.stringify({ apiKey: key })
159
- });
160
- const data = await r.json();
161
- res.className = 'helper-result ' + (r.ok ? 'ok' : 'error');
162
- res.textContent = data.message;
163
- } catch (e) { res.className = 'helper-result error'; res.textContent = 'Connection failed'; }
164
- };
165
  }
166
  </script>
167
  </body>
@@ -169,7 +266,13 @@ function renderDashboard(data) {
169
  }
170
 
171
  async function resolveSpaceIsPrivate(req) {
172
- const params = new URLSearchParams(parseRequestUrl(req.url).search);
 
 
 
 
 
 
173
  const token = params.get("__sign");
174
  if (!token) return false;
175
  try {
@@ -177,12 +280,12 @@ async function resolveSpaceIsPrivate(req) {
177
  Buffer.from(token.split(".")[1], "base64").toString(),
178
  );
179
  const sub = payload.sub || "";
180
- const match = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
181
- if (!match) return false;
182
  return new Promise((resolve) => {
183
  https
184
  .get(
185
- `https://huggingface.co/api/spaces/${match[1]}/${match[2]}`,
186
  { headers: { "User-Agent": "Hugging8n" } },
187
  (res) => {
188
  resolve(
@@ -203,11 +306,11 @@ const server = http.createServer(async (req, res) => {
203
  const url = parseRequestUrl(req.url);
204
  const pathname = url.pathname;
205
 
 
206
  if (pathname === "/health") {
207
  res.writeHead(200, { "Content-Type": "application/json" });
208
  return res.end(JSON.stringify({ status: "ok", ...getStatus() }));
209
  }
210
-
211
  if (pathname === "/status") {
212
  const uptime = Math.floor((Date.now() - startTime) / 1000);
213
  return res.end(
@@ -217,7 +320,6 @@ const server = http.createServer(async (req, res) => {
217
  }),
218
  );
219
  }
220
-
221
  if (pathname === "/uptimerobot/setup" && req.method === "POST") {
222
  let body = "";
223
  req.on("data", (c) => (body += c));
@@ -286,7 +388,6 @@ const server = http.createServer(async (req, res) => {
286
  });
287
  return;
288
  }
289
-
290
  if (pathname === "/" || pathname === "/dashboard") {
291
  const uptime = Math.floor((Date.now() - startTime) / 1000);
292
  const isPrivate = await resolveSpaceIsPrivate(req);
@@ -300,67 +401,68 @@ const server = http.createServer(async (req, res) => {
300
  );
301
  }
302
 
303
- // 1. Redirect root /app to /app/ (trailing slash)
304
- if (pathname === APP_BASE) {
305
- res.writeHead(301, { Location: APP_BASE + "/" });
306
- return res.end();
307
- }
 
 
 
 
 
 
 
 
 
 
308
 
309
- // 2. Confine n8n to /app subpath
310
- // If it's not a dashboard path and doesn't start with /app, redirect to /app/
311
- if (!pathname.startsWith(APP_BASE + "/")) {
312
- res.writeHead(302, { Location: APP_BASE + "/" });
313
  return res.end();
314
  }
315
 
316
- // 3. Proxy to n8n (Pass full path as n8n is natively configured for /app/)
317
- const proxyPath = pathname;
318
-
319
- // Handle n8n's common 404 on root /app/ by redirecting to workflows
320
- if (proxyPath === APP_BASE + "/" && req.method === "GET") {
321
- res.writeHead(302, { Location: APP_BASE + "/home/workflows" });
322
- return res.end();
323
- }
324
 
325
- const proxyHeaders = {
326
- ...req.headers,
327
- host: `127.0.0.1:${TARGET_PORT}`,
328
- "x-forwarded-for": req.socket.remoteAddress,
329
- "x-forwarded-host": req.headers.host,
330
- "x-forwarded-proto": "https",
331
- };
 
 
 
 
 
 
332
 
333
- const proxyReq = http.request(
334
- {
335
- hostname: TARGET_HOST,
336
- port: TARGET_PORT,
337
- path: proxyPath + url.search,
338
- method: req.method,
339
- headers: proxyHeaders,
340
- },
341
- (proxyRes) => {
342
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
343
- proxyRes.pipe(res);
344
- },
345
- );
346
 
347
- proxyReq.on("error", () => {
348
- res.writeHead(503, { "Content-Type": "application/json" });
349
- res.end(
350
- JSON.stringify({
351
- status: "starting",
352
- message: "n8n is initializing, please wait...",
353
- }),
354
- );
355
- });
356
 
357
- req.pipe(proxyReq);
 
 
358
  });
359
 
360
  server.on("upgrade", (req, socket, head) => {
361
  const url = parseRequestUrl(req.url);
362
  const proxyPath = url.pathname;
363
-
364
  const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
365
  proxySocket.write(
366
  `${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`,
@@ -376,5 +478,5 @@ server.on("upgrade", (req, socket, head) => {
376
  });
377
 
378
  server.listen(PORT, "0.0.0.0", () =>
379
- console.log(`Dashboard/Proxy on ${PORT} -> n8n on ${TARGET_PORT}`),
380
  );
 
32
  }
33
 
34
  function renderDashboard(data) {
35
+ const { status } = data.sync;
36
+ const getBadge = (status) => {
37
  let cls = "status-offline";
38
  if (
39
  status === "success" ||
 
50
  ? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
51
  : `
52
  <div id="uptimerobot-flow">
53
+ <p class="helper-text">Setup a free monitor to prevent this Space from sleeping.</p>
54
+ <div class="input-group">
55
+ <input type="password" id="ur-key" placeholder="UptimeRobot Main API Key">
56
+ <button id="ur-btn" onclick="setupMonitor()">Set Up Monitor</button>
 
57
  </div>
58
+ <p id="ur-status"></p>
59
  </div>
60
  `;
61
 
 
66
  <meta charset="UTF-8">
67
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
68
  <title>Hugging8n Dashboard</title>
69
+ <link rel="preconnect" href="https://fonts.googleapis.com">
70
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
71
  <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
72
  <style>
73
+ :root {
74
+ --bg: #0f172a;
75
+ --card: #1e293b;
76
+ --accent: #6366f1;
77
+ --text: #f8fafc;
78
+ --text-muted: #94a3b8;
79
+ --success: #22c55e;
80
+ --warning: #f59e0b;
81
+ --error: #ef4444;
82
+ }
83
  * { box-sizing: border-box; margin: 0; padding: 0; }
84
+ body {
85
+ font-family: 'Outfit', sans-serif;
86
+ background: var(--bg);
87
+ color: var(--text);
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ min-height: 100vh;
92
+ padding: 20px;
93
+ }
94
+ .dashboard {
95
+ background: var(--card);
96
+ width: 100%;
97
+ max-width: 500px;
98
+ padding: 40px;
99
+ border-radius: 24px;
100
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
101
+ text-align: center;
102
+ border: 1px solid rgba(255,255,255,0.05);
103
+ }
104
+ h1 { font-size: 2.5rem; margin-bottom: 8px; letter-spacing: -1px; }
105
+ .subtitle { color: var(--text-muted); margin-bottom: 32px; font-weight: 300; }
106
+
107
+ .stats {
108
+ display: grid;
109
+ grid-template-columns: 1fr 1fr;
110
+ gap: 16px;
111
+ margin-bottom: 24px;
112
+ }
113
+ .stat-card {
114
+ background: rgba(255,255,255,0.03);
115
+ padding: 20px;
116
+ border-radius: 16px;
117
+ text-align: left;
118
+ }
119
+ .stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; margin-bottom: 4px; }
120
+ .stat-value { font-size: 1.25rem; font-weight: 600; }
121
+
122
+ .sync-box {
123
+ background: rgba(255,255,255,0.03);
124
+ padding: 24px;
125
+ border-radius: 16px;
126
+ margin-bottom: 32px;
127
+ text-align: left;
128
+ position: relative;
129
+ }
130
+ .sync-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
131
+ .status-badge {
132
+ padding: 4px 10px;
133
+ border-radius: 20px;
134
+ font-size: 0.7rem;
135
+ font-weight: 600;
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 6px;
139
+ }
140
+ .status-online { background: rgba(34, 197, 94, 0.2); color: var(--success); }
141
+ .status-syncing { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
142
+ .status-offline { background: rgba(239, 68, 68, 0.2); color: var(--error); }
143
+
144
+ .pulse {
145
+ width: 8px;
146
+ height: 8px;
147
+ background: currentColor;
148
+ border-radius: 50%;
149
+ animation: pulse 2s infinite;
150
+ }
151
+ @keyframes pulse {
152
+ 0% { transform: scale(0.95); opacity: 0.7; }
153
+ 70% { transform: scale(1.5); opacity: 0; }
154
+ 100% { transform: scale(0.95); opacity: 0; }
155
+ }
156
+
157
+ .btn-primary {
158
+ display: block;
159
+ width: 100%;
160
+ padding: 18px;
161
+ background: var(--accent);
162
+ color: white;
163
+ text-decoration: none;
164
+ border-radius: 16px;
165
+ font-weight: 600;
166
+ font-size: 1.1rem;
167
+ transition: all 0.2s;
168
+ box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
169
+ margin-bottom: 32px;
170
+ }
171
+ .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 20px 25px -5px rgba(99, 102, 241, 0.4); }
172
+
173
+ .keep-alive {
174
+ border-top: 1px solid rgba(255,255,255,0.05);
175
+ padding-top: 24px;
176
+ text-align: left;
177
+ }
178
+ .keep-alive h3 { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
179
+ .helper-text { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 16px; }
180
+ .input-group { display: flex; gap: 8px; }
181
+ input {
182
+ flex: 1;
183
+ background: #0f172a;
184
+ border: 1px solid rgba(255,255,255,0.1);
185
+ padding: 12px;
186
+ border-radius: 12px;
187
+ color: white;
188
+ font-family: inherit;
189
+ }
190
+ button#ur-btn {
191
+ background: rgba(255,255,255,0.05);
192
+ border: none;
193
+ color: white;
194
+ padding: 0 16px;
195
+ border-radius: 12px;
196
+ cursor: pointer;
197
+ font-weight: 600;
198
+ font-size: 0.85rem;
199
+ }
200
+ button#ur-btn:hover { background: rgba(255,255,255,0.1); }
201
+ #ur-status { font-size: 0.8rem; margin-top: 8px; }
202
  </style>
203
  </head>
204
  <body>
 
207
  <p class="subtitle">Workflow Automation Space</p>
208
 
209
  <div class="stats">
210
+ <div class="stat-card">
211
+ <div class="stat-label">Uptime</div>
212
+ <div class="stat-value">${data.uptimeHuman}</div>
213
+ </div>
214
+ <div class="stat-card">
215
+ <div class="stat-label">n8n Port</div>
216
+ <div class="stat-value">${TARGET_PORT}</div>
217
+ </div>
218
  </div>
219
 
220
+ <div class="sync-box">
221
+ <div class="sync-header">
222
+ <div class="stat-label">Sync Status</div>
223
+ ${getBadge(data.sync.status)}
 
 
 
 
224
  </div>
225
+ <div class="stat-value" style="font-size: 1rem; margin-bottom: 4px;">Last Activity: ${data.sync.timestamp.split('.')[0]}Z</div>
226
+ <div class="stat-label" style="text-transform: none;">${data.sync.message}</div>
227
  </div>
228
 
229
+ <a href="/app/" target="_blank" class="btn-primary">Open n8n Editor</a>
230
 
231
+ <div class="keep-alive">
232
+ <h3>Keep Alive</h3>
233
  ${keepAwakeHtml}
 
234
  </div>
235
  </div>
236
 
237
  <script>
238
+ async function setupMonitor() {
239
+ const key = document.getElementById('ur-key').value;
240
+ const btn = document.getElementById('ur-btn');
241
+ const status = document.getElementById('ur-status');
242
+ if (!key) return alert('Please enter an API key');
243
+
244
+ btn.disabled = true;
245
+ btn.innerText = 'Setting up...';
246
+
247
  try {
248
+ const res = await fetch('/uptimerobot/setup', {
249
+ method: 'POST',
250
+ body: JSON.stringify({ apiKey: key })
251
+ });
252
+ const data = await res.json();
253
+ status.innerText = data.message;
254
+ status.style.color = res.ok ? '#22c55e' : '#ef4444';
255
+ } catch (e) {
256
+ status.innerText = 'Connection error.';
257
+ status.style.color = '#ef4444';
258
+ } finally {
259
+ btn.disabled = false;
260
+ btn.innerText = 'Set Up Monitor';
261
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  }
263
  </script>
264
  </body>
 
266
  }
267
 
268
  async function resolveSpaceIsPrivate(req) {
269
+ const host = req.headers.host || "";
270
+ const match = host.match(/^([^.]+)-([^.]+)\.hf\.space$/);
271
+ if (!match) return false;
272
+ const user = match[1];
273
+ const space = match[2];
274
+
275
+ const params = new URLSearchParams(req.url.split("?")[1] || "");
276
  const token = params.get("__sign");
277
  if (!token) return false;
278
  try {
 
280
  Buffer.from(token.split(".")[1], "base64").toString(),
281
  );
282
  const sub = payload.sub || "";
283
+ const match_sub = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
284
+ if (!match_sub) return false;
285
  return new Promise((resolve) => {
286
  https
287
  .get(
288
+ `https://huggingface.co/api/spaces/${match_sub[1]}/${match_sub[2]}`,
289
  { headers: { "User-Agent": "Hugging8n" } },
290
  (res) => {
291
  resolve(
 
306
  const url = parseRequestUrl(req.url);
307
  const pathname = url.pathname;
308
 
309
+ // 1. Dashboard Routes
310
  if (pathname === "/health") {
311
  res.writeHead(200, { "Content-Type": "application/json" });
312
  return res.end(JSON.stringify({ status: "ok", ...getStatus() }));
313
  }
 
314
  if (pathname === "/status") {
315
  const uptime = Math.floor((Date.now() - startTime) / 1000);
316
  return res.end(
 
320
  }),
321
  );
322
  }
 
323
  if (pathname === "/uptimerobot/setup" && req.method === "POST") {
324
  let body = "";
325
  req.on("data", (c) => (body += c));
 
388
  });
389
  return;
390
  }
 
391
  if (pathname === "/" || pathname === "/dashboard") {
392
  const uptime = Math.floor((Date.now() - startTime) / 1000);
393
  const isPrivate = await resolveSpaceIsPrivate(req);
 
401
  );
402
  }
403
 
404
+ // 2. n8n Routing Logic (Namespace-based Proxy)
405
+ // These are the prefixes n8n uses. If it matches, we proxy to n8n at ROOT.
406
+ const isN8nPath =
407
+ pathname.startsWith("/static/") ||
408
+ pathname.startsWith("/assets/") ||
409
+ pathname.startsWith("/rest/") ||
410
+ pathname.startsWith("/api/") ||
411
+ pathname.startsWith("/webhook/") ||
412
+ pathname.startsWith("/home/") ||
413
+ pathname.startsWith("/auth/") ||
414
+ pathname.startsWith("/login") ||
415
+ pathname.startsWith("/signup") ||
416
+ pathname.startsWith("/logout") ||
417
+ pathname.startsWith("/nodes/") ||
418
+ pathname.startsWith("/healthz");
419
 
420
+ // Special case: /app/ redirects to /home/workflows
421
+ if (pathname === "/app" || pathname === "/app/") {
422
+ res.writeHead(302, { Location: "/home/workflows" });
 
423
  return res.end();
424
  }
425
 
426
+ if (isN8nPath) {
427
+ const proxyHeaders = {
428
+ ...req.headers,
429
+ host: `127.0.0.1:${TARGET_PORT}`,
430
+ "x-forwarded-for": req.socket.remoteAddress,
431
+ "x-forwarded-host": req.headers.host,
432
+ "x-forwarded-proto": "https",
433
+ };
434
 
435
+ const proxyReq = http.request(
436
+ {
437
+ hostname: TARGET_HOST,
438
+ port: TARGET_PORT,
439
+ path: pathname + url.search,
440
+ method: req.method,
441
+ headers: proxyHeaders,
442
+ },
443
+ (proxyRes) => {
444
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
445
+ proxyRes.pipe(res);
446
+ },
447
+ );
448
 
449
+ proxyReq.on("error", () => {
450
+ res.writeHead(503, { "Content-Type": "application/json" });
451
+ res.end(JSON.stringify({ status: "starting", message: "n8n is initializing..." }));
452
+ });
 
 
 
 
 
 
 
 
 
453
 
454
+ req.pipe(proxyReq);
455
+ return;
456
+ }
 
 
 
 
 
 
457
 
458
+ // 3. Fallback: Redirect anything else to Dashboard
459
+ res.writeHead(302, { Location: "/" });
460
+ res.end();
461
  });
462
 
463
  server.on("upgrade", (req, socket, head) => {
464
  const url = parseRequestUrl(req.url);
465
  const proxyPath = url.pathname;
 
466
  const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
467
  proxySocket.write(
468
  `${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`,
 
478
  });
479
 
480
  server.listen(PORT, "0.0.0.0", () =>
481
+ console.log(`Namespace Proxy on ${PORT} -> n8n on ${TARGET_PORT}`),
482
  );
start.sh CHANGED
@@ -15,12 +15,12 @@ mkdir -p "$N8N_HOME"
15
  SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
16
  if [ -n "$SPACE_HOST_DETECTED" ]; then
17
  export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
18
- # Strict Native Subpath Configuration
19
- export N8N_PATH="/app/"
20
  export N8N_PROTOCOL="https"
21
  export N8N_HOST="${SPACE_HOST_DETECTED}"
22
- export WEBHOOK_URL="https://${SPACE_HOST_DETECTED}/app/"
23
- export N8N_EDITOR_BASE_URL="https://${SPACE_HOST_DETECTED}/app/"
24
  fi
25
 
26
  export N8N_PORT
 
15
  SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
16
  if [ -n "$SPACE_HOST_DETECTED" ]; then
17
  export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
18
+ # Namespace-based Proxy Configuration (n8n at root internally)
19
+ export N8N_PATH="/"
20
  export N8N_PROTOCOL="https"
21
  export N8N_HOST="${SPACE_HOST_DETECTED}"
22
+ export WEBHOOK_URL="https://${SPACE_HOST_DETECTED}/"
23
+ export N8N_EDITOR_BASE_URL="https://${SPACE_HOST_DETECTED}/"
24
  fi
25
 
26
  export N8N_PORT