somratpro commited on
Commit
15f10b1
·
1 Parent(s): 00136ee

Improve dashboard and browser API auth flow

Browse files
Files changed (1) hide show
  1. health-server.js +169 -37
health-server.js CHANGED
@@ -182,6 +182,11 @@ function requireAuth(req, res) {
182
  return false;
183
  }
184
 
 
 
 
 
 
185
  async function handleLogin(req, res, parsed) {
186
  const nextPath = sanitizeNext(parsed.searchParams.get("next") || `${APP_BASE}/`);
187
 
@@ -303,8 +308,16 @@ async function statusPayload() {
303
  return {
304
  ok: gateway,
305
  uptime: formatUptime(Date.now() - startTime),
 
306
  gateway,
307
  dashboard,
 
 
 
 
 
 
 
308
  telegram: {
309
  configured: !!process.env.TELEGRAM_BOT_TOKEN,
310
  webhook: !!process.env.TELEGRAM_WEBHOOK_URL,
@@ -320,19 +333,113 @@ async function statusPayload() {
320
  }
321
 
322
  function badge(label, state) {
323
- const cls = state ? "ok" : "off";
324
- return `<span class="badge ${cls}">${label}</span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  }
326
 
327
  function renderDashboard(data) {
328
- const syncStatus = String(data.backup?.status || "unknown").toUpperCase();
329
- const dashboardLink = data.dashboard ? `<a class="button" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes App</a>` : "";
330
- const apiLink = data.gateway ? `<a class="button secondary" href="/v1/models">API Models</a>` : "";
331
- const keepAlive = data.uptimerobot?.configured
332
- ? `UptimeRobot is monitoring <code>${data.uptimerobot.url}</code>.`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  : process.env.UPTIMEROBOT_API_KEY
334
- ? "UptimeRobot setup is pending or failed; check logs."
335
  : "Add UPTIMEROBOT_API_KEY to create a keep-awake monitor.";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
  return `<!doctype html>
338
  <html lang="en">
@@ -341,26 +448,44 @@ function renderDashboard(data) {
341
  <meta name="viewport" content="width=device-width, initial-scale=1" />
342
  <title>HuggingMess</title>
343
  <style>
344
- :root { color-scheme: dark; --bg:#10141f; --panel:#171d2b; --line:#293246; --text:#f4f7fb; --muted:#9aa7bd; --good:#22c55e; --warn:#f59e0b; --bad:#ef4444; --accent:#38bdf8; }
345
  * { box-sizing:border-box; }
346
- body { margin:0; min-height:100vh; font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); }
347
- main { width:min(960px, calc(100% - 32px)); margin:0 auto; padding:36px 0; }
348
- header { display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:28px; }
349
- h1 { margin:0; font-size:clamp(2rem, 6vw, 4.4rem); line-height:.95; letter-spacing:0; }
350
- .subtitle { margin-top:12px; color:var(--muted); max-width:620px; line-height:1.5; }
351
- .grid { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:14px; }
352
- .card { border:1px solid var(--line); background:var(--panel); border-radius:8px; padding:18px; min-height:120px; }
353
- .wide { grid-column:1 / -1; }
354
- .label { color:var(--muted); font-size:.78rem; letter-spacing:.08em; text-transform:uppercase; margin-bottom:10px; }
355
- .value { font-size:1.05rem; overflow-wrap:anywhere; }
356
- code { background:#0b0f18; border:1px solid var(--line); border-radius:6px; padding:2px 6px; }
357
- .row { display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
358
- .badge { display:inline-flex; border:1px solid var(--line); border-radius:999px; padding:5px 10px; font-size:.8rem; font-weight:700; }
359
- .badge.ok { color:var(--good); border-color:rgba(34,197,94,.35); background:rgba(34,197,94,.08); }
360
- .badge.off { color:var(--bad); border-color:rgba(239,68,68,.35); background:rgba(239,68,68,.08); }
361
- .button { display:inline-flex; align-items:center; justify-content:center; min-height:42px; padding:0 14px; border-radius:7px; color:#07111f; background:var(--accent); text-decoration:none; font-weight:750; }
362
- .button.secondary { color:var(--text); background:#222b3c; border:1px solid var(--line); }
363
- @media (max-width: 720px) { header { display:block; } .grid { grid-template-columns:1fr; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </style>
365
  </head>
366
  <body>
@@ -368,18 +493,21 @@ function renderDashboard(data) {
368
  <header>
369
  <div>
370
  <h1>HuggingMess</h1>
371
- <div class="subtitle">Hermes Agent running as an always-on Hugging Face Docker Space, with Telegram gateway, state backup, Cloudflare proxy support, and keep-awake monitoring.</div>
 
 
 
 
 
 
372
  </div>
373
- <div class="row">${badge("Gateway", data.gateway)}${badge("Dashboard", data.dashboard)}${badge("Backup", data.backup?.status !== "disabled")}</div>
374
  </header>
375
- <section class="grid">
376
- <div class="card"><div class="label">Uptime</div><div class="value">${data.uptime}</div></div>
377
- <div class="card"><div class="label">Model</div><div class="value"><code>${data.model || "not set"}</code></div></div>
378
- <div class="card"><div class="label">Provider</div><div class="value"><code>${data.provider}</code></div></div>
379
- <div class="card"><div class="label">Telegram</div><div class="value">${data.telegram.configured ? "Configured" : "Not configured"}${data.telegram.webhook ? " via webhook" : ""}</div></div>
380
- <div class="card wide"><div class="label">Backup</div><div class="value"><strong>${syncStatus}</strong><br>${data.backup?.message || ""}</div></div>
381
- <div class="card wide"><div class="label">Keep Awake</div><div class="value">${keepAlive}</div></div>
382
- <div class="card wide"><div class="label">Entrypoints</div><div class="row">${dashboardLink}${apiLink}<a class="button secondary" href="/status">Status JSON</a></div></div>
383
  </section>
384
  </main>
385
  </body>
@@ -471,6 +599,10 @@ const server = http.createServer(async (req, res) => {
471
 
472
  if (path === "/v1" || path.startsWith("/v1/")) {
473
  if (!isAuthorized(req)) {
 
 
 
 
474
  res.writeHead(401, {
475
  "content-type": "application/json",
476
  "cache-control": "no-store",
 
182
  return false;
183
  }
184
 
185
+ function wantsHtml(req) {
186
+ const accept = String(req.headers.accept || "");
187
+ return accept.includes("text/html");
188
+ }
189
+
190
  async function handleLogin(req, res, parsed) {
191
  const nextPath = sanitizeNext(parsed.searchParams.get("next") || `${APP_BASE}/`);
192
 
 
308
  return {
309
  ok: gateway,
310
  uptime: formatUptime(Date.now() - startTime),
311
+ startedAt: new Date(startTime).toISOString(),
312
  gateway,
313
  dashboard,
314
+ authConfigured: !!API_SERVER_KEY,
315
+ ports: {
316
+ public: PORT,
317
+ gateway: GATEWAY_PORT,
318
+ dashboard: DASHBOARD_PORT,
319
+ telegramWebhook: TELEGRAM_WEBHOOK_PORT,
320
+ },
321
  telegram: {
322
  configured: !!process.env.TELEGRAM_BOT_TOKEN,
323
  webhook: !!process.env.TELEGRAM_WEBHOOK_URL,
 
333
  }
334
 
335
  function badge(label, state) {
336
+ return `<span class="badge ${state ? "ok" : "off"}">${escapeHtml(label)}</span>`;
337
+ }
338
+
339
+ function toneBadge(label, tone = "neutral") {
340
+ return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
341
+ }
342
+
343
+ function valueOrUnset(value, fallback = "Not set") {
344
+ return value ? escapeHtml(value) : `<span class="muted">${escapeHtml(fallback)}</span>`;
345
+ }
346
+
347
+ function renderTile({ title, value, detail = "", tone = "neutral", meta = "" }) {
348
+ return `<article class="tile ${tone}">
349
+ <div class="tile-head">
350
+ <span class="tile-title">${escapeHtml(title)}</span>
351
+ <span class="tile-dot"></span>
352
+ </div>
353
+ <div class="tile-value">${value}</div>
354
+ ${detail ? `<div class="tile-detail">${detail}</div>` : ""}
355
+ ${meta ? `<div class="tile-meta">${meta}</div>` : ""}
356
+ </article>`;
357
  }
358
 
359
  function renderDashboard(data) {
360
+ const syncStatus = String(data.backup?.status || "unknown");
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>.`;
369
+ const appDetail = data.dashboard
370
+ ? `Hermes dashboard is listening on internal port <code>${data.ports.dashboard}</code>.`
371
+ : `Hermes dashboard is not reachable on internal port <code>${data.ports.dashboard}</code>.`;
372
+ const authDetail = data.authConfigured
373
+ ? `Protected by <code>GATEWAY_TOKEN</code> with a token-only login page.`
374
+ : `No <code>GATEWAY_TOKEN</code> is set; public app routes are unlocked.`;
375
+ const telegramDetail = data.telegram.configured
376
+ ? `${data.telegram.webhook ? "Webhook mode" : "Polling mode"}${data.telegram.proxy ? ` through Cloudflare proxy` : ""}.`
377
+ : "Add TELEGRAM_BOT_TOKEN to enable Telegram.";
378
+ const backupDetail = data.backup?.message ? escapeHtml(data.backup.message) : "No backup status has been written yet.";
379
+ const backupMeta = data.backup?.timestamp ? `Last update ${escapeHtml(data.backup.timestamp)}` : "";
380
+ const keepAliveDetail = data.uptimerobot?.configured
381
+ ? `Monitoring <code>${escapeHtml(data.uptimerobot.url || "/health")}</code>.`
382
  : process.env.UPTIMEROBOT_API_KEY
383
+ ? "UptimeRobot setup is pending or failed; check Space logs."
384
  : "Add UPTIMEROBOT_API_KEY to create a keep-awake monitor.";
385
+ const tiles = [
386
+ renderTile({
387
+ title: "Gateway",
388
+ value: toneBadge(data.gateway ? "Online" : "Offline", data.gateway ? "ok" : "off"),
389
+ detail: gatewayDetail,
390
+ tone: data.gateway ? "ok" : "off",
391
+ meta: `<code>/v1/models</code> requires token auth.`,
392
+ }),
393
+ renderTile({
394
+ title: "Hermes App",
395
+ value: toneBadge(data.dashboard ? "Ready" : "Starting", data.dashboard ? "ok" : "warn"),
396
+ detail: appDetail,
397
+ tone: data.dashboard ? "ok" : "warn",
398
+ meta: `<code>/app/</code> opens in a new window.`,
399
+ }),
400
+ renderTile({
401
+ title: "Auth",
402
+ value: toneBadge(data.authConfigured ? "Token set" : "Unlocked", data.authConfigured ? "ok" : "warn"),
403
+ detail: authDetail,
404
+ tone: data.authConfigured ? "ok" : "warn",
405
+ meta: data.authConfigured ? "Browser visits use the login page; API clients use Bearer auth." : "Set GATEWAY_TOKEN before sharing this Space.",
406
+ }),
407
+ renderTile({
408
+ title: "Runtime",
409
+ value: escapeHtml(data.uptime),
410
+ detail: `Public port <code>${data.ports.public}</code>. Started <code>${escapeHtml(data.startedAt)}</code>.`,
411
+ tone: "neutral",
412
+ meta: `Health endpoint: <code>/health</code>`,
413
+ }),
414
+ renderTile({
415
+ title: "Model",
416
+ value: `<code>${valueOrUnset(data.model)}</code>`,
417
+ detail: `Provider <code>${valueOrUnset(data.provider || "auto")}</code>.`,
418
+ tone: data.model ? "ok" : "warn",
419
+ meta: "For Gemini: LLM_MODEL=google/gemini-2.5-flash",
420
+ }),
421
+ renderTile({
422
+ title: "Telegram",
423
+ value: toneBadge(data.telegram.configured ? "Configured" : "Disabled", telegramTone),
424
+ detail: telegramDetail,
425
+ tone: telegramTone,
426
+ meta: data.telegram.webhookUrl ? `<code>${escapeHtml(data.telegram.webhookUrl)}</code>` : "",
427
+ }),
428
+ renderTile({
429
+ title: "Backup",
430
+ value: toneBadge(syncStatus.toUpperCase(), syncTone),
431
+ detail: backupDetail,
432
+ tone: syncTone,
433
+ meta: backupMeta,
434
+ }),
435
+ renderTile({
436
+ title: "Keep Awake",
437
+ value: toneBadge(data.uptimerobot?.configured ? "Monitor active" : "Not configured", keepAliveTone),
438
+ detail: keepAliveDetail,
439
+ tone: keepAliveTone,
440
+ meta: process.env.UPTIMEROBOT_API_KEY ? "UPTIMEROBOT_API_KEY detected." : "",
441
+ }),
442
+ ].join("");
443
 
444
  return `<!doctype html>
445
  <html lang="en">
 
448
  <meta name="viewport" content="width=device-width, initial-scale=1" />
449
  <title>HuggingMess</title>
450
  <style>
451
+ :root { color-scheme: dark; --bg:#101010; --panel:#171717; --panel2:#1e1e1e; --line:#303030; --text:#f3f4f6; --muted:#a1a1aa; --soft:#d4d4d8; --good:#4ade80; --warn:#fbbf24; --bad:#fb7185; --accent:#67e8f9; }
452
  * { box-sizing:border-box; }
453
+ body { margin:0; min-height:100vh; font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); font-size:14px; }
454
+ main { width:min(1180px, calc(100% - 32px)); margin:0 auto; padding:24px 0 32px; }
455
+ header { display:flex; justify-content:space-between; gap:24px; align-items:flex-start; margin-bottom:18px; border-bottom:1px solid var(--line); padding-bottom:18px; }
456
+ h1 { margin:0; font-size:clamp(2rem, 4vw, 3.2rem); line-height:1; letter-spacing:0; }
457
+ .subtitle { margin-top:10px; color:var(--muted); max-width:700px; line-height:1.45; font-size:.95rem; }
458
+ .top-actions { display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; min-width:300px; }
459
+ .overview { display:grid; grid-template-columns:repeat(4, minmax(0, 1fr)); gap:10px; margin-bottom:10px; }
460
+ .tile { border:1px solid var(--line); background:var(--panel); border-radius:8px; padding:14px; min-height:142px; display:flex; flex-direction:column; gap:10px; }
461
+ .tile.ok { border-color:rgba(74,222,128,.28); }
462
+ .tile.warn { border-color:rgba(251,191,36,.28); }
463
+ .tile.off { border-color:rgba(251,113,133,.32); }
464
+ .tile-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
465
+ .tile-title { color:var(--muted); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; font-weight:800; }
466
+ .tile-dot { width:7px; height:7px; border-radius:50%; background:var(--line); }
467
+ .tile.ok .tile-dot { background:var(--good); }
468
+ .tile.warn .tile-dot { background:var(--warn); }
469
+ .tile.off .tile-dot { background:var(--bad); }
470
+ .tile-value { font-size:1.05rem; font-weight:760; overflow-wrap:anywhere; }
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
+ .panel-title { color:var(--muted); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; font-weight:800; margin-bottom:10px; }
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; }
478
+ .badge { display:inline-flex; align-items:center; border:1px solid var(--line); border-radius:999px; padding:4px 9px; font-size:.75rem; font-weight:800; line-height:1; }
479
+ .badge.ok { color:var(--good); border-color:rgba(74,222,128,.36); background:rgba(74,222,128,.08); }
480
+ .badge.warn { color:var(--warn); border-color:rgba(251,191,36,.34); background:rgba(251,191,36,.08); }
481
+ .badge.off { color:var(--bad); border-color:rgba(251,113,133,.36); background:rgba(251,113,133,.08); }
482
+ .badge.neutral { color:var(--soft); }
483
+ .muted { color:var(--muted); }
484
+ .button { display:inline-flex; align-items:center; justify-content:center; min-height:34px; padding:0 11px; border-radius:7px; color:#081012; background:var(--accent); text-decoration:none; font-weight:800; font-size:.86rem; }
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>
491
  <body>
 
493
  <header>
494
  <div>
495
  <h1>HuggingMess</h1>
496
+ <div class="subtitle">Hermes Agent on Hugging Face Spaces: app gateway, OpenAI-compatible API, Telegram webhook, Cloudflare proxy, backup, and keep-awake state in one place.</div>
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 class="panel-title">API Access</div>
510
+ <pre>${escapeHtml(apiCurl)}</pre>
 
 
511
  </section>
512
  </main>
513
  </body>
 
599
 
600
  if (path === "/v1" || path.startsWith("/v1/")) {
601
  if (!isAuthorized(req)) {
602
+ if (wantsHtml(req)) {
603
+ redirect(res, loginUrl(`${path}${parsed.search}`));
604
+ return;
605
+ }
606
  res.writeHead(401, {
607
  "content-type": "application/json",
608
  "cache-control": "no-store",