Spaces:
Running
Running
Simplify dashboard actions and forward API auth
Browse files- README.md +1 -1
- health-server.js +17 -31
README.md
CHANGED
|
@@ -190,7 +190,7 @@ http://localhost:7861
|
|
| 190 |
| `/health` | Health check for HF and UptimeRobot |
|
| 191 |
| `/status` | JSON status |
|
| 192 |
| `/app/` | Proxied Hermes dashboard/app |
|
| 193 |
-
| `/v1/
|
| 194 |
| `/telegram` | Telegram webhook endpoint |
|
| 195 |
|
| 196 |
The `/v1/*` routes require:
|
|
|
|
| 190 |
| `/health` | Health check for HF and UptimeRobot |
|
| 191 |
| `/status` | JSON status |
|
| 192 |
| `/app/` | Proxied Hermes dashboard/app |
|
| 193 |
+
| `/v1/*` | Proxied Hermes OpenAI-compatible API routes |
|
| 194 |
| `/telegram` | Telegram webhook endpoint |
|
| 195 |
|
| 196 |
The `/v1/*` routes require:
|
health-server.js
CHANGED
|
@@ -14,7 +14,6 @@ const startTime = Date.now();
|
|
| 14 |
const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
|
| 15 |
const APP_BASE = "/app";
|
| 16 |
const LOGIN_PATH = "/login";
|
| 17 |
-
const LOGOUT_PATH = "/logout";
|
| 18 |
const SESSION_COOKIE = "huggingmess_session";
|
| 19 |
|
| 20 |
const SYNC_STATUS_FILE = "/tmp/huggingmess-sync-status.json";
|
|
@@ -85,11 +84,6 @@ function buildSessionCookie(req) {
|
|
| 85 |
return `${SESSION_COOKIE}=${encodeURIComponent(expectedSessionValue())}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${secure}`;
|
| 86 |
}
|
| 87 |
|
| 88 |
-
function buildClearSessionCookie(req) {
|
| 89 |
-
const secure = isHttpsRequest(req) ? "; Secure" : "";
|
| 90 |
-
return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure}`;
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
function getBearerToken(req) {
|
| 94 |
const value = req.headers.authorization || "";
|
| 95 |
const match = /^Bearer\s+(.+)$/i.exec(value);
|
|
@@ -240,20 +234,12 @@ async function handleLogin(req, res, parsed) {
|
|
| 240 |
}
|
| 241 |
}
|
| 242 |
|
| 243 |
-
function
|
| 244 |
-
res.writeHead(302, {
|
| 245 |
-
location: LOGIN_PATH,
|
| 246 |
-
"set-cookie": buildClearSessionCookie(req),
|
| 247 |
-
"cache-control": "no-store",
|
| 248 |
-
});
|
| 249 |
-
res.end();
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
function proxyRequest(req, res, targetPort, rewritePath = (path) => path) {
|
| 253 |
const parsed = new URL(req.url, "http://localhost");
|
| 254 |
const targetPath = rewritePath(parsed.pathname) + parsed.search;
|
| 255 |
const headers = {
|
| 256 |
...req.headers,
|
|
|
|
| 257 |
host: `${GATEWAY_HOST}:${targetPort}`,
|
| 258 |
"x-forwarded-host": req.headers.host || "",
|
| 259 |
"x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
|
|
@@ -361,8 +347,6 @@ function renderDashboard(data) {
|
|
| 361 |
const syncTone = ["success", "restored", "synced", "configured"].includes(syncStatus) ? "ok" : syncStatus === "disabled" ? "warn" : "neutral";
|
| 362 |
const telegramTone = data.telegram.configured ? (data.telegram.webhookListening || !data.telegram.webhook ? "ok" : "warn") : "warn";
|
| 363 |
const keepAliveTone = data.uptimerobot?.configured ? "ok" : process.env.UPTIMEROBOT_API_KEY ? "warn" : "neutral";
|
| 364 |
-
const publicBase = process.env.SPACE_HOST ? `https://${process.env.SPACE_HOST}` : `http://localhost:${PORT}`;
|
| 365 |
-
const apiCurl = `curl -H "Authorization: Bearer $GATEWAY_TOKEN" ${publicBase}/v1/models`;
|
| 366 |
const gatewayDetail = data.gateway
|
| 367 |
? `OpenAI-compatible API is listening on internal port <code>${data.ports.gateway}</code>.`
|
| 368 |
: `Gateway API is not reachable on internal port <code>${data.ports.gateway}</code>.`;
|
|
@@ -388,7 +372,7 @@ function renderDashboard(data) {
|
|
| 388 |
value: toneBadge(data.gateway ? "Online" : "Offline", data.gateway ? "ok" : "off"),
|
| 389 |
detail: gatewayDetail,
|
| 390 |
tone: data.gateway ? "ok" : "off",
|
| 391 |
-
meta: `<code>
|
| 392 |
}),
|
| 393 |
renderTile({
|
| 394 |
title: "Hermes App",
|
|
@@ -471,7 +455,9 @@ function renderDashboard(data) {
|
|
| 471 |
.tile-detail { color:var(--soft); line-height:1.45; font-size:.86rem; }
|
| 472 |
.tile-meta { color:var(--muted); line-height:1.4; font-size:.78rem; margin-top:auto; overflow-wrap:anywhere; }
|
| 473 |
.panel { border:1px solid var(--line); background:var(--panel2); border-radius:8px; padding:14px; margin-top:10px; }
|
| 474 |
-
.
|
|
|
|
|
|
|
| 475 |
code { background:#0d0d0d; border:1px solid var(--line); border-radius:6px; padding:2px 5px; color:var(--text); font-size:.9em; }
|
| 476 |
pre { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; background:#0d0d0d; border:1px solid var(--line); border-radius:7px; padding:10px; color:var(--soft); font-size:.82rem; line-height:1.45; }
|
| 477 |
.row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
|
@@ -485,6 +471,7 @@ function renderDashboard(data) {
|
|
| 485 |
.button.secondary { color:var(--text); background:#242424; border:1px solid var(--line); }
|
| 486 |
.button.subtle { color:var(--soft); background:transparent; border:1px solid var(--line); }
|
| 487 |
@media (max-width: 980px) { .overview { grid-template-columns:repeat(2, minmax(0, 1fr)); } header { display:block; } .top-actions { justify-content:flex-start; margin-top:14px; min-width:0; } }
|
|
|
|
| 488 |
@media (max-width: 620px) { main { width:min(100% - 20px, 1180px); padding-top:16px; } .overview { grid-template-columns:1fr; } h1 { font-size:2rem; } }
|
| 489 |
</style>
|
| 490 |
</head>
|
|
@@ -497,17 +484,18 @@ function renderDashboard(data) {
|
|
| 497 |
</div>
|
| 498 |
<div class="top-actions">
|
| 499 |
<a class="button" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open App</a>
|
| 500 |
-
<a class="button secondary" href="/v1/models" target="_blank" rel="noopener noreferrer">Models</a>
|
| 501 |
<a class="button secondary" href="/status">Status JSON</a>
|
| 502 |
-
<a class="button subtle" href="${LOGOUT_PATH}">Logout</a>
|
| 503 |
</div>
|
| 504 |
</header>
|
| 505 |
<section class="overview">
|
| 506 |
${tiles}
|
| 507 |
</section>
|
| 508 |
-
<section class="panel">
|
| 509 |
-
<div
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
| 511 |
</section>
|
| 512 |
</main>
|
| 513 |
</body>
|
|
@@ -523,11 +511,6 @@ const server = http.createServer(async (req, res) => {
|
|
| 523 |
return;
|
| 524 |
}
|
| 525 |
|
| 526 |
-
if (path === LOGOUT_PATH) {
|
| 527 |
-
handleLogout(req, res);
|
| 528 |
-
return;
|
| 529 |
-
}
|
| 530 |
-
|
| 531 |
if (path === "/health" || path === `${APP_BASE}/health`) {
|
| 532 |
const data = await statusPayload();
|
| 533 |
res.writeHead(data.ok ? 200 : 503, { "content-type": "application/json" });
|
|
@@ -610,7 +593,10 @@ const server = http.createServer(async (req, res) => {
|
|
| 610 |
res.end(JSON.stringify({ error: "unauthorized", message: "Use Authorization: Bearer <GATEWAY_TOKEN>." }));
|
| 611 |
return;
|
| 612 |
}
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
| 614 |
return;
|
| 615 |
}
|
| 616 |
|
|
|
|
| 14 |
const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
|
| 15 |
const APP_BASE = "/app";
|
| 16 |
const LOGIN_PATH = "/login";
|
|
|
|
| 17 |
const SESSION_COOKIE = "huggingmess_session";
|
| 18 |
|
| 19 |
const SYNC_STATUS_FILE = "/tmp/huggingmess-sync-status.json";
|
|
|
|
| 84 |
return `${SESSION_COOKIE}=${encodeURIComponent(expectedSessionValue())}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${secure}`;
|
| 85 |
}
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
function getBearerToken(req) {
|
| 88 |
const value = req.headers.authorization || "";
|
| 89 |
const match = /^Bearer\s+(.+)$/i.exec(value);
|
|
|
|
| 234 |
}
|
| 235 |
}
|
| 236 |
|
| 237 |
+
function proxyRequest(req, res, targetPort, rewritePath = (path) => path, headerOverrides = {}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
const parsed = new URL(req.url, "http://localhost");
|
| 239 |
const targetPath = rewritePath(parsed.pathname) + parsed.search;
|
| 240 |
const headers = {
|
| 241 |
...req.headers,
|
| 242 |
+
...headerOverrides,
|
| 243 |
host: `${GATEWAY_HOST}:${targetPort}`,
|
| 244 |
"x-forwarded-host": req.headers.host || "",
|
| 245 |
"x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
|
|
|
|
| 347 |
const syncTone = ["success", "restored", "synced", "configured"].includes(syncStatus) ? "ok" : syncStatus === "disabled" ? "warn" : "neutral";
|
| 348 |
const telegramTone = data.telegram.configured ? (data.telegram.webhookListening || !data.telegram.webhook ? "ok" : "warn") : "warn";
|
| 349 |
const keepAliveTone = data.uptimerobot?.configured ? "ok" : process.env.UPTIMEROBOT_API_KEY ? "warn" : "neutral";
|
|
|
|
|
|
|
| 350 |
const gatewayDetail = data.gateway
|
| 351 |
? `OpenAI-compatible API is listening on internal port <code>${data.ports.gateway}</code>.`
|
| 352 |
: `Gateway API is not reachable on internal port <code>${data.ports.gateway}</code>.`;
|
|
|
|
| 372 |
value: toneBadge(data.gateway ? "Online" : "Offline", data.gateway ? "ok" : "off"),
|
| 373 |
detail: gatewayDetail,
|
| 374 |
tone: data.gateway ? "ok" : "off",
|
| 375 |
+
meta: `API routes are protected by <code>GATEWAY_TOKEN</code>.`,
|
| 376 |
}),
|
| 377 |
renderTile({
|
| 378 |
title: "Hermes App",
|
|
|
|
| 455 |
.tile-detail { color:var(--soft); line-height:1.45; font-size:.86rem; }
|
| 456 |
.tile-meta { color:var(--muted); line-height:1.4; font-size:.78rem; margin-top:auto; overflow-wrap:anywhere; }
|
| 457 |
.panel { border:1px solid var(--line); background:var(--panel2); border-radius:8px; padding:14px; margin-top:10px; }
|
| 458 |
+
.launch-panel { display:flex; align-items:center; justify-content:space-between; gap:18px; }
|
| 459 |
+
.panel-title { color:var(--muted); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; font-weight:800; margin-bottom:7px; }
|
| 460 |
+
.panel-copy { color:var(--soft); line-height:1.45; font-size:.9rem; margin:0; }
|
| 461 |
code { background:#0d0d0d; border:1px solid var(--line); border-radius:6px; padding:2px 5px; color:var(--text); font-size:.9em; }
|
| 462 |
pre { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; background:#0d0d0d; border:1px solid var(--line); border-radius:7px; padding:10px; color:var(--soft); font-size:.82rem; line-height:1.45; }
|
| 463 |
.row { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
|
|
|
| 471 |
.button.secondary { color:var(--text); background:#242424; border:1px solid var(--line); }
|
| 472 |
.button.subtle { color:var(--soft); background:transparent; border:1px solid var(--line); }
|
| 473 |
@media (max-width: 980px) { .overview { grid-template-columns:repeat(2, minmax(0, 1fr)); } header { display:block; } .top-actions { justify-content:flex-start; margin-top:14px; min-width:0; } }
|
| 474 |
+
@media (max-width: 760px) { .launch-panel { display:block; } .launch-panel .button { margin-top:12px; width:100%; } }
|
| 475 |
@media (max-width: 620px) { main { width:min(100% - 20px, 1180px); padding-top:16px; } .overview { grid-template-columns:1fr; } h1 { font-size:2rem; } }
|
| 476 |
</style>
|
| 477 |
</head>
|
|
|
|
| 484 |
</div>
|
| 485 |
<div class="top-actions">
|
| 486 |
<a class="button" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open App</a>
|
|
|
|
| 487 |
<a class="button secondary" href="/status">Status JSON</a>
|
|
|
|
| 488 |
</div>
|
| 489 |
</header>
|
| 490 |
<section class="overview">
|
| 491 |
${tiles}
|
| 492 |
</section>
|
| 493 |
+
<section class="panel launch-panel">
|
| 494 |
+
<div>
|
| 495 |
+
<div class="panel-title">Hermes Agent</div>
|
| 496 |
+
<p class="panel-copy">Open the full Hermes Agent dashboard in a new window. You will be asked for only your <code>GATEWAY_TOKEN</code> if your session is not already active.</p>
|
| 497 |
+
</div>
|
| 498 |
+
<a class="button" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes Agent</a>
|
| 499 |
</section>
|
| 500 |
</main>
|
| 501 |
</body>
|
|
|
|
| 511 |
return;
|
| 512 |
}
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
if (path === "/health" || path === `${APP_BASE}/health`) {
|
| 515 |
const data = await statusPayload();
|
| 516 |
res.writeHead(data.ok ? 200 : 503, { "content-type": "application/json" });
|
|
|
|
| 593 |
res.end(JSON.stringify({ error: "unauthorized", message: "Use Authorization: Bearer <GATEWAY_TOKEN>." }));
|
| 594 |
return;
|
| 595 |
}
|
| 596 |
+
const upstreamHeaders = getBearerToken(req) || !API_SERVER_KEY
|
| 597 |
+
? {}
|
| 598 |
+
: { authorization: `Bearer ${API_SERVER_KEY}` };
|
| 599 |
+
proxyRequest(req, res, GATEWAY_PORT, (p) => p, upstreamHeaders);
|
| 600 |
return;
|
| 601 |
}
|
| 602 |
|