Doom01 commited on
Commit
37c0f3a
Β·
verified Β·
1 Parent(s): 77b8687

Update health-server.js

Browse files
Files changed (1) hide show
  1. health-server.js +470 -20
health-server.js CHANGED
@@ -1,3 +1,4 @@
 
1
  const express = require('express');
2
  const { createProxyMiddleware } = require('http-proxy-middleware');
3
  const fetch = require('node-fetch');
@@ -6,57 +7,501 @@ const app = express();
6
  const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || 'changeme';
7
  const LLAMA_URL = 'http://127.0.0.1:8080';
8
  const PORT = 7860;
 
 
9
 
 
10
  function requireAuth(req, res, next) {
11
  const auth = req.headers['authorization'] || '';
12
  const token = auth.replace('Bearer ', '').trim();
13
- if (token !== GATEWAY_TOKEN) {
14
- return res.status(401).json({ error: 'Unauthorized' });
15
- }
16
  next();
17
  }
18
 
19
- app.get('/', (req, res) => {
20
- res.send(`<h1>🦾 LLM Space Gateway</h1><p>Status: Running</p>`);
21
- });
 
 
 
 
 
 
 
22
 
 
23
  app.get('/health', async (req, res) => {
24
  try {
25
  const controller = new AbortController();
26
  const timeout = setTimeout(() => controller.abort(), 5000);
27
-
28
  const resp = await fetch(`${LLAMA_URL}/v1/models`, {
29
  headers: { 'Authorization': `Bearer ${GATEWAY_TOKEN}` },
30
  signal: controller.signal
31
  });
32
  clearTimeout(timeout);
33
-
34
- if (resp.ok) {
35
- return res.status(200).json({ status: 'ok', llm: 'ready' });
36
- }
37
  return res.status(503).json({ status: 'degraded', code: resp.status });
38
  } catch (e) {
39
  return res.status(503).json({ status: 'unavailable', error: e.message });
40
  }
41
  });
42
 
43
- // FIX #10: Added body size limit (1mb) to prevent DoS via oversized payloads.
44
- // FIX #12: Telegram webhook is intentionally open (Telegram needs to POST here
45
- // without auth). Kept as-is but capped body size for safety.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  app.use('/telegram',
47
  express.json({ limit: '1mb' }),
48
  async (req, res) => {
49
  if (req.method !== 'POST') return res.status(405).send('Method Not Allowed');
50
-
51
- // Basic sanity-check: Telegram updates always have update_id
52
  if (!req.body || typeof req.body.update_id !== 'number') {
53
  return res.status(400).json({ ok: false, error: 'Invalid update payload' });
54
  }
55
-
56
  try {
57
  const { handleTelegramUpdate } = require('./telegram-bot');
58
  await handleTelegramUpdate(req.body);
59
- // Always return 200 to Telegram to prevent retries
60
  res.json({ ok: true });
61
  } catch (e) {
62
  console.error('Webhook Endpoint Error:', e.message);
@@ -65,7 +510,11 @@ app.use('/telegram',
65
  }
66
  );
67
 
68
- app.use('/v1', requireAuth, createProxyMiddleware({
 
 
 
 
69
  target: LLAMA_URL,
70
  changeOrigin: true,
71
  on: {
@@ -75,6 +524,7 @@ app.use('/v1', requireAuth, createProxyMiddleware({
75
  }
76
  }));
77
 
 
78
  app.listen(PORT, '0.0.0.0', () => {
79
  console.log(`🌐 Gateway running on port ${PORT}`);
80
- });
 
1
+ 'use strict';
2
  const express = require('express');
3
  const { createProxyMiddleware } = require('http-proxy-middleware');
4
  const fetch = require('node-fetch');
 
7
  const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || 'changeme';
8
  const LLAMA_URL = 'http://127.0.0.1:8080';
9
  const PORT = 7860;
10
+ const START_TIME = Date.now();
11
+ let proxiedCount = 0;
12
 
13
+ // ── Helpers ───────────────────────────────────────────────────────────────────
14
  function requireAuth(req, res, next) {
15
  const auth = req.headers['authorization'] || '';
16
  const token = auth.replace('Bearer ', '').trim();
17
+ if (token !== GATEWAY_TOKEN) return res.status(401).json({ error: 'Unauthorized' });
 
 
18
  next();
19
  }
20
 
21
+ function formatUptime(ms) {
22
+ const s = Math.floor(ms / 1000);
23
+ const m = Math.floor(s / 60);
24
+ const h = Math.floor(m / 60);
25
+ const d = Math.floor(h / 24);
26
+ if (d > 0) return d + 'd ' + (h % 24) + 'h';
27
+ if (h > 0) return h + 'h ' + (m % 60) + 'm';
28
+ if (m > 0) return m + 'm ' + (s % 60) + 's';
29
+ return s + 's';
30
+ }
31
 
32
+ // ── /health ───────────────────────────────────────────────────────────────────
33
  app.get('/health', async (req, res) => {
34
  try {
35
  const controller = new AbortController();
36
  const timeout = setTimeout(() => controller.abort(), 5000);
 
37
  const resp = await fetch(`${LLAMA_URL}/v1/models`, {
38
  headers: { 'Authorization': `Bearer ${GATEWAY_TOKEN}` },
39
  signal: controller.signal
40
  });
41
  clearTimeout(timeout);
42
+ if (resp.ok) return res.status(200).json({ status: 'ok', llm: 'ready' });
 
 
 
43
  return res.status(503).json({ status: 'degraded', code: resp.status });
44
  } catch (e) {
45
  return res.status(503).json({ status: 'unavailable', error: e.message });
46
  }
47
  });
48
 
49
+ // ── /stats (consumed by the dashboard and external monitors) ──────────────────
50
+ app.get('/stats', async (req, res) => {
51
+ let llmOnline = false;
52
+ let modelId = process.env.MODEL_FILENAME || 'Unknown';
53
+
54
+ try {
55
+ const ctrl = new AbortController();
56
+ const t = setTimeout(() => ctrl.abort(), 3000);
57
+ const r = await fetch(`${LLAMA_URL}/v1/models`, {
58
+ headers: { 'Authorization': `Bearer ${GATEWAY_TOKEN}` },
59
+ signal: ctrl.signal
60
+ });
61
+ clearTimeout(t);
62
+ if (r.ok) {
63
+ llmOnline = true;
64
+ const j = await r.json();
65
+ if (j.data && j.data[0] && j.data[0].id) modelId = j.data[0].id;
66
+ }
67
+ } catch (_) {}
68
+
69
+ const bs = global.botState || {};
70
+
71
+ res.json({
72
+ llmOnline,
73
+ modelId,
74
+ contextLength: parseInt(process.env.CONTEXT_LENGTH || '2048'),
75
+ threads: process.env.CPU_THREADS || 'auto',
76
+ uptime: formatUptime(Date.now() - START_TIME),
77
+ uptimeMs: Date.now() - START_TIME,
78
+ proxiedCount,
79
+ telegramConfigured: !!process.env.TELEGRAM_BOT_TOKEN,
80
+ cfConfigured: !!(process.env.CLOUDFLARE_WORKERS_TOKEN && process.env.CLOUDFLARE_ACCOUNT_ID),
81
+ proxyUrl: process.env.CLOUDFLARE_TELEGRAM_PROXY_URL || null,
82
+ spaceUrl: process.env.SPACE_URL || null,
83
+ backupEnabled: !!(process.env.HF_TOKEN && process.env.HF_BACKUP_DATASET),
84
+ backupDataset: process.env.HF_BACKUP_DATASET || null,
85
+ totalMessages: bs.totalMessages || 0,
86
+ lastMessageAt: bs.lastMessageAt || null,
87
+ activeChats: bs.activeChats || 0,
88
+ });
89
+ });
90
+
91
+ // ── / (Dashboard) ─────────────────────────────────────────────────────────────
92
+ const DASHBOARD_HTML = `<!DOCTYPE html>
93
+ <html lang="en">
94
+ <head>
95
+ <meta charset="UTF-8">
96
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
+ <title>LLM Space</title>
98
+ <style>
99
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
100
+
101
+ :root {
102
+ --bg: #06080f;
103
+ --surface: #0c1018;
104
+ --border: #161d2e;
105
+ --border-hi: #1e2840;
106
+ --text: #d4dbe8;
107
+ --dim: #475569;
108
+ --green: #22c55e;
109
+ --amber: #f59e0b;
110
+ --red: #ef4444;
111
+ --purple: #a78bfa;
112
+ --blue: #38bdf8;
113
+ }
114
+
115
+ body {
116
+ background: var(--bg);
117
+ color: var(--text);
118
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
119
+ min-height: 100vh;
120
+ padding: 1.75rem 1.5rem 3rem;
121
+ }
122
+
123
+ /* ── Header ── */
124
+ .header {
125
+ display: flex;
126
+ justify-content: space-between;
127
+ align-items: flex-end;
128
+ margin-bottom: 2rem;
129
+ padding-bottom: 1.25rem;
130
+ border-bottom: 1px solid var(--border);
131
+ }
132
+ .logo { display: flex; align-items: center; gap: 0.75rem; }
133
+ .logo-icon {
134
+ width: 36px; height: 36px;
135
+ background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%);
136
+ border-radius: 8px;
137
+ display: flex; align-items: center; justify-content: center;
138
+ font-size: 1rem;
139
+ }
140
+ .logo-text { font-size: 1.15rem; font-weight: 700; letter-spacing: -0.3px; color: #f1f5f9; }
141
+ .logo-text span { color: var(--purple); }
142
+ .logo-sub { font-size: 0.7rem; color: var(--dim); margin-top: 1px; letter-spacing: 0.05em; }
143
+
144
+ .live-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.7rem; color: var(--dim); letter-spacing: 0.1em; }
145
+ .pulse-dot {
146
+ width: 7px; height: 7px; border-radius: 50%;
147
+ background: var(--green);
148
+ box-shadow: 0 0 0 0 rgba(34,197,94,0.4);
149
+ animation: ping 2s infinite;
150
+ }
151
+ @keyframes ping {
152
+ 0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.4); }
153
+ 50% { box-shadow: 0 0 0 7px rgba(34,197,94,0); }
154
+ }
155
+
156
+ /* ── Grid ── */
157
+ .grid {
158
+ display: grid;
159
+ grid-template-columns: repeat(2, 1fr);
160
+ gap: 0.875rem;
161
+ }
162
+ @media (max-width: 560px) { .grid { grid-template-columns: 1fr; } }
163
+
164
+ /* ── Card ── */
165
+ .card {
166
+ background: var(--surface);
167
+ border: 1px solid var(--border);
168
+ border-radius: 10px;
169
+ padding: 1.1rem 1.1rem 1rem;
170
+ transition: border-color 0.2s;
171
+ }
172
+ .card:hover { border-color: var(--border-hi); }
173
+
174
+ .card-top {
175
+ display: flex;
176
+ justify-content: space-between;
177
+ align-items: center;
178
+ margin-bottom: 0.9rem;
179
+ }
180
+ .card-label {
181
+ font-size: 0.6rem;
182
+ font-weight: 700;
183
+ letter-spacing: 0.18em;
184
+ text-transform: uppercase;
185
+ color: var(--dim);
186
+ }
187
+
188
+ /* Status dot */
189
+ .dot {
190
+ width: 7px; height: 7px; border-radius: 50%;
191
+ background: var(--dim);
192
+ transition: background 0.3s, box-shadow 0.3s;
193
+ }
194
+ .dot.green { background: var(--green); box-shadow: 0 0 6px var(--green); }
195
+ .dot.amber { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
196
+ .dot.red { background: var(--red); box-shadow: 0 0 6px var(--red); }
197
+
198
+ /* Badge */
199
+ .badge {
200
+ display: inline-block;
201
+ padding: 0.2rem 0.55rem;
202
+ border-radius: 20px;
203
+ font-size: 0.62rem;
204
+ font-weight: 700;
205
+ letter-spacing: 0.1em;
206
+ text-transform: uppercase;
207
+ }
208
+ .badge.green { background: rgba(34,197,94,0.12); color: var(--green); border: 1px solid rgba(34,197,94,0.25); }
209
+ .badge.amber { background: rgba(245,158,11,0.12); color: var(--amber); border: 1px solid rgba(245,158,11,0.25); }
210
+ .badge.red { background: rgba(239,68,68,0.12); color: var(--red); border: 1px solid rgba(239,68,68,0.25); }
211
+ .badge.purple { background: rgba(167,139,250,0.12);color: var(--purple); border: 1px solid rgba(167,139,250,0.25);}
212
+ .badge.blue { background: rgba(56,189,248,0.12); color: var(--blue); border: 1px solid rgba(56,189,248,0.25); }
213
+ .badge.gray { background: rgba(71,85,105,0.15); color: var(--dim); border: 1px solid rgba(71,85,105,0.3); }
214
+
215
+ /* Value + sub */
216
+ .card-val {
217
+ font-size: 1.6rem;
218
+ font-weight: 700;
219
+ color: #f1f5f9;
220
+ line-height: 1;
221
+ margin-bottom: 0.3rem;
222
+ font-family: system-ui, -apple-system, sans-serif;
223
+ }
224
+ .card-val.mono { font-family: inherit; font-size: 1.1rem; }
225
+ .card-sub { font-size: 0.72rem; color: var(--dim); line-height: 1.5; }
226
+
227
+ /* Chip (for URLs / model paths) */
228
+ .chip {
229
+ display: inline-block;
230
+ margin-top: 0.6rem;
231
+ background: #0a0e18;
232
+ border: 1px solid var(--border);
233
+ border-radius: 5px;
234
+ padding: 0.22rem 0.5rem;
235
+ font-size: 0.68rem;
236
+ color: #64748b;
237
+ word-break: break-all;
238
+ max-width: 100%;
239
+ overflow: hidden;
240
+ text-overflow: ellipsis;
241
+ white-space: nowrap;
242
+ }
243
+
244
+ /* Divider between badge row and value */
245
+ .card-body { margin-top: 0.75rem; }
246
+
247
+ /* ── Footer ── */
248
+ .footer {
249
+ margin-top: 2rem;
250
+ text-align: center;
251
+ font-size: 0.65rem;
252
+ color: #1e2840;
253
+ letter-spacing: 0.05em;
254
+ }
255
+ </style>
256
+ </head>
257
+ <body>
258
+
259
+ <header class="header">
260
+ <div class="logo">
261
+ <div class="logo-icon">πŸ€–</div>
262
+ <div>
263
+ <div class="logo-text"><span>LLM</span> Space</div>
264
+ <div class="logo-sub">Inference Gateway Β· Port 7860</div>
265
+ </div>
266
+ </div>
267
+ <div class="live-row">
268
+ <div class="pulse-dot"></div>
269
+ <span id="refresh-label">LIVE</span>
270
+ </div>
271
+ </header>
272
+
273
+ <div class="grid">
274
+
275
+ <!-- Card 1: Inference Engine -->
276
+ <div class="card">
277
+ <div class="card-top">
278
+ <span class="card-label">Inference Engine</span>
279
+ <div class="dot" id="llm-dot"></div>
280
+ </div>
281
+ <div id="llm-badge" class="badge gray">Checking…</div>
282
+ <div class="card-body">
283
+ <div class="card-val mono">llama.cpp</div>
284
+ <div class="card-sub">Port 8080 Β· Bearer protected</div>
285
+ </div>
286
+ </div>
287
+
288
+ <!-- Card 2: Model -->
289
+ <div class="card">
290
+ <div class="card-top">
291
+ <span class="card-label">Model</span>
292
+ <div class="dot green" id="model-dot"></div>
293
+ </div>
294
+ <div class="card-body">
295
+ <div class="card-val mono" id="model-name" style="font-size:0.95rem">β€”</div>
296
+ <div class="card-sub" id="model-sub">Loading…</div>
297
+ <div class="chip" id="model-chip" style="display:none"></div>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Card 3: Uptime -->
302
+ <div class="card">
303
+ <div class="card-top">
304
+ <span class="card-label">Uptime</span>
305
+ <div class="dot green"></div>
306
+ </div>
307
+ <div class="card-body">
308
+ <div class="card-val" id="uptime-val">β€”</div>
309
+ <div class="card-sub" id="uptime-sub">Port 7860 Β· Express</div>
310
+ </div>
311
+ </div>
312
+
313
+ <!-- Card 4: Telegram -->
314
+ <div class="card">
315
+ <div class="card-top">
316
+ <span class="card-label">Telegram</span>
317
+ <div class="dot" id="tg-dot"></div>
318
+ </div>
319
+ <div id="tg-badge" class="badge gray">Checking…</div>
320
+ <div class="card-body">
321
+ <div class="card-val mono" id="tg-val" style="font-size:1rem">β€”</div>
322
+ <div class="card-sub" id="tg-sub"></div>
323
+ </div>
324
+ </div>
325
+
326
+ <!-- Card 5: Memory Sync -->
327
+ <div class="card">
328
+ <div class="card-top">
329
+ <span class="card-label">Memory Sync</span>
330
+ <div class="dot" id="sync-dot"></div>
331
+ </div>
332
+ <div id="sync-badge" class="badge gray">Checking…</div>
333
+ <div class="card-body">
334
+ <div class="card-val mono" id="sync-val" style="font-size:1rem">β€”</div>
335
+ <div class="chip" id="sync-chip" style="display:none"></div>
336
+ </div>
337
+ </div>
338
+
339
+ <!-- Card 6: Keep Alive -->
340
+ <div class="card">
341
+ <div class="card-top">
342
+ <span class="card-label">Keep Alive</span>
343
+ <div class="dot" id="ka-dot"></div>
344
+ </div>
345
+ <div id="ka-badge" class="badge gray">Checking…</div>
346
+ <div class="card-body">
347
+ <div class="card-val mono" id="ka-val" style="font-size:1rem">β€”</div>
348
+ <div class="chip" id="ka-chip" style="display:none"></div>
349
+ </div>
350
+ </div>
351
+
352
+ </div>
353
+
354
+ <div class="footer" id="footer-ts">Initialising…</div>
355
+
356
+ <script>
357
+ function el(id) { return document.getElementById(id); }
358
+
359
+ function setBadge(id, cls, label) {
360
+ var e = el(id);
361
+ if (!e) return;
362
+ e.className = 'badge ' + cls;
363
+ e.textContent = label;
364
+ }
365
+
366
+ function setDot(id, cls) {
367
+ var e = el(id);
368
+ if (!e) return;
369
+ e.className = 'dot ' + cls;
370
+ }
371
+
372
+ function setText(id, txt) {
373
+ var e = el(id);
374
+ if (e) e.textContent = txt;
375
+ }
376
+
377
+ function setChip(id, txt) {
378
+ var e = el(id);
379
+ if (!e) return;
380
+ if (txt) { e.textContent = txt; e.style.display = 'inline-block'; }
381
+ else { e.style.display = 'none'; }
382
+ }
383
+
384
+ function shortModel(s) {
385
+ if (!s || s === 'Unknown') return 'β€”';
386
+ var parts = s.split('/');
387
+ var name = parts[parts.length - 1];
388
+ // Trim .gguf extension
389
+ return name.replace(/\.gguf$/i, '');
390
+ }
391
+
392
+ function timeSince(ts) {
393
+ if (!ts) return 'never';
394
+ var diff = Math.floor((Date.now() - ts) / 1000);
395
+ if (diff < 60) return diff + 's ago';
396
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
397
+ return Math.floor(diff / 3600) + 'h ago';
398
+ }
399
+
400
+ async function refresh() {
401
+ try {
402
+ var r = await fetch('/stats');
403
+ var d = await r.json();
404
+
405
+ // ── Inference Engine ──────────────────────────────────────────────────
406
+ if (d.llmOnline) {
407
+ setBadge('llm-badge', 'green', 'Online');
408
+ setDot('llm-dot', 'green');
409
+ } else {
410
+ setBadge('llm-badge', 'red', 'Offline');
411
+ setDot('llm-dot', 'red');
412
+ }
413
+
414
+ // ── Model ─────────────────────────────────────────────────────────────
415
+ var short = shortModel(d.modelId);
416
+ setText('model-name', short || 'β€”');
417
+ setText('model-sub', 'ctx ' + d.contextLength + ' Β· ' + (d.threads || 'auto') + ' threads');
418
+ if (d.modelId && d.modelId.length > 20) {
419
+ setChip('model-chip', d.modelId);
420
+ }
421
+
422
+ // ── Uptime ────────────────────────────────────────────────────────────
423
+ setText('uptime-val', d.uptime || 'β€”');
424
+ var msgPart = d.totalMessages > 0
425
+ ? d.totalMessages + ' messages Β· ' + d.activeChats + ' chats'
426
+ : 'No messages yet';
427
+ setText('uptime-sub', msgPart);
428
+
429
+ // ── Telegram ──────────────────────────────────────────────────────────
430
+ if (d.telegramConfigured) {
431
+ setBadge('tg-badge', 'green', 'Configured');
432
+ setDot('tg-dot', 'green');
433
+ setText('tg-val', 'Bot Active');
434
+ var tgSub = d.proxyUrl ? 'Webhook via CF proxy' : 'Direct webhook';
435
+ if (d.lastMessageAt) tgSub += ' Β· last msg ' + timeSince(d.lastMessageAt);
436
+ setText('tg-sub', tgSub);
437
+ } else {
438
+ setBadge('tg-badge', 'gray', 'Not Set');
439
+ setDot('tg-dot', 'gray');
440
+ setText('tg-val', 'No Bot Token');
441
+ setText('tg-sub', 'Set TELEGRAM_BOT_TOKEN in Space secrets');
442
+ }
443
+
444
+ // ── Memory Sync ───────────────────────────────────────────────────────
445
+ if (d.backupEnabled) {
446
+ setBadge('sync-badge', 'blue', 'HF Dataset');
447
+ setDot('sync-dot', 'green');
448
+ setText('sync-val', 'Enabled');
449
+ setChip('sync-chip', d.backupDataset);
450
+ } else {
451
+ setBadge('sync-badge', 'gray', 'Disabled');
452
+ setDot('sync-dot', 'gray');
453
+ setText('sync-val', 'No Backup');
454
+ setChip('sync-chip', null);
455
+ }
456
+
457
+ // ── Keep Alive ────────────────────────────────────────────────────────
458
+ if (d.cfConfigured) {
459
+ setBadge('ka-badge', 'purple', 'CF Cron');
460
+ setDot('ka-dot', 'green');
461
+ setText('ka-val', 'Active');
462
+ setChip('ka-chip', d.spaceUrl ? d.spaceUrl + '/health' : null);
463
+ } else {
464
+ setBadge('ka-badge', 'gray', 'Disabled');
465
+ setDot('ka-dot', 'gray');
466
+ setText('ka-val', 'Not Set');
467
+ setChip('ka-chip', null);
468
+ }
469
+
470
+ // ── Footer timestamp ──────────────────────────────────────────────────
471
+ var t = new Date();
472
+ var ts = t.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
473
+ setText('refresh-label', 'LIVE');
474
+ setText('footer-ts', 'Last updated ' + ts + ' Β· refreshes every 30s');
475
+
476
+ } catch (e) {
477
+ setText('refresh-label', 'ERR');
478
+ console.error('Stats fetch failed:', e);
479
+ }
480
+ }
481
+
482
+ refresh();
483
+ setInterval(refresh, 30000);
484
+ </script>
485
+
486
+ </body>
487
+ </html>`;
488
+
489
+ app.get('/', (req, res) => {
490
+ res.setHeader('Content-Type', 'text/html');
491
+ res.send(DASHBOARD_HTML);
492
+ });
493
+
494
+ // ── /telegram (Webhook, intentionally open) ───────────────────────────────────
495
  app.use('/telegram',
496
  express.json({ limit: '1mb' }),
497
  async (req, res) => {
498
  if (req.method !== 'POST') return res.status(405).send('Method Not Allowed');
 
 
499
  if (!req.body || typeof req.body.update_id !== 'number') {
500
  return res.status(400).json({ ok: false, error: 'Invalid update payload' });
501
  }
 
502
  try {
503
  const { handleTelegramUpdate } = require('./telegram-bot');
504
  await handleTelegramUpdate(req.body);
 
505
  res.json({ ok: true });
506
  } catch (e) {
507
  console.error('Webhook Endpoint Error:', e.message);
 
510
  }
511
  );
512
 
513
+ // ── /v1 (Authenticated LLM proxy) ────────────────────────────────────────────
514
+ app.use('/v1', requireAuth, (req, res, next) => {
515
+ proxiedCount++;
516
+ next();
517
+ }, createProxyMiddleware({
518
  target: LLAMA_URL,
519
  changeOrigin: true,
520
  on: {
 
524
  }
525
  }));
526
 
527
+ // ── Start ─────────────────────────────────────────────────────────────────────
528
  app.listen(PORT, '0.0.0.0', () => {
529
  console.log(`🌐 Gateway running on port ${PORT}`);
530
+ });