Админка: журнал «Падения туннеля» + эндпоинт /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:
69
app/main.py
69
app/main.py
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user