// Single public entrypoint for HF Spaces: local dashboard + reverse proxy to OpenClaw.
const http = require("http");
const fs = require("fs");
const net = require("net");
const PORT = 7861;
const GATEWAY_PORT = 7860;
const GATEWAY_HOST = "127.0.0.1";
const startTime = Date.now();
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
const HF_BACKUP_ENABLED = !!process.env.HF_TOKEN;
const SYNC_INTERVAL = process.env.SYNC_INTERVAL || "180";
const APP_BASE = "/app";
const SYNC_STATUS_FILE = "/tmp/sync-status.json";
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
"/tmp/huggingclaw-cloudflare-keepalive-status.json";
function parseRequestUrl(url) {
try {
return new URL(url, "http://localhost");
} catch {
return new URL("http://localhost/");
}
}
function getSyncStatus() {
try {
if (fs.existsSync(SYNC_STATUS_FILE)) {
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
}
} catch {}
if (HF_BACKUP_ENABLED) {
return {
status: "configured",
message: `Backup is enabled. Waiting for sync window (${SYNC_INTERVAL}s).`,
};
}
return { status: "unknown", message: "No sync data yet" };
}
function readGuardianStatus() {
if (!WHATSAPP_ENABLED) {
return { configured: false, connected: false, pairing: false };
}
try {
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
const parsed = JSON.parse(fs.readFileSync(WHATSAPP_STATUS_FILE, "utf8"));
return {
configured: parsed.configured !== false,
connected: parsed.connected === true,
pairing: parsed.pairing === true,
};
}
} catch {}
return { configured: true, connected: false, pairing: false };
}
function getKeepaliveStatus() {
try {
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE)) {
return JSON.parse(
fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"),
);
}
} catch {}
return null;
}
function probeGatewayHealth(timeoutMs = 1500) {
return new Promise((resolve) => {
const request = http.get(
{
hostname: GATEWAY_HOST,
port: GATEWAY_PORT,
path: "/health",
timeout: timeoutMs,
},
(response) => {
response.resume();
resolve(response.statusCode >= 200 && response.statusCode < 400);
},
);
request.on("timeout", () => {
request.destroy();
resolve(false);
});
request.on("error", () => resolve(false));
});
}
function formatUptime(ms) {
const total = Math.floor(ms / 1000);
const days = Math.floor(total / 86400);
const hours = Math.floor((total % 86400) / 3600);
const minutes = Math.floor((total % 3600) / 60);
if (days) return `${days}d ${hours}h ${minutes}m`;
if (hours) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """);
}
function toneBadge(label, tone = "neutral") {
return `${escapeHtml(label)}`;
}
function renderTile({
title,
value,
detail = "",
tone = "neutral",
meta = "",
}) {
return `${escapeHtml(data.keepalive.targetUrl || "/health")}`
: process.env.CLOUDFLARE_WORKERS_TOKEN
? "Worker pending or failed"
: "Not configured";
const tiles = [
renderTile({
title: "Gateway",
value: toneBadge(
data.gatewayReady ? "Online" : "Offline",
data.gatewayReady ? "ok" : "off",
),
detail: `Internal Port ${GATEWAY_PORT}`,
tone: data.gatewayReady ? "ok" : "off",
}),
renderTile({
title: "Model",
value: `${escapeHtml(LLM_MODEL)}`,
detail: `Primary LLM Configured`,
tone: "neutral",
}),
renderTile({
title: "Runtime",
value: escapeHtml(data.uptimeHuman),
detail: `Public Port ${PORT}`,
tone: "neutral",
}),
renderTile({
title: "Telegram",
value: toneBadge(
TELEGRAM_ENABLED ? "Enabled" : "Disabled",
TELEGRAM_ENABLED ? "ok" : "neutral",
),
detail: TELEGRAM_ENABLED ? "Bot Channel active" : "Not configured",
tone: TELEGRAM_ENABLED ? "ok" : "neutral",
}),
renderTile({
title: "Backup",
value: toneBadge(syncStatus.toUpperCase(), syncTone),
detail: backupDetail,
tone: syncTone,
meta: data.sync?.timestamp
? ``
: "",
}),
renderTile({
title: "Keep Awake",
value: toneBadge(
keepaliveConfigured ? "CF Cron" : keepaliveStatus.toUpperCase(),
keepAliveTone,
),
detail: keepAliveDetail,
tone: keepAliveTone,
}),
].join("");
return `