Spaces:
Running
Running
refactor: migrate dashboard to /dashboard and implement internal reverse proxy for gateway traffic
Browse files- Dockerfile +3 -1
- README.md +6 -5
- health-server.js +211 -64
- keep-alive.sh +2 -2
- start.sh +7 -2
Dockerfile
CHANGED
|
@@ -35,8 +35,10 @@ RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw
|
|
| 35 |
# Copy HuggingClaw files
|
| 36 |
COPY --chown=1000:1000 dns-fix.js /opt/dns-fix.js
|
| 37 |
COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
|
|
|
|
| 38 |
COPY --chown=1000:1000 start.sh /home/node/app/start.sh
|
| 39 |
COPY --chown=1000:1000 keep-alive.sh /home/node/app/keep-alive.sh
|
|
|
|
| 40 |
COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
|
| 41 |
RUN chmod +x /home/node/app/start.sh /home/node/app/keep-alive.sh
|
| 42 |
|
|
@@ -48,6 +50,6 @@ ENV HOME=/home/node \
|
|
| 48 |
|
| 49 |
WORKDIR /home/node/app
|
| 50 |
|
| 51 |
-
EXPOSE
|
| 52 |
|
| 53 |
CMD ["/home/node/app/start.sh"]
|
|
|
|
| 35 |
# Copy HuggingClaw files
|
| 36 |
COPY --chown=1000:1000 dns-fix.js /opt/dns-fix.js
|
| 37 |
COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
|
| 38 |
+
COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
|
| 39 |
COPY --chown=1000:1000 start.sh /home/node/app/start.sh
|
| 40 |
COPY --chown=1000:1000 keep-alive.sh /home/node/app/keep-alive.sh
|
| 41 |
+
COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
|
| 42 |
COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
|
| 43 |
RUN chmod +x /home/node/app/start.sh /home/node/app/keep-alive.sh
|
| 44 |
|
|
|
|
| 50 |
|
| 51 |
WORKDIR /home/node/app
|
| 52 |
|
| 53 |
+
EXPOSE 7861
|
| 54 |
|
| 55 |
CMD ["/home/node/app/start.sh"]
|
README.md
CHANGED
|
@@ -4,7 +4,8 @@ emoji: π¦
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
-
app_port:
|
|
|
|
| 8 |
pinned: true
|
| 9 |
license: mit
|
| 10 |
---
|
|
@@ -95,7 +96,7 @@ After restarting, the bot should appear online on Telegram.
|
|
| 95 |
|
| 96 |
To use WhatsApp:
|
| 97 |
|
| 98 |
-
1. Visit your Space
|
| 99 |
2. In the Control UI, go to **Channels** β **WhatsApp** β **Login**.
|
| 100 |
3. Scan the QR code with your phone. π±
|
| 101 |
|
|
@@ -110,7 +111,7 @@ Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose t
|
|
| 110 |
|
| 111 |
## π Dashboard & Monitoring
|
| 112 |
|
| 113 |
-
HuggingClaw now features a
|
| 114 |
|
| 115 |
- **Uptime Tracking:** Real-time uptime monitoring.
|
| 116 |
- **Sync Status:** Visual indicators for workspace backup operations.
|
|
@@ -165,7 +166,7 @@ See `.env.example` for complete settings. Key environment variables:
|
|
| 165 |
| Variable | Default | Description |
|
| 166 |
|--------------------|----------|-------------------------------------|
|
| 167 |
| `OPENCLAW_VERSION` | `latest` | Pin a specific OpenClaw version |
|
| 168 |
-
| `HEALTH_PORT` | `7861` |
|
| 169 |
|
| 170 |
## π€ LLM Providers
|
| 171 |
|
|
@@ -226,7 +227,7 @@ cp .env.example .env
|
|
| 226 |
|
| 227 |
```bash
|
| 228 |
docker build -t huggingclaw .
|
| 229 |
-
docker run -p
|
| 230 |
```
|
| 231 |
|
| 232 |
**Without Docker:**
|
|
|
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7861
|
| 8 |
+
base_path: /dashboard
|
| 9 |
pinned: true
|
| 10 |
license: mit
|
| 11 |
---
|
|
|
|
| 96 |
|
| 97 |
To use WhatsApp:
|
| 98 |
|
| 99 |
+
1. Visit your Space URL. It opens the dashboard at `/dashboard` by default, then click **Open Control UI**.
|
| 100 |
2. In the Control UI, go to **Channels** β **WhatsApp** β **Login**.
|
| 101 |
3. Scan the QR code with your phone. π±
|
| 102 |
|
|
|
|
| 111 |
|
| 112 |
## π Dashboard & Monitoring
|
| 113 |
|
| 114 |
+
HuggingClaw now features a built-in dashboard at `/dashboard`, served from the same public HF Space URL as the Control UI:
|
| 115 |
|
| 116 |
- **Uptime Tracking:** Real-time uptime monitoring.
|
| 117 |
- **Sync Status:** Visual indicators for workspace backup operations.
|
|
|
|
| 166 |
| Variable | Default | Description |
|
| 167 |
|--------------------|----------|-------------------------------------|
|
| 168 |
| `OPENCLAW_VERSION` | `latest` | Pin a specific OpenClaw version |
|
| 169 |
+
| `HEALTH_PORT` | `7861` | Public dashboard / proxy port on HF Spaces |
|
| 170 |
|
| 171 |
## π€ LLM Providers
|
| 172 |
|
|
|
|
| 227 |
|
| 228 |
```bash
|
| 229 |
docker build -t huggingclaw .
|
| 230 |
+
docker run -p 7861:7861 --env-file .env huggingclaw
|
| 231 |
```
|
| 232 |
|
| 233 |
**Without Docker:**
|
health-server.js
CHANGED
|
@@ -1,57 +1,67 @@
|
|
| 1 |
-
//
|
| 2 |
const http = require("http");
|
| 3 |
const fs = require("fs");
|
|
|
|
| 4 |
|
| 5 |
-
const PORT = process.env.HEALTH_PORT || 7861;
|
|
|
|
|
|
|
| 6 |
const startTime = Date.now();
|
| 7 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
| 8 |
-
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
|
| 9 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
res.writeHead(200, { "Content-Type": "application/json" });
|
| 17 |
-
res.end(
|
| 18 |
-
JSON.stringify({
|
| 19 |
-
status: "ok",
|
| 20 |
-
uptime: uptime,
|
| 21 |
-
uptimeHuman: uptimeHuman,
|
| 22 |
-
timestamp: new Date().toISOString(),
|
| 23 |
-
}),
|
| 24 |
-
);
|
| 25 |
-
return;
|
| 26 |
}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
<!DOCTYPE html>
|
| 56 |
<html lang="en">
|
| 57 |
<head>
|
|
@@ -72,7 +82,7 @@ const server = http.createServer((req, res) => {
|
|
| 72 |
}
|
| 73 |
|
| 74 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 75 |
-
|
| 76 |
body {
|
| 77 |
font-family: 'Outfit', sans-serif;
|
| 78 |
background-color: var(--bg);
|
|
@@ -82,7 +92,7 @@ const server = http.createServer((req, res) => {
|
|
| 82 |
align-items: center;
|
| 83 |
min-height: 100vh;
|
| 84 |
overflow: hidden;
|
| 85 |
-
background-image:
|
| 86 |
radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
|
| 87 |
radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
|
| 88 |
}
|
|
@@ -224,14 +234,13 @@ const server = http.createServer((req, res) => {
|
|
| 224 |
}
|
| 225 |
|
| 226 |
#sync-msg { color: var(--text); display: block; margin-top: 4px; }
|
| 227 |
-
|
| 228 |
</style>
|
| 229 |
</head>
|
| 230 |
<body>
|
| 231 |
<div class="dashboard">
|
| 232 |
<header>
|
| 233 |
<h1>π¦ HuggingClaw</h1>
|
| 234 |
-
<p class="subtitle">Space
|
| 235 |
</header>
|
| 236 |
|
| 237 |
<div class="stats-grid">
|
|
@@ -251,7 +260,7 @@ const server = http.createServer((req, res) => {
|
|
| 251 |
<span class="stat-label">Telegram</span>
|
| 252 |
<span id="tg-status">Loading...</span>
|
| 253 |
</div>
|
| 254 |
-
<a href="/" class="stat-btn">
|
| 255 |
</div>
|
| 256 |
|
| 257 |
<div class="stat-card" style="width: 100%;">
|
|
@@ -264,7 +273,7 @@ const server = http.createServer((req, res) => {
|
|
| 264 |
</div>
|
| 265 |
|
| 266 |
<div class="footer">
|
| 267 |
-
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
|
|
@@ -276,12 +285,12 @@ const server = http.createServer((req, res) => {
|
|
| 276 |
|
| 277 |
document.getElementById('model-id').textContent = data.model;
|
| 278 |
document.getElementById('uptime').textContent = data.uptime;
|
| 279 |
-
|
| 280 |
-
document.getElementById('wa-status').innerHTML = data.whatsapp
|
| 281 |
? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
|
| 282 |
: '<div class="status-badge status-offline">Disabled</div>';
|
| 283 |
|
| 284 |
-
document.getElementById('tg-status').innerHTML = data.telegram
|
| 285 |
? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
|
| 286 |
: '<div class="status-badge status-offline">Disabled</div>';
|
| 287 |
|
|
@@ -295,16 +304,13 @@ const server = http.createServer((req, res) => {
|
|
| 295 |
} else if (syncData.status === 'syncing') {
|
| 296 |
badgeClass = 'status-syncing';
|
| 297 |
pulseHtml = '<div class="pulse" style="background:#3b82f6"></div>';
|
| 298 |
-
} else if (syncData.status === 'error') {
|
| 299 |
-
badgeClass = 'status-offline';
|
| 300 |
}
|
| 301 |
|
| 302 |
-
document.getElementById('sync-badge-container').innerHTML =
|
| 303 |
'<div class="status-badge ' + badgeClass + '">' + pulseHtml + syncData.status.toUpperCase() + '</div>';
|
| 304 |
-
|
| 305 |
document.getElementById('sync-time').textContent = syncData.timestamp || 'Never';
|
| 306 |
document.getElementById('sync-msg').textContent = syncData.message || 'Waiting for first sync...';
|
| 307 |
-
|
| 308 |
} catch (e) {
|
| 309 |
console.error("Failed to fetch status", e);
|
| 310 |
}
|
|
@@ -315,14 +321,155 @@ const server = http.createServer((req, res) => {
|
|
| 315 |
</script>
|
| 316 |
</body>
|
| 317 |
</html>
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
return;
|
| 320 |
}
|
| 321 |
|
| 322 |
-
|
| 323 |
-
res.end();
|
| 324 |
});
|
| 325 |
|
| 326 |
server.listen(PORT, "0.0.0.0", () => {
|
| 327 |
-
console.log(
|
|
|
|
|
|
|
| 328 |
});
|
|
|
|
| 1 |
+
// Single public entrypoint for HF Spaces: local dashboard + reverse proxy to OpenClaw.
|
| 2 |
const http = require("http");
|
| 3 |
const fs = require("fs");
|
| 4 |
+
const net = require("net");
|
| 5 |
|
| 6 |
+
const PORT = Number(process.env.HEALTH_PORT || 7861);
|
| 7 |
+
const GATEWAY_PORT = Number(process.env.GATEWAY_PORT || 7860);
|
| 8 |
+
const GATEWAY_HOST = "127.0.0.1";
|
| 9 |
const startTime = Date.now();
|
| 10 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
|
|
|
| 11 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
| 12 |
|
| 13 |
+
function getPathname(url) {
|
| 14 |
+
try {
|
| 15 |
+
return new URL(url, "http://localhost").pathname;
|
| 16 |
+
} catch {
|
| 17 |
+
return "/";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function isDashboardRoute(pathname) {
|
| 22 |
+
return pathname === "/dashboard" || pathname === "/dashboard/";
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function isLocalRoute(pathname) {
|
| 26 |
+
return (
|
| 27 |
+
pathname === "/health" ||
|
| 28 |
+
pathname === "/status" ||
|
| 29 |
+
isDashboardRoute(pathname)
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function appendForwarded(existingValue, nextValue) {
|
| 34 |
+
const cleanNext = nextValue || "";
|
| 35 |
+
if (!existingValue) return cleanNext;
|
| 36 |
+
if (Array.isArray(existingValue))
|
| 37 |
+
return `${existingValue.join(", ")}, ${cleanNext}`;
|
| 38 |
+
return `${existingValue}, ${cleanNext}`;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function buildProxyHeaders(headers, remoteAddress) {
|
| 42 |
+
return {
|
| 43 |
+
...headers,
|
| 44 |
+
host: headers.host || `${GATEWAY_HOST}:${GATEWAY_PORT}`,
|
| 45 |
+
"x-forwarded-for": appendForwarded(
|
| 46 |
+
headers["x-forwarded-for"],
|
| 47 |
+
remoteAddress,
|
| 48 |
+
),
|
| 49 |
+
"x-forwarded-host": headers["x-forwarded-host"] || headers.host || "",
|
| 50 |
+
"x-forwarded-proto": headers["x-forwarded-proto"] || "https",
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function readSyncStatus() {
|
| 55 |
+
try {
|
| 56 |
+
if (fs.existsSync("/tmp/sync-status.json")) {
|
| 57 |
+
return JSON.parse(fs.readFileSync("/tmp/sync-status.json", "utf8"));
|
| 58 |
+
}
|
| 59 |
+
} catch {}
|
| 60 |
+
return { status: "unknown", message: "No sync data yet" };
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function renderDashboard() {
|
| 64 |
+
return `
|
| 65 |
<!DOCTYPE html>
|
| 66 |
<html lang="en">
|
| 67 |
<head>
|
|
|
|
| 82 |
}
|
| 83 |
|
| 84 |
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 85 |
+
|
| 86 |
body {
|
| 87 |
font-family: 'Outfit', sans-serif;
|
| 88 |
background-color: var(--bg);
|
|
|
|
| 92 |
align-items: center;
|
| 93 |
min-height: 100vh;
|
| 94 |
overflow: hidden;
|
| 95 |
+
background-image:
|
| 96 |
radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
|
| 97 |
radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
|
| 98 |
}
|
|
|
|
| 234 |
}
|
| 235 |
|
| 236 |
#sync-msg { color: var(--text); display: block; margin-top: 4px; }
|
|
|
|
| 237 |
</style>
|
| 238 |
</head>
|
| 239 |
<body>
|
| 240 |
<div class="dashboard">
|
| 241 |
<header>
|
| 242 |
<h1>π¦ HuggingClaw</h1>
|
| 243 |
+
<p class="subtitle">Space Dashboard</p>
|
| 244 |
</header>
|
| 245 |
|
| 246 |
<div class="stats-grid">
|
|
|
|
| 260 |
<span class="stat-label">Telegram</span>
|
| 261 |
<span id="tg-status">Loading...</span>
|
| 262 |
</div>
|
| 263 |
+
<a href="/" class="stat-btn">Open Control UI</a>
|
| 264 |
</div>
|
| 265 |
|
| 266 |
<div class="stat-card" style="width: 100%;">
|
|
|
|
| 273 |
</div>
|
| 274 |
|
| 275 |
<div class="footer">
|
| 276 |
+
Live updates every 10s
|
| 277 |
</div>
|
| 278 |
</div>
|
| 279 |
|
|
|
|
| 285 |
|
| 286 |
document.getElementById('model-id').textContent = data.model;
|
| 287 |
document.getElementById('uptime').textContent = data.uptime;
|
| 288 |
+
|
| 289 |
+
document.getElementById('wa-status').innerHTML = data.whatsapp
|
| 290 |
? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
|
| 291 |
: '<div class="status-badge status-offline">Disabled</div>';
|
| 292 |
|
| 293 |
+
document.getElementById('tg-status').innerHTML = data.telegram
|
| 294 |
? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
|
| 295 |
: '<div class="status-badge status-offline">Disabled</div>';
|
| 296 |
|
|
|
|
| 304 |
} else if (syncData.status === 'syncing') {
|
| 305 |
badgeClass = 'status-syncing';
|
| 306 |
pulseHtml = '<div class="pulse" style="background:#3b82f6"></div>';
|
|
|
|
|
|
|
| 307 |
}
|
| 308 |
|
| 309 |
+
document.getElementById('sync-badge-container').innerHTML =
|
| 310 |
'<div class="status-badge ' + badgeClass + '">' + pulseHtml + syncData.status.toUpperCase() + '</div>';
|
| 311 |
+
|
| 312 |
document.getElementById('sync-time').textContent = syncData.timestamp || 'Never';
|
| 313 |
document.getElementById('sync-msg').textContent = syncData.message || 'Waiting for first sync...';
|
|
|
|
| 314 |
} catch (e) {
|
| 315 |
console.error("Failed to fetch status", e);
|
| 316 |
}
|
|
|
|
| 321 |
</script>
|
| 322 |
</body>
|
| 323 |
</html>
|
| 324 |
+
`;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
function proxyHttp(req, res) {
|
| 328 |
+
const proxyReq = http.request(
|
| 329 |
+
{
|
| 330 |
+
hostname: GATEWAY_HOST,
|
| 331 |
+
port: GATEWAY_PORT,
|
| 332 |
+
method: req.method,
|
| 333 |
+
path: req.url,
|
| 334 |
+
headers: buildProxyHeaders(req.headers, req.socket.remoteAddress),
|
| 335 |
+
},
|
| 336 |
+
(proxyRes) => {
|
| 337 |
+
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
| 338 |
+
proxyRes.pipe(res);
|
| 339 |
+
},
|
| 340 |
+
);
|
| 341 |
+
|
| 342 |
+
proxyReq.on("error", (error) => {
|
| 343 |
+
res.writeHead(502, { "Content-Type": "application/json" });
|
| 344 |
+
res.end(
|
| 345 |
+
JSON.stringify({
|
| 346 |
+
status: "error",
|
| 347 |
+
message: "Gateway unavailable",
|
| 348 |
+
detail: error.message,
|
| 349 |
+
}),
|
| 350 |
+
);
|
| 351 |
+
});
|
| 352 |
+
|
| 353 |
+
req.pipe(proxyReq);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
function serializeUpgradeHeaders(req, remoteAddress) {
|
| 357 |
+
const forwardedHeaders = [];
|
| 358 |
+
|
| 359 |
+
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 360 |
+
const name = req.rawHeaders[i];
|
| 361 |
+
const value = req.rawHeaders[i + 1];
|
| 362 |
+
const lower = name.toLowerCase();
|
| 363 |
+
|
| 364 |
+
if (
|
| 365 |
+
lower === "x-forwarded-for" ||
|
| 366 |
+
lower === "x-forwarded-host" ||
|
| 367 |
+
lower === "x-forwarded-proto"
|
| 368 |
+
) {
|
| 369 |
+
continue;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
forwardedHeaders.push(`${name}: ${value}`);
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
forwardedHeaders.push(
|
| 376 |
+
`X-Forwarded-For: ${appendForwarded(req.headers["x-forwarded-for"], remoteAddress)}`,
|
| 377 |
+
);
|
| 378 |
+
forwardedHeaders.push(
|
| 379 |
+
`X-Forwarded-Host: ${req.headers["x-forwarded-host"] || req.headers.host || ""}`,
|
| 380 |
+
);
|
| 381 |
+
forwardedHeaders.push(
|
| 382 |
+
`X-Forwarded-Proto: ${req.headers["x-forwarded-proto"] || "https"}`,
|
| 383 |
+
);
|
| 384 |
+
|
| 385 |
+
return forwardedHeaders;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
function proxyUpgrade(req, socket, head) {
|
| 389 |
+
const proxySocket = net.connect(GATEWAY_PORT, GATEWAY_HOST);
|
| 390 |
+
|
| 391 |
+
proxySocket.on("connect", () => {
|
| 392 |
+
const requestLines = [
|
| 393 |
+
`${req.method} ${req.url} HTTP/${req.httpVersion}`,
|
| 394 |
+
...serializeUpgradeHeaders(req, req.socket.remoteAddress),
|
| 395 |
+
"",
|
| 396 |
+
"",
|
| 397 |
+
];
|
| 398 |
+
|
| 399 |
+
proxySocket.write(requestLines.join("\r\n"));
|
| 400 |
+
|
| 401 |
+
if (head && head.length > 0) {
|
| 402 |
+
proxySocket.write(head);
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
socket.pipe(proxySocket).pipe(socket);
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
proxySocket.on("error", () => {
|
| 409 |
+
if (socket.writable) {
|
| 410 |
+
socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
| 411 |
+
}
|
| 412 |
+
socket.destroy();
|
| 413 |
+
});
|
| 414 |
+
|
| 415 |
+
socket.on("error", () => {
|
| 416 |
+
proxySocket.destroy();
|
| 417 |
+
});
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
const server = http.createServer((req, res) => {
|
| 421 |
+
const pathname = getPathname(req.url || "/");
|
| 422 |
+
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
| 423 |
+
const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
|
| 424 |
+
|
| 425 |
+
if (pathname === "/health") {
|
| 426 |
+
res.writeHead(200, { "Content-Type": "application/json" });
|
| 427 |
+
res.end(
|
| 428 |
+
JSON.stringify({
|
| 429 |
+
status: "ok",
|
| 430 |
+
uptime,
|
| 431 |
+
uptimeHuman,
|
| 432 |
+
timestamp: new Date().toISOString(),
|
| 433 |
+
}),
|
| 434 |
+
);
|
| 435 |
+
return;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
if (pathname === "/status") {
|
| 439 |
+
res.writeHead(200, { "Content-Type": "application/json" });
|
| 440 |
+
res.end(
|
| 441 |
+
JSON.stringify({
|
| 442 |
+
model: LLM_MODEL,
|
| 443 |
+
whatsapp: true,
|
| 444 |
+
telegram: TELEGRAM_ENABLED,
|
| 445 |
+
sync: readSyncStatus(),
|
| 446 |
+
uptime: uptimeHuman,
|
| 447 |
+
}),
|
| 448 |
+
);
|
| 449 |
+
return;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
if (isDashboardRoute(pathname)) {
|
| 453 |
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
| 454 |
+
res.end(renderDashboard());
|
| 455 |
+
return;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
proxyHttp(req, res);
|
| 459 |
+
});
|
| 460 |
+
|
| 461 |
+
server.on("upgrade", (req, socket, head) => {
|
| 462 |
+
const pathname = getPathname(req.url || "/");
|
| 463 |
+
if (isLocalRoute(pathname)) {
|
| 464 |
+
socket.destroy();
|
| 465 |
return;
|
| 466 |
}
|
| 467 |
|
| 468 |
+
proxyUpgrade(req, socket, head);
|
|
|
|
| 469 |
});
|
| 470 |
|
| 471 |
server.listen(PORT, "0.0.0.0", () => {
|
| 472 |
+
console.log(
|
| 473 |
+
`Health server listening on port ${PORT}; proxying gateway traffic to ${GATEWAY_HOST}:${GATEWAY_PORT}`,
|
| 474 |
+
);
|
| 475 |
});
|
keep-alive.sh
CHANGED
|
@@ -18,8 +18,8 @@ if [ -z "$SPACE_HOST" ]; then
|
|
| 18 |
exit 0
|
| 19 |
fi
|
| 20 |
|
| 21 |
-
# Ping the
|
| 22 |
-
PING_URL="https://${SPACE_HOST}"
|
| 23 |
|
| 24 |
echo "π Keep-alive started: pinging ${PING_URL} every ${INTERVAL}s"
|
| 25 |
|
|
|
|
| 18 |
exit 0
|
| 19 |
fi
|
| 20 |
|
| 21 |
+
# Ping the health endpoint so we keep the Space warm without touching the gateway
|
| 22 |
+
PING_URL="https://${SPACE_HOST}/health"
|
| 23 |
|
| 24 |
echo "π Keep-alive started: pinging ${PING_URL} every ${INTERVAL}s"
|
| 25 |
|
start.sh
CHANGED
|
@@ -271,6 +271,7 @@ fi
|
|
| 271 |
if [ -n "$SPACE_HOST" ]; then
|
| 272 |
printf " β %-40s β\n" "Keep-alive: β
every ${KEEP_ALIVE_INTERVAL:-300}s"
|
| 273 |
printf " β %-40s β\n" "Control UI: https://${SPACE_HOST}"
|
|
|
|
| 274 |
else
|
| 275 |
printf " β %-40s β\n" "Keep-alive: βΈοΈ local mode"
|
| 276 |
fi
|
|
@@ -325,12 +326,16 @@ export LLM_MODEL="$LLM_MODEL"
|
|
| 325 |
node /home/node/app/health-server.js &
|
| 326 |
HEALTH_PID=$!
|
| 327 |
|
| 328 |
-
# 11. Start
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
node /home/node/app/wa-guardian.js &
|
| 330 |
GUARDIAN_PID=$!
|
| 331 |
echo "π‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
|
| 332 |
|
| 333 |
-
#
|
| 334 |
python3 -u /home/node/app/workspace-sync.py &
|
| 335 |
|
| 336 |
# ββ Launch gateway ββ
|
|
|
|
| 271 |
if [ -n "$SPACE_HOST" ]; then
|
| 272 |
printf " β %-40s β\n" "Keep-alive: β
every ${KEEP_ALIVE_INTERVAL:-300}s"
|
| 273 |
printf " β %-40s β\n" "Control UI: https://${SPACE_HOST}"
|
| 274 |
+
printf " β %-40s β\n" "Dashboard: https://${SPACE_HOST}/dashboard"
|
| 275 |
else
|
| 276 |
printf " β %-40s β\n" "Keep-alive: βΈοΈ local mode"
|
| 277 |
fi
|
|
|
|
| 326 |
node /home/node/app/health-server.js &
|
| 327 |
HEALTH_PID=$!
|
| 328 |
|
| 329 |
+
# 11. Start HF keep-alive
|
| 330 |
+
/home/node/app/keep-alive.sh &
|
| 331 |
+
KEEP_ALIVE_PID=$!
|
| 332 |
+
|
| 333 |
+
# 12. Start WhatsApp Guardian (Automates pairing)
|
| 334 |
node /home/node/app/wa-guardian.js &
|
| 335 |
GUARDIAN_PID=$!
|
| 336 |
echo "π‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
|
| 337 |
|
| 338 |
+
# 13. Start Workspace Sync
|
| 339 |
python3 -u /home/node/app/workspace-sync.py &
|
| 340 |
|
| 341 |
# ββ Launch gateway ββ
|