somratpro commited on
Commit
15e3b0d
·
1 Parent(s): 32b834b

refactor: standardize code formatting and update project emoji to link icon

Browse files
Files changed (3) hide show
  1. CONTRIBUTING.md +6 -1
  2. README.md +3 -2
  3. health-server.js +133 -42
CONTRIBUTING.md CHANGED
@@ -1,19 +1,22 @@
1
  # Contributing to Hugging8n
2
 
3
- Thanks for your interest in contributing! ♾️
4
 
5
  ## How to Contribute
6
 
7
  ### Bug Reports
 
8
  - Open an issue with a clear description
9
  - Include your HF Space logs if possible
10
  - Mention the n8n version you're using (check Space logs on startup)
11
 
12
  ### Feature Requests
 
13
  - Open an issue with the `enhancement` label
14
  - Describe the use case — why is this needed?
15
 
16
  ### Pull Requests
 
17
  1. Fork the repo
18
  2. Create a feature branch: `git checkout -b feature/my-feature`
19
  3. Make your changes
@@ -22,11 +25,13 @@ Thanks for your interest in contributing! ♾️
22
  6. Push and open a PR
23
 
24
  ### Code Style
 
25
  - Shell scripts: use `set -e`, quote variables, comment non-obvious logic
26
  - Keep it simple — this project should stay easy to understand
27
  - No unnecessary dependencies
28
 
29
  ### Testing
 
30
  - Test with and without `HF_TOKEN` (backup enabled and disabled)
31
  - Test with and without `N8N_BASIC_AUTH_ACTIVE`
32
  - Verify the `/health` endpoint responds correctly
 
1
  # Contributing to Hugging8n
2
 
3
+ Thanks for your interest in contributing! 🔗
4
 
5
  ## How to Contribute
6
 
7
  ### Bug Reports
8
+
9
  - Open an issue with a clear description
10
  - Include your HF Space logs if possible
11
  - Mention the n8n version you're using (check Space logs on startup)
12
 
13
  ### Feature Requests
14
+
15
  - Open an issue with the `enhancement` label
16
  - Describe the use case — why is this needed?
17
 
18
  ### Pull Requests
19
+
20
  1. Fork the repo
21
  2. Create a feature branch: `git checkout -b feature/my-feature`
22
  3. Make your changes
 
25
  6. Push and open a PR
26
 
27
  ### Code Style
28
+
29
  - Shell scripts: use `set -e`, quote variables, comment non-obvious logic
30
  - Keep it simple — this project should stay easy to understand
31
  - No unnecessary dependencies
32
 
33
  ### Testing
34
+
35
  - Test with and without `HF_TOKEN` (backup enabled and disabled)
36
  - Test with and without `N8N_BASIC_AUTH_ACTIVE`
37
  - Verify the `/health` endpoint responds correctly
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Hugging8n
3
- emoji: ♾️
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
@@ -12,7 +12,7 @@ secrets:
12
  description: HuggingFace token with write access. Used for automatic backup.
13
  ---
14
 
15
- # ♾️ Hugging8n
16
 
17
  **Self-hosted n8n workflow automation — free, no server needed.** Hugging8n runs [n8n](https://n8n.io) on HuggingFace Spaces Docker, serving a premium dashboard at `/` and the n8n editor at `/app/`.
18
 
@@ -42,6 +42,7 @@ When the Space starts, visit the URL and click **Open n8n Editor**. On the first
42
  ## 🔐 Authentication
43
 
44
  Hugging8n uses n8n's native user management.
 
45
  1. The first person to visit `/app/` on a fresh install becomes the owner.
46
  2. If you are restoring from a backup, your existing user accounts will be active.
47
 
 
1
  ---
2
  title: Hugging8n
3
+ emoji: 🔗
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: docker
 
12
  description: HuggingFace token with write access. Used for automatic backup.
13
  ---
14
 
15
+ # 🔗 Hugging8n
16
 
17
  **Self-hosted n8n workflow automation — free, no server needed.** Hugging8n runs [n8n](https://n8n.io) on HuggingFace Spaces Docker, serving a premium dashboard at `/` and the n8n editor at `/app/`.
18
 
 
42
  ## 🔐 Authentication
43
 
44
  Hugging8n uses n8n's native user management.
45
+
46
  1. The first person to visit `/app/` on a fresh install becomes the owner.
47
  2. If you are restoring from a backup, your existing user accounts will be active.
48
 
health-server.js CHANGED
@@ -24,18 +24,27 @@ function getStatus() {
24
  return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
25
  }
26
  } catch {}
27
- return { status: "unknown", message: "Initial startup...", timestamp: new Date().toISOString() };
 
 
 
 
28
  }
