somratpro commited on
Commit
e110d9d
Β·
0 Parent(s):

initial commit

Browse files
Files changed (7) hide show
  1. Dockerfile +38 -0
  2. LICENSE +21 -0
  3. README.md +57 -0
  4. health-server.js +97 -0
  5. n8n-sync.py +240 -0
  6. setup-uptimerobot.sh +67 -0
  7. start.sh +79 -0
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-slim
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive \
4
+ N8N_PORT=5678 \
5
+ HF_HUB_DISABLE_PROGRESS_BARS=1 \
6
+ PYTHONUNBUFFERED=1
7
+
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ ca-certificates \
10
+ curl \
11
+ git \
12
+ jq \
13
+ python3 \
14
+ python3-pip \
15
+ sqlite3 \
16
+ tini \
17
+ && pip3 install --no-cache-dir --break-system-packages huggingface_hub==0.34.4 \
18
+ && npm install -g n8n@latest \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ RUN mkdir -p /home/node/app /home/node/.n8n && \
22
+ chown -R node:node /home/node
23
+
24
+ WORKDIR /home/node/app
25
+
26
+ COPY --chown=node:node health-server.js /home/node/app/health-server.js
27
+ COPY --chown=node:node n8n-sync.py /home/node/app/n8n-sync.py
28
+ COPY --chown=node:node setup-uptimerobot.sh /home/node/app/setup-uptimerobot.sh
29
+ COPY --chown=node:node start.sh /home/node/app/start.sh
30
+
31
+ RUN chmod +x /home/node/app/start.sh /home/node/app/setup-uptimerobot.sh
32
+
33
+ USER node
34
+
35
+ EXPOSE 7861
36
+
37
+ ENTRYPOINT ["/usr/bin/tini", "--"]
38
+ CMD ["/home/node/app/start.sh"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Somrat Sorkar (@somratpro)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Hugging8n
3
+ emoji: "8"
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7861
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # Hugging8n
13
+
14
+ Run a self-hosted n8n instance on Hugging Face Spaces without external database services.
15
+
16
+ This Space uses:
17
+
18
+ - `n8n` running locally on port `5678`
19
+ - a small proxy/health server on port `7861` for HF Spaces
20
+ - periodic backup of `/home/node/.n8n` to a private Hugging Face Dataset
21
+
22
+ ## Required secret
23
+
24
+ - `HF_TOKEN`: Hugging Face token with write access to create/update a private dataset backup
25
+
26
+ ## Recommended variables
27
+
28
+ - `BACKUP_DATASET_NAME`: backup dataset name. Default: `hugging8n-backup`
29
+ - `SYNC_INTERVAL`: backup interval in seconds. Default: `180`
30
+ - `GENERIC_TIMEZONE`: timezone for schedule triggers. Example: `Asia/Dhaka`
31
+ - `N8N_ENCRYPTION_KEY`: optional explicit encryption key. If omitted, n8n stores it inside `.n8n` and the backup preserves it
32
+ - `N8N_BASIC_AUTH_ACTIVE`: set to `true` if you want built-in basic auth
33
+ - `N8N_BASIC_AUTH_USER`: basic auth username
34
+ - `N8N_BASIC_AUTH_PASSWORD`: basic auth password
35
+
36
+ ## Optional variables
37
+
38
+ - `HF_USERNAME`: owner of the backup dataset. By default this is inferred from `SPACE_AUTHOR_NAME`
39
+ - `SPACE_HOST_OVERRIDE`: set this only if you want to override the detected Space hostname
40
+ - `N8N_DIAGNOSTICS_ENABLED=false`
41
+ - `N8N_PERSONALIZATION_ENABLED=false`
42
+
43
+ ## How persistence works
44
+
45
+ On startup, the Space restores `/home/node/.n8n` from a private dataset repo.
46
+
47
+ While running, it watches for changes and uploads the updated `.n8n` directory back to the dataset. This preserves:
48
+
49
+ - workflows
50
+ - credentials
51
+ - users
52
+ - SQLite database
53
+ - encryption key
54
+
55
+ ## Keep-alive
56
+
57
+ If you already solved the sleep problem in your HF setup, use that. If not, `setup-uptimerobot.sh` can create an external monitor for `https://<your-space>.hf.space/health`.
health-server.js ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const http = require("http");
2
+ const net = require("net");
3
+ const fs = require("fs");
4
+
5
+ const PUBLIC_PORT = Number(process.env.PUBLIC_PORT || 7861);
6
+ const TARGET_PORT = Number(process.env.N8N_PORT || 5678);
7
+ const TARGET_HOST = "127.0.0.1";
8
+ const STATUS_FILE = "/tmp/hugging8n-sync-status.json";
9
+
10
+ function getStatus() {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(STATUS_FILE, "utf8"));
13
+ } catch {
14
+ return {
15
+ status: "unknown",
16
+ message: "No sync status yet",
17
+ timestamp: new Date().toISOString(),
18
+ };
19
+ }
20
+ }
21
+
22
+ function writeJson(res, statusCode, payload) {
23
+ res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
24
+ res.end(JSON.stringify(payload));
25
+ }
26
+
27
+ function buildHeaders(req) {
28
+ return {
29
+ ...req.headers,
30
+ host: req.headers.host || "",
31
+ "x-forwarded-for": req.socket.remoteAddress || "",
32
+ "x-forwarded-host": req.headers.host || "",
33
+ "x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
34
+ };
35
+ }
36
+
37
+ const server = http.createServer((req, res) => {
38
+ if (req.url === "/health") {
39
+ return writeJson(res, 200, {
40
+ ok: true,
41
+ service: "hugging8n",
42
+ n8nPort: TARGET_PORT,
43
+ ...getStatus(),
44
+ });
45
+ }
46
+
47
+ if (req.url === "/status") {
48
+ return writeJson(res, 200, getStatus());
49
+ }
50
+
51
+ const upstream = http.request(
52
+ {
53
+ hostname: TARGET_HOST,
54
+ port: TARGET_PORT,
55
+ path: req.url,
56
+ method: req.method,
57
+ headers: buildHeaders(req),
58
+ },
59
+ (upstreamRes) => {
60
+ res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers);
61
+ upstreamRes.pipe(res);
62
+ },
63
+ );
64
+
65
+ upstream.on("error", (error) => {
66
+ writeJson(res, 502, {
67
+ ok: false,
68
+ error: "upstream_unavailable",
69
+ detail: error.message,
70
+ });
71
+ });
72
+
73
+ req.pipe(upstream);
74
+ });
75
+
76
+ server.on("upgrade", (req, socket, head) => {
77
+ const upstream = net.connect(TARGET_PORT, TARGET_HOST, () => {
78
+ socket.write(
79
+ `${req.method} ${req.url} HTTP/${req.httpVersion}\r\n` +
80
+ Object.entries(buildHeaders(req))
81
+ .map(([key, value]) => `${key}: ${value}`)
82
+ .join("\r\n") +
83
+ "\r\n\r\n",
84
+ );
85
+ if (head && head.length) upstream.write(head);
86
+ upstream.pipe(socket);
87
+ socket.pipe(upstream);
88
+ });
89
+
90
+ upstream.on("error", () => {
91
+ socket.destroy();
92
+ });
93
+ });
94
+
95
+ server.listen(PUBLIC_PORT, "0.0.0.0", () => {
96
+ console.log(`Hugging8n proxy listening on ${PUBLIC_PORT}, forwarding to ${TARGET_PORT}`);
97
+ });
n8n-sync.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import shutil
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import time
12
+ from pathlib import Path
13
+
14
+ os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
15
+
16
+ from huggingface_hub import HfApi, snapshot_download, upload_folder
17
+ from huggingface_hub.errors import RepositoryNotFoundError
18
+
19
+ N8N_HOME = Path(os.environ.get("N8N_USER_FOLDER", "/home/node/.n8n"))
20
+ STATUS_FILE = Path("/tmp/hugging8n-sync-status.json")
21
+ INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
22
+ HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
23
+ HF_USERNAME = (
24
+ os.environ.get("HF_USERNAME", "").strip()
25
+ or os.environ.get("SPACE_AUTHOR_NAME", "").strip()
26
+ )
27
+ BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "hugging8n-backup").strip()
28
+ HF_API = HfApi(token=HF_TOKEN) if HF_TOKEN else None
29
+ RUNNING = True
30
+
31
+
32
+ def write_status(status: str, message: str) -> None:
33
+ payload = {
34
+ "status": status,
35
+ "message": message,
36
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
37
+ }
38
+ STATUS_FILE.write_text(json.dumps(payload), encoding="utf-8")
39
+
40
+
41
+ def dataset_repo_id() -> str:
42
+ if not HF_USERNAME:
43
+ raise RuntimeError("HF_USERNAME or SPACE_AUTHOR_NAME is required for backup repo naming")
44
+ return f"{HF_USERNAME}/{BACKUP_DATASET_NAME}"
45
+
46
+
47
+ def ensure_repo_exists() -> str:
48
+ repo_id = dataset_repo_id()
49
+ try:
50
+ HF_API.repo_info(repo_id=repo_id, repo_type="dataset")
51
+ except RepositoryNotFoundError:
52
+ HF_API.create_repo(repo_id=repo_id, repo_type="dataset", private=True)
53
+ return repo_id
54
+
55
+
56
+ def fingerprint_dir(root: Path) -> str:
57
+ hasher = hashlib.sha256()
58
+ if not root.exists():
59
+ return hasher.hexdigest()
60
+
61
+ for path in sorted(p for p in root.rglob("*") if p.is_file()):
62
+ rel = path.relative_to(root).as_posix()
63
+ if rel.startswith(".cache/"):
64
+ continue
65
+ hasher.update(rel.encode("utf-8"))
66
+ with path.open("rb") as handle:
67
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
68
+ hasher.update(chunk)
69
+ return hasher.hexdigest()
70
+
71
+
72
+ def create_snapshot_dir(source_root: Path) -> Path:
73
+ staging_root = Path(tempfile.mkdtemp(prefix="hugging8n-sync-"))
74
+ database_path = source_root / "database.sqlite"
75
+
76
+ for path in sorted(source_root.rglob("*")):
77
+ rel = path.relative_to(source_root)
78
+ rel_posix = rel.as_posix()
79
+ if rel_posix.startswith(".cache/"):
80
+ continue
81
+ target = staging_root / rel
82
+ if path.is_dir():
83
+ target.mkdir(parents=True, exist_ok=True)
84
+ continue
85
+ if rel_posix in {"database.sqlite", "database.sqlite-shm", "database.sqlite-wal"}:
86
+ continue
87
+ target.parent.mkdir(parents=True, exist_ok=True)
88
+ shutil.copy2(path, target)
89
+
90
+ if database_path.exists():
91
+ target_db = staging_root / "database.sqlite"
92
+ target_db.parent.mkdir(parents=True, exist_ok=True)
93
+ try:
94
+ subprocess.run(
95
+ ["sqlite3", str(database_path), f".backup {target_db}"],
96
+ check=True,
97
+ stdout=subprocess.DEVNULL,
98
+ stderr=subprocess.DEVNULL,
99
+ )
100
+ except Exception:
101
+ shutil.copy2(database_path, target_db)
102
+ for suffix in ("-wal", "-shm"):
103
+ sidecar = source_root / f"database.sqlite{suffix}"
104
+ if sidecar.exists():
105
+ shutil.copy2(sidecar, staging_root / sidecar.name)
106
+
107
+ return staging_root
108
+
109
+
110
+ def restore() -> bool:
111
+ if not HF_TOKEN:
112
+ write_status("disabled", "HF_TOKEN is not configured.")
113
+ return False
114
+
115
+ repo_id = dataset_repo_id()
116
+ write_status("restoring", f"Restoring state from {repo_id}")
117
+
118
+ try:
119
+ with tempfile.TemporaryDirectory() as tmpdir:
120
+ snapshot_download(
121
+ repo_id=repo_id,
122
+ repo_type="dataset",
123
+ token=HF_TOKEN,
124
+ local_dir=tmpdir,
125
+ local_dir_use_symlinks=False,
126
+ )
127
+
128
+ tmp_path = Path(tmpdir)
129
+ if not any(tmp_path.iterdir()):
130
+ write_status("fresh", "Backup dataset is empty. Starting fresh.")
131
+ return True
132
+
133
+ N8N_HOME.mkdir(parents=True, exist_ok=True)
134
+ for child in N8N_HOME.iterdir():
135
+ if child.name == ".cache":
136
+ continue
137
+ if child.is_dir():
138
+ shutil.rmtree(child, ignore_errors=True)
139
+ else:
140
+ child.unlink(missing_ok=True)
141
+
142
+ for child in tmp_path.iterdir():
143
+ if child.name == ".cache":
144
+ continue
145
+ destination = N8N_HOME / child.name
146
+ if child.is_dir():
147
+ shutil.copytree(child, destination)
148
+ else:
149
+ shutil.copy2(child, destination)
150
+
151
+ write_status("restored", f"Restored state from {repo_id}")
152
+ return True
153
+ except RepositoryNotFoundError:
154
+ write_status("fresh", f"Backup dataset {repo_id} does not exist yet.")
155
+ return True
156
+ except Exception as exc:
157
+ write_status("error", f"Restore failed: {exc}")
158
+ print(f"Restore failed: {exc}", file=sys.stderr)
159
+ return False
160
+
161
+
162
+ def sync_once(last_fingerprint: str | None = None) -> str:
163
+ if not HF_TOKEN:
164
+ write_status("disabled", "HF_TOKEN is not configured.")
165
+ return last_fingerprint or ""
166
+
167
+ repo_id = ensure_repo_exists()
168
+ current_fingerprint = fingerprint_dir(N8N_HOME)
169
+ if last_fingerprint is not None and current_fingerprint == last_fingerprint:
170
+ write_status("idle", "No state changes detected.")
171
+ return last_fingerprint
172
+
173
+ write_status("syncing", f"Uploading state to {repo_id}")
174
+ snapshot_dir = create_snapshot_dir(N8N_HOME)
175
+ try:
176
+ upload_folder(
177
+ folder_path=str(snapshot_dir),
178
+ repo_id=repo_id,
179
+ repo_type="dataset",
180
+ token=HF_TOKEN,
181
+ commit_message=f"Hugging8n sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
182
+ ignore_patterns=[".cache/*"],
183
+ )
184
+ finally:
185
+ shutil.rmtree(snapshot_dir, ignore_errors=True)
186
+ write_status("success", f"Uploaded state to {repo_id}")
187
+ return current_fingerprint
188
+
189
+
190
+ def handle_signal(_sig, _frame) -> None:
191
+ global RUNNING
192
+ RUNNING = False
193
+
194
+
195
+ def loop() -> int:
196
+ signal.signal(signal.SIGTERM, handle_signal)
197
+ signal.signal(signal.SIGINT, handle_signal)
198
+
199
+ last_fingerprint = fingerprint_dir(N8N_HOME)
200
+ write_status("configured", f"Backup loop active with {INTERVAL}s interval.")
201
+
202
+ while RUNNING:
203
+ try:
204
+ last_fingerprint = sync_once(last_fingerprint)
205
+ except Exception as exc:
206
+ write_status("error", f"Sync failed: {exc}")
207
+ print(f"Sync failed: {exc}", file=sys.stderr)
208
+ for _ in range(INTERVAL):
209
+ if not RUNNING:
210
+ break
211
+ time.sleep(1)
212
+
213
+ try:
214
+ sync_once(None)
215
+ except Exception as exc:
216
+ write_status("error", f"Final sync failed: {exc}")
217
+ print(f"Final sync failed: {exc}", file=sys.stderr)
218
+ return 0
219
+
220
+
221
+ def main() -> int:
222
+ if len(sys.argv) < 2:
223
+ print("Usage: n8n-sync.py [restore|sync-once|loop]", file=sys.stderr)
224
+ return 1
225
+
226
+ command = sys.argv[1]
227
+ if command == "restore":
228
+ return 0 if restore() else 1
229
+ if command == "sync-once":
230
+ sync_once(None)
231
+ return 0
232
+ if command == "loop":
233
+ return loop()
234
+
235
+ print(f"Unknown command: {command}", file=sys.stderr)
236
+ return 1
237
+
238
+
239
+ if __name__ == "__main__":
240
+ raise SystemExit(main())
setup-uptimerobot.sh ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
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."
10
+ exit 1
11
+ fi
12
+
13
+ if [ -z "$SPACE_HOST_INPUT" ]; then
14
+ echo "Missing Space host."
15
+ echo "Usage: UPTIMEROBOT_API_KEY=... ./setup-uptimerobot.sh your-space.hf.space"
16
+ exit 1
17
+ fi
18
+
19
+ SPACE_HOST_CLEAN="${SPACE_HOST_INPUT#https://}"
20
+ SPACE_HOST_CLEAN="${SPACE_HOST_CLEAN#http://}"
21
+ 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}" \
29
+ -d "format=json" \
30
+ -d "logs=0" \
31
+ -d "response_times=0" \
32
+ -d "response_times_limit=1")
33
+
34
+ MONITOR_ID=$(printf '%s' "$MONITORS_RESPONSE" | jq -r --arg url "$MONITOR_URL" '
35
+ (.monitors // []) | map(select(.url == $url)) | first | .id // empty
36
+ ')
37
+
38
+ if [ -n "$MONITOR_ID" ]; then
39
+ echo "Monitor already exists (id=${MONITOR_ID}) for ${MONITOR_URL}"
40
+ exit 0
41
+ fi
42
+
43
+ CURL_ARGS=(
44
+ -sS
45
+ -X POST "${API_URL}/newMonitor"
46
+ -d "api_key=${API_KEY}"
47
+ -d "format=json"
48
+ -d "type=1"
49
+ -d "friendly_name=${MONITOR_NAME}"
50
+ -d "url=${MONITOR_URL}"
51
+ -d "interval=${INTERVAL}"
52
+ )
53
+
54
+ if [ -n "${UPTIMEROBOT_ALERT_CONTACTS:-}" ]; then
55
+ CURL_ARGS+=(-d "alert_contacts=${UPTIMEROBOT_ALERT_CONTACTS}")
56
+ fi
57
+
58
+ 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}"
start.sh ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ APP_DIR="/home/node/app"
5
+ N8N_HOME="/home/node/.n8n"
6
+ N8N_PORT="${N8N_PORT:-5678}"
7
+ PUBLIC_PORT="${PUBLIC_PORT:-7861}"
8
+ SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
9
+
10
+ mkdir -p "$N8N_HOME"
11
+
12
+ SPACE_HOST_DETECTED="${SPACE_HOST_OVERRIDE:-${SPACE_HOST:-}}"
13
+ if [ -n "$SPACE_HOST_DETECTED" ]; then
14
+ export N8N_HOST="${N8N_HOST:-$SPACE_HOST_DETECTED}"
15
+ export WEBHOOK_URL="${WEBHOOK_URL:-https://${SPACE_HOST_DETECTED}/}"
16
+ export N8N_EDITOR_BASE_URL="${N8N_EDITOR_BASE_URL:-https://${SPACE_HOST_DETECTED}/}"
17
+ fi
18
+
19
+ export N8N_PORT
20
+ export N8N_PROTOCOL="${N8N_PROTOCOL:-https}"
21
+ export N8N_PROXY_HOPS="${N8N_PROXY_HOPS:-1}"
22
+ export N8N_LISTEN_ADDRESS="${N8N_LISTEN_ADDRESS:-0.0.0.0}"
23
+ export N8N_SECURE_COOKIE="${N8N_SECURE_COOKIE:-true}"
24
+ export N8N_DIAGNOSTICS_ENABLED="${N8N_DIAGNOSTICS_ENABLED:-false}"
25
+ export N8N_PERSONALIZATION_ENABLED="${N8N_PERSONALIZATION_ENABLED:-false}"
26
+ export N8N_USER_FOLDER="$N8N_HOME"
27
+ export N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS="${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS:-true}"
28
+ export GENERIC_TIMEZONE="${GENERIC_TIMEZONE:-${TZ:-UTC}}"
29
+ export TZ="${TZ:-$GENERIC_TIMEZONE}"
30
+
31
+ echo ""
32
+ echo " ╔════════════════════════════════════╗"
33
+ echo " β•‘ Hugging8n β•‘"
34
+ echo " β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•"
35
+ echo ""
36
+ echo "Public host : ${SPACE_HOST_DETECTED:-not detected}"
37
+ echo "n8n port : ${N8N_PORT}"
38
+ echo "Public port : ${PUBLIC_PORT}"
39
+ echo "Timezone : ${GENERIC_TIMEZONE}"
40
+ echo "Sync every : ${SYNC_INTERVAL}s"
41
+
42
+ if [ -n "${HF_TOKEN:-}" ]; then
43
+ echo "Restoring persisted n8n state from HF Dataset..."
44
+ python3 "$APP_DIR/n8n-sync.py" restore || true
45
+ else
46
+ echo "HF_TOKEN is not set. Running without dataset persistence."
47
+ fi
48
+
49
+ cleanup() {
50
+ echo "Stopping Hugging8n..."
51
+ if [ -n "${SYNC_PID:-}" ] && kill -0 "$SYNC_PID" 2>/dev/null; then
52
+ kill "$SYNC_PID" 2>/dev/null || true
53
+ fi
54
+ if [ -n "${N8N_PID:-}" ] && kill -0 "$N8N_PID" 2>/dev/null; then
55
+ kill "$N8N_PID" 2>/dev/null || true
56
+ fi
57
+ if [ -n "${PROXY_PID:-}" ] && kill -0 "$PROXY_PID" 2>/dev/null; then
58
+ kill "$PROXY_PID" 2>/dev/null || true
59
+ fi
60
+ if [ -n "${HF_TOKEN:-}" ]; then
61
+ echo "Running final backup pass..."
62
+ python3 "$APP_DIR/n8n-sync.py" sync-once || true
63
+ fi
64
+ }
65
+
66
+ trap cleanup EXIT INT TERM
67
+
68
+ if [ -n "${HF_TOKEN:-}" ]; then
69
+ python3 "$APP_DIR/n8n-sync.py" loop &
70
+ SYNC_PID=$!
71
+ fi
72
+
73
+ node "$APP_DIR/health-server.js" &
74
+ PROXY_PID=$!
75
+
76
+ n8n start &
77
+ N8N_PID=$!
78
+
79
+ wait "$N8N_PID"