Админка: журнал «Падения туннеля» + эндпоинт /api/tunnel-event

Таблица WEB_TUNNEL_EVENTS (ts/bad/bad2/recovered). Health-check на vds178 шлёт
событие на КАЖДЫЙ авто-рестарт цепочки -51 (auth ALERT_TOKEN). В админке раздел
с датой/временем и признаком, помог ли рестарт (24ч/7дн + сколько не восстановилось).
Telegram при этом остаётся тихим (только неустранённые простои).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
berkio_admin_gitea
2026-07-19 10:24:39 +00:00
parent ccacbb123c
commit 464d412de9

View File

@@ -351,6 +351,16 @@ def _db_ensure_auth_table():
PRIMARY KEY (day, event)
)
""")
# Падения туннеля -51 (health-check на vds178): дата/время + помог ли авто-рестарт.
con.execute("""
CREATE TABLE IF NOT EXISTS WEB_TUNNEL_EVENTS (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts REAL,
bad INTEGER,
bad2 INTEGER,
recovered INTEGER
)
""")
# Показы пейвола: по каждому придержанному файлу — кто (account/user) и какой файл.
con.execute("""
CREATE TABLE IF NOT EXISTS WEB_PAYWALL_EVENTS (
@@ -3225,6 +3235,12 @@ _ADMIN_HTML = """<!doctype html>
<tbody id="t-paywall"></tbody></table></div>
</section>
<section>
<h2>Падения туннеля <span class="muted" id="tunnel-note"></span></h2>
<div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Плохих проб</th><th>Авто-рестарт</th></tr></thead>
<tbody id="t-tunnel"></tbody></table></div>
</section>
<section>
<h2>По дням</h2>
<div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Ср/стр</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead>
@@ -3434,6 +3450,19 @@ _ADMIN_HTML = """<!doctype html>
'</td><td>'+esc(e.source||'')+'</td><td><span class="pill '+st[0]+'">'+esc(st[1])+'</span></td></tr>';
}).join('') : '<tr><td colspan="7" class="muted">Пока не было показов пейвола</td></tr>';
// Падения туннеля (журнал health-check vds178)
var tun = (d.tunnel && d.tunnel.recent) || [];
document.getElementById('tunnel-note').textContent = d.tunnel
? ('· 24ч: ' + d.tunnel.count_24h + ' · 7дн: ' + d.tunnel.count_7d +
(d.tunnel.failed_7d ? ' (не восстановилось: ' + d.tunnel.failed_7d + ')' : ''))
: '';
document.getElementById('t-tunnel').innerHTML = tun.length ? tun.map(function(e){
var ok = e.recovered
? '<span class="pill ok">восстановился сам</span>'
: '<span class="pill cancel">рестарт не помог</span>';
return '<tr><td>'+dts(e.ts)+'</td><td class="num">'+(e.bad||0)+'/3</td><td>'+ok+'</td></tr>';
}).join('') : '<tr><td colspan="3" class="muted">Падений туннеля не зафиксировано</td></tr>';
// По дням (свежие сверху)
var daily = (d.daily||[]).slice().reverse();
document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){
@@ -3693,6 +3722,33 @@ async def api_alert(payload: dict):
return {"ok": True}
@app.post("/api/tunnel-event")
async def api_tunnel_event(payload: dict):
"""Событие падения туннеля -51 с vds178 (health-check) → журнал для админки.
Пишется на КАЖДЫЙ авто-рестарт (в отличие от Telegram-алерта). Авторизация — ALERT_TOKEN."""
if not ALERT_TOKEN:
raise HTTPException(status_code=503, detail="alerts not configured")
tok = str((payload or {}).get("token") or "")
if not hmac.compare_digest(tok, ALERT_TOKEN):
raise HTTPException(status_code=401, detail="unauthorized")
def _int(k):
try:
return int((payload or {}).get(k) or 0)
except (TypeError, ValueError):
return 0
bad, bad2 = _int("bad"), _int("bad2")
def _ins():
with sqlite3.connect(DB_PATH, timeout=10) as con:
con.execute(
"INSERT INTO WEB_TUNNEL_EVENTS (ts, bad, bad2, recovered) VALUES (?,?,?,?)",
(time.time(), bad, bad2, 1 if bad2 < 2 else 0))
con.commit()
await asyncio.to_thread(_ins)
return {"ok": True}
def _require_admin(request: Request) -> bool:
if not ADMIN_TOKEN:
return False
@@ -3804,6 +3860,19 @@ def _admin_collect_stats() -> dict:
"FROM WEB_PAYWALL_EVENTS ORDER BY ts DESC LIMIT 100")]
except Exception:
out["paywall_events"] = []
# Падения туннеля -51 (журнал health-check vds178)
try:
tun = [dict(r) for r in con.execute(
"SELECT ts, bad, bad2, recovered FROM WEB_TUNNEL_EVENTS ORDER BY ts DESC LIMIT 100")]
t24 = con.execute("SELECT COUNT(*) c FROM WEB_TUNNEL_EVENTS WHERE ts >= ?",
(now - 86400,)).fetchone()["c"]
t7 = con.execute("SELECT COUNT(*) c FROM WEB_TUNNEL_EVENTS WHERE ts >= ?",
(now - 7 * 86400,)).fetchone()["c"]
tfail7 = con.execute("SELECT COUNT(*) c FROM WEB_TUNNEL_EVENTS WHERE recovered=0 AND ts >= ?",
(now - 7 * 86400,)).fetchone()["c"]
out["tunnel"] = {"recent": tun, "count_24h": t24, "count_7d": t7, "failed_7d": tfail7}
except Exception:
out["tunnel"] = {"recent": [], "count_24h": 0, "count_7d": 0, "failed_7d": 0}
# Документы
cols = {r[1] for r in con.execute("PRAGMA table_info(FILES)")}
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"