29
 
30
  function renderDashboard(data) {
31
  const syncBadge = (status) => {
32
  let cls = "status-offline";
33
- if (status === "success" || status === "configured" || status === "restored") cls = "status-online";
 
 
 
 
 
34
  if (status === "syncing" || status === "restoring") cls = "status-syncing";
35
- return `<div class="status-badge ${cls}">${cls === 'status-online' ? '<div class="pulse"></div>' : ''}${String(status).toUpperCase()}</div>`;
36
  };
37
 
38
- const keepAwakeHtml = data.isPrivate
39
  ? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
40
  : `
41
  <div id="uptimerobot-flow">
@@ -87,7 +96,7 @@ function renderDashboard(data) {
87
  </head>
88
  <body>
89
  <div class="dashboard">
90
- <h1>♾️ Hugging8n</h1>
91
  <p class="subtitle">Workflow Automation Space</p>
92
 
93
  <div class="stats">
@@ -163,16 +172,30 @@ async function resolveSpaceIsPrivate(req) {
163
  const token = params.get("__sign");
164
  if (!token) return false;
165
  try {
166
- const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
 
 
167
  const sub = payload.sub || "";
168
  const match = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
169
  if (!match) return false;
170
  return new Promise((resolve) => {
171
- https.get(`https://huggingface.co/api/spaces/${match[1]}/${match[2]}`, { headers: { "User-Agent": "Hugging8n" } }, (res) => {
172
- resolve(res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404);
173
- }).on("error", () => resolve(false));
 
 
 
 
 
 
 
 
 
 
174
  });
175
- } catch { return false; }
 
 
176
  }
177
 
