Update health-server.js
Browse files- 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 |
-
|
| 20 |
-
|
| 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 |
-
//
|
| 44 |
-
/
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
});
|