Add monitoring TG service

This commit is contained in:
Grendgi
2026-06-04 14:55:41 +03:00
commit f9e072774c
74 changed files with 7232 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import { api, toast, fmtRelative } from "/api/monitoring-tg/static/js/api.js";
import { isAdmin } from "/api/monitoring-tg/static/js/access.js";
import { getVertical, getSection, sectionBase, VERTICAL_META } from "/api/monitoring-tg/static/js/vertical.js";
const V = getVertical();
const section = getSection();
const sBase = sectionBase();
const meta = VERTICAL_META[V];
function escape(s) {
if (s == null) return "";
return String(s).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
}
async function loadStats() {
const [stats, llm, queue] = await Promise.all([
api.globalStats(),
api.llmStatus().catch(() => ({ enabled: false, ready: false, model: "—" })),
api.llmQueue().catch(() => ({ pending: null })),
]);
const grid = document.getElementById("stats");
const llmBadge = llm.enabled
? (llm.ready ? `<span class="badge ok">ready</span>` : `<span class="badge warn">загружается</span>`)
: `<span class="badge off">off</span>`;
const queueValue = queue.pending == null ? "—" : queue.pending.toLocaleString();
grid.innerHTML = `
<div class="card stat"><div class="label">Каналы</div><div class="value">${stats.channels_active} / ${stats.channels_total}</div></div>
<div class="card stat"><div class="label">Сообщений всего</div><div class="value">${stats.messages_total.toLocaleString()}</div></div>
<div class="card stat"><div class="label">Сообщений за 24ч</div><div class="value">${stats.messages_last_24h.toLocaleString()}</div></div>
<div class="card stat"><div class="label">🎯 Лидов всего</div><div class="value">${(stats.leads_total ?? 0).toLocaleString()}</div></div>
<div class="card stat"><div class="label">🎯 Лидов за 24ч</div><div class="value"><a href="${sBase}/messages.html?leads_only=true">${(stats.leads_last_24h ?? 0).toLocaleString()}</a></div></div>
<div class="card stat"><div class="label">⏳ В очереди ИИ</div><div class="value">${queueValue}</div></div>
<div class="card stat"><div class="label">Период опроса</div><div class="value">${stats.poll_interval_seconds}s</div></div>
<div class="card stat"><div class="label">Последний опрос</div><div class="value">${fmtRelative(stats.last_poll_at)}</div></div>
<div class="card stat"><div class="label">Локальный ИИ</div><div class="value" style="font-size:14px">${llmBadge}<div class="muted mono" style="font-size:11px;margin-top:4px">${escape(llm.model || "")}</div></div></div>
`;
}
async function loadChannels() {
const channels = await api.listChannels();
const tbody = document.getElementById("channels-tbody");
if (!channels.length) {
tbody.innerHTML = `<tr><td colspan="5" class="empty">Каналов в этом подразделе пока нет — добавьте их на странице <a href="${sBase}/channels.html">Каналы</a></td></tr>`;
return;
}
const stats = await Promise.all(channels.map(c => api.channelStats(c.id).catch(() => null)));
tbody.innerHTML = channels.map((c, i) => {
const s = stats[i] || {};
return `
<tr>
<td>
<div><a href="${sBase}/messages.html?channel_id=${c.id}">${escape(c.title || c.identifier)}</a></div>
<div class="muted mono" style="font-size:12px">${escape(c.identifier)}</div>
</td>
<td>${(s.message_count ?? 0).toLocaleString()}</td>
<td>${fmtRelative(s.last_message_at)}</td>
<td>${fmtRelative(c.last_polled_at)}</td>
<td>${c.is_active ? '<span class="badge ok">on</span>' : '<span class="badge off">off</span>'}</td>
</tr>`;
}).join("");
}
document.getElementById("poll-all").addEventListener("click", async (e) => {
e.target.disabled = true;
try {
const res = await api.pollAll();
const scope = section ? `${meta.short} / ${section}` : meta.short;
toast(`В очереди ${res.queued ?? 0} каналов (${scope}) — опрос идёт в фоне`, "success");
await loadAll();
} catch (err) {
toast(err.message, "error");
} finally {
e.target.disabled = false;
}
});
async function loadAll() {
try {
document.getElementById("poll-all").hidden = !(await isAdmin());
await Promise.all([loadStats(), loadChannels()]);
} catch (err) {
toast(err.message, "error");
}
}
loadAll();
setInterval(loadAll, 15000);