178
  const server = http.createServer(async (req, res) => {
@@ -186,36 +209,79 @@ const server = http.createServer(async (req, res) => {
186
 
187
  if (pathname === "/status") {
188
  const uptime = Math.floor((Date.now() - startTime) / 1000);
189
- return res.end(JSON.stringify({
190
- uptime: `${Math.floor(uptime/3600)}h ${Math.floor((uptime%3600)/60)}m`,
191
- sync: getStatus()
192
- }));
 
 
193
  }
194
 
195
  if (pathname === "/uptimerobot/setup" && req.method === "POST") {
196
  let body = "";
197
- req.on("data", c => body += c);
198
  req.on("end", async () => {
199
  try {
200
  const { apiKey } = JSON.parse(body);
201
  const host = req.headers.host;
202
  const monitorUrl = `https://${host}/health`;
203
- const post = (p, d) => new Promise((res, rej) => {
204
- const r = https.request({ hostname: "api.uptimerobot.com", port: 443, path: p, method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" } }, r => {
205
- let b = ""; r.on("data", c => b += c); r.on("end", () => res(JSON.parse(b)));
206
- }); r.on("error", rej); r.write(new URLSearchParams(d).toString()); r.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  });
208
-
209
- const existing = await post("/v2/getMonitors", { api_key: apiKey, format: "json" });
210
- if (existing.monitors?.some(m => m.url === monitorUrl)) {
211
- res.writeHead(200); return res.end(JSON.stringify({ message: "Monitor already exists." }));
 
212
  }
213
- const created = await post("/v2/newMonitor", { api_key: apiKey, format: "json", type: "1", friendly_name: `Hugging8n ${host}`, url: monitorUrl, interval: "300" });
 
 
 
 
 
 
 
214
  if (created.stat === "ok") {
215
- res.writeHead(200); return res.end(JSON.stringify({ message: "Monitor created successfully!" }));
 
 
 
216
  }
217
- res.writeHead(400); res.end(JSON.stringify({ message: created.error?.message || "Failed to create monitor." }));
218
- } catch (e) { res.writeHead(400); res.end(JSON.stringify({ message: "Invalid request." })); }
 
 
 
 
 
 
 
 
219
  });
220
  return;
221
  }
@@ -224,25 +290,46 @@ const server = http.createServer(async (req, res) => {
224
  const uptime = Math.floor((Date.now() - startTime) / 1000);
225
  const isPrivate = await resolveSpaceIsPrivate(req);
226
  res.writeHead(200, { "Content-Type": "text/html" });
227
- return res.end(renderDashboard({
228
- uptimeHuman: `${Math.floor(uptime/3600)}h ${Math.floor((uptime%3600)/60)}m`,
229
- sync: getStatus(),
230
- isPrivate
231
- }));
 
 
232
  }
233
 
234
  // Proxy to n8n (pass full path as n8n is configured with N8N_PATH=/app/)
235
  const proxyPath = pathname;
236
- const proxyHeaders = { ...req.headers, host: `127.0.0.1:${TARGET_PORT}`, "x-forwarded-for": req.socket.remoteAddress, "x-forwarded-proto": "https" };
237
-
238
- const proxyReq = http.request({ hostname: TARGET_HOST, port: TARGET_PORT, path: proxyPath + url.search, method: req.method, headers: proxyHeaders }, (proxyRes) => {
239
- res.writeHead(proxyRes.statusCode, proxyRes.headers);
240
- proxyRes.pipe(res);
241
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  proxyReq.on("error", () => {
244
  res.writeHead(503, { "Content-Type": "application/json" });
245
- res.end(JSON.stringify({ status: "starting", message: "n8n is initializing, please wait..." }));
 
 
 
 
 
246
  });
247
 
248
  req.pipe(proxyReq);
@@ -252,9 +339,11 @@ server.on("upgrade", (req, socket, head) => {
252
  const url = parseRequestUrl(req.url);
253
  const proxyPath = url.pathname;
254
  const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
255
- proxySocket.write(`${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`);
 
 
256
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
257
- proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
258
  }
259
  proxySocket.write("\r\n");
260
  if (head && head.length) proxySocket.write(head);
@@ -263,4 +352,6 @@ server.on("upgrade", (req, socket, head) => {
263
  proxySocket.on("error", () => socket.destroy());
264
  });
265
 
266
- server.listen(PORT, "0.0.0.0", () => console.log(`Dashboard/Proxy on ${PORT} -> n8n on ${TARGET_PORT}`));
 
 
 
24
  return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
25
  }
26
  } catch {}
27
+ return {
28
+ status: "unknown",
29
+ message: "Initial startup...",
30
+ timestamp: new Date().toISOString(),
31
+ };
32
  }
33
 
34
  function renderDashboard(data) {
35
  const syncBadge = (status) => {
36
  let cls = "status-offline";
37
+ if (
38
+ status === "success" ||
39
+ status === "configured" ||
40
+ status === "restored"
41
+ )
42
+ cls = "status-online";
43
  if (status === "syncing" || status === "restoring") cls = "status-syncing";
44
+ return `<div class="status-badge ${cls}">${cls === "status-online" ? '<div class="pulse"></div>' : ""}${String(status).toUpperCase()}</div>`;
45
  };
46
 
47
+ const keepAwakeHtml = data.isPrivate
48
  ? `<div class="helper-summary"><strong>Private Space detected.</strong> External monitors cannot access private health URLs.</div>`
49
  : `
50
  <div id="uptimerobot-flow">
 
96
  </head>
97
  <body>
98
  <div class="dashboard">
99
+ <h1>🔗 Hugging8n</h1>
100
  <p class="subtitle">Workflow Automation Space</p>
101
 
102
  <div class="stats">
 
172
  const token = params.get("__sign");
173
  if (!token) return false;
174
  try {
175
+ const payload = JSON.parse(
176
+ Buffer.from(token.split(".")[1], "base64").toString(),
177
+ );
178
  const sub = payload.sub || "";
179
  const match = sub.match(/^\/spaces\/([^/]+)\/([^/]+)$/);
180
  if (!match) return false;
181
  return new Promise((resolve) => {
182
+ https
183
+ .get(
184
+ `https://huggingface.co/api/spaces/${match[1]}/${match[2]}`,
185
+ { headers: { "User-Agent": "Hugging8n" } },
186
+ (res) => {
187
+ resolve(
188
+ res.statusCode === 401 ||
189
+ res.statusCode === 403 ||
190
+ res.statusCode === 404,
191
+ );
192
+ },
193
+ )
194
+ .on("error", () => resolve(false));
195
  });
196
+ } catch {
197
+ return false;
198
+ }
199
  }
200
 
201
  const server = http.createServer(async (req, res) => {
 
209
 
210
  if (pathname === "/status") {
211
  const uptime = Math.floor((Date.now() - startTime) / 1000);
212
+ return res.end(
213
+ JSON.stringify({
214
+ uptime: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
215
+ sync: getStatus(),
216
+ }),
217
+ );
218
  }
219
 
220
  if (pathname === "/uptimerobot/setup" && req.method === "POST") {
221
  let body = "";
222
+ req.on("data", (c) => (body += c));
223
  req.on("end", async () => {
224
  try {
225
  const { apiKey } = JSON.parse(body);
226
  const host = req.headers.host;
227
  const monitorUrl = `https://${host}/health`;
228
+ const post = (p, d) =>
229
+ new Promise((res, rej) => {
230
+ const r = https.request(
231
+ {
232
+ hostname: "api.uptimerobot.com",
233
+ port: 443,
234
+ path: p,
235
+ method: "POST",
236
+ headers: {
237
+ "Content-Type": "application/x-www-form-urlencoded",
238
+ },
239
+ },
240
+ (r) => {
241
+ let b = "";
242
+ r.on("data", (c) => (b += c));
243
+ r.on("end", () => res(JSON.parse(b)));
244
+ },
245
+ );
246
+ r.on("error", rej);
247
+ r.write(new URLSearchParams(d).toString());
248
+ r.end();
249
+ });
250
+
251
+ const existing = await post("/v2/getMonitors", {
252
+ api_key: apiKey,
253
+ format: "json",
254
  });
255
+ if (existing.monitors?.some((m) => m.url === monitorUrl)) {
256
+ res.writeHead(200);
257
+ return res.end(
258
+ JSON.stringify({ message: "Monitor already exists." }),
259
+ );
260
  }
261
+ const created = await post("/v2/newMonitor", {
262
+ api_key: apiKey,
263
+ format: "json",
264
+ type: "1",
265
+ friendly_name: `Hugging8n ${host}`,
266
+ url: monitorUrl,
267
+ interval: "300",
268
+ });
269
  if (created.stat === "ok") {
270
+ res.writeHead(200);
271
+ return res.end(
272
+ JSON.stringify({ message: "Monitor created successfully!" }),
273
+ );
274
  }
275
+ res.writeHead(400);
276
+ res.end(
277
+ JSON.stringify({
278
+ message: created.error?.message || "Failed to create monitor.",
279
+ }),
280
+ );
281
+ } catch (e) {
282
+ res.writeHead(400);
283
+ res.end(JSON.stringify({ message: "Invalid request." }));
284
+ }
285
  });
286
  return;
287
  }
 
290
  const uptime = Math.floor((Date.now() - startTime) / 1000);
291
  const isPrivate = await resolveSpaceIsPrivate(req);
292
  res.writeHead(200, { "Content-Type": "text/html" });
293
+ return res.end(
294
+ renderDashboard({
295
+ uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
296
+ sync: getStatus(),
297
+ isPrivate,
298
+ }),
299
+ );
300
  }
301
 
302
  // Proxy to n8n (pass full path as n8n is configured with N8N_PATH=/app/)
303
  const proxyPath = pathname;
304
+ const proxyHeaders = {
305
+ ...req.headers,
306
+ host: `127.0.0.1:${TARGET_PORT}`,
307
+ "x-forwarded-for": req.socket.remoteAddress,
308
+ "x-forwarded-proto": "https",
309
+ };
310
+
311
+ const proxyReq = http.request(
312
+ {
313
+ hostname: TARGET_HOST,
314
+ port: TARGET_PORT,
315
+ path: proxyPath + url.search,
316
+ method: req.method,
317
+ headers: proxyHeaders,
318
+ },
319
+ (proxyRes) => {
320
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
321
+ proxyRes.pipe(res);
322
+ },
323
+ );
324
 
325
  proxyReq.on("error", () => {
326
  res.writeHead(503, { "Content-Type": "application/json" });
327
+ res.end(
328
+ JSON.stringify({
329
+ status: "starting",
330
+ message: "n8n is initializing, please wait...",
331
+ }),
332
+ );
333
  });
334
 
335
  req.pipe(proxyReq);
 
339
  const url = parseRequestUrl(req.url);
340
  const proxyPath = url.pathname;
341
  const proxySocket = net.connect(TARGET_PORT, TARGET_HOST, () => {
342
+ proxySocket.write(
343
+ `${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`,
344
+ );
345
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
346
+ proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`);
347
  }
348
  proxySocket.write("\r\n");
349
  if (head && head.length) proxySocket.write(head);
 
352
  proxySocket.on("error", () => socket.destroy());
353
  });
354
 
355
+ server.listen(PORT, "0.0.0.0", () =>
356
+ console.log(`Dashboard/Proxy on ${PORT} -> n8n on ${TARGET_PORT}`),
357
+ );