feat: заменить прогресс-бар uptime на сегменты истории проверок
Build and deploy / build-deploy (push) Has been cancelled

Каждая сегментная полоска — одна проверка модели за последние 24ч
(зелёная = успех, красная = провал), слева старее, справа свежее.
Бэкенд отдаёт историю проверок в /api/models и обновляет её
после ручного теста.
This commit is contained in:
2026-10-21 21:53:14 +00:00
parent 76e67c68f3
commit 3f6b5768d0
2 changed files with 51 additions and 14 deletions
+16 -2
View File
@@ -22,6 +22,8 @@ OMNIROUTE_URL = os.environ.get("OMNIROUTE_URL", "http://192.168.1.101:20128")
OMNIROUTE_KEY = os.environ.get("OMNIROUTE_API_KEY", "") OMNIROUTE_KEY = os.environ.get("OMNIROUTE_API_KEY", "")
ARENA_CACHE_TEXT = os.environ.get("ARENA_CACHE_TEXT", "/data/arena_text.json") ARENA_CACHE_TEXT = os.environ.get("ARENA_CACHE_TEXT", "/data/arena_text.json")
ARENA_CACHE_AGENT = os.environ.get("ARENA_CACHE_AGENT", "/data/arena_agent.json") ARENA_CACHE_AGENT = os.environ.get("ARENA_CACHE_AGENT", "/data/arena_agent.json")
# Сколько последних проверок показывать сегментами в колонке Uptime.
HISTORY_LIMIT = int(os.environ.get("HISTORY_LIMIT", "40"))
# ─── DB setup ─────────────────────────────────────────────────────────────── # ─── DB setup ───────────────────────────────────────────────────────────────
@@ -352,7 +354,7 @@ def model_stats(db: sqlite3.Connection, model_id: str, hours: int = 24) -> dict:
).fetchone() ).fetchone()
rows = db.execute( rows = db.execute(
"SELECT ok FROM checks WHERE model_id=? AND checked_at > ?", "SELECT ok FROM checks WHERE model_id=? AND checked_at > ? ORDER BY checked_at ASC",
(model_id, cutoff) (model_id, cutoff)
).fetchall() ).fetchall()
@@ -360,12 +362,16 @@ def model_stats(db: sqlite3.Connection, model_id: str, hours: int = 24) -> dict:
success = sum(1 for r in rows if r["ok"]) success = sum(1 for r in rows if r["ok"])
pct = round(100 * success / total) if total else None pct = round(100 * success / total) if total else None
# История последних проверок (старые → новые) для сегментной витрины.
history = [bool(r["ok"]) for r in rows][-HISTORY_LIMIT:]
return { return {
"last_ok": bool(last["ok"]) if last else None, "last_ok": bool(last["ok"]) if last else None,
"last_latency_ms": last["latency_ms"] if last else None, "last_latency_ms": last["latency_ms"] if last else None,
"last_checked": last["checked_at"] if last else None, "last_checked": last["checked_at"] if last else None,
"uptime_pct": pct, "uptime_pct": pct,
"checks_count": total, "checks_count": total,
"history": history,
} }
@@ -499,8 +505,16 @@ def api_check(model_id: str):
(model_id, now, int(ok), latency), (model_id, now, int(ok), latency),
) )
db.commit() db.commit()
stats = model_stats(db, model_id)
db.close() db.close()
return {"model_id": model_id, "ok": ok, "latency_ms": latency, "checked_at": now} return {
"model_id": model_id,
"ok": ok,
"latency_ms": latency,
"checked_at": now,
"history": stats["history"],
"uptime_pct": stats["uptime_pct"],
}
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
+35 -12
View File
@@ -134,11 +134,17 @@
.uptime-low { color: var(--red); } .uptime-low { color: var(--red); }
.uptime-none { color: var(--border); } .uptime-none { color: var(--border); }
.uptime-bar-wrap { display: flex; align-items: center; gap: 6px; } .uptime-segs {
.uptime-bar { display: flex; align-items: center; gap: 2px; flex-wrap: nowrap;
width: 60px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden;
} }
.uptime-bar-fill { height: 100%; border-radius: 3px; transition: width .3s; } .uptime-seg {
width: 4px; height: 14px; border-radius: 1px; background: var(--border);
flex-shrink: 0;
}
.uptime-seg.ok { background: var(--green); }
.uptime-seg.fail { background: var(--red); }
.uptime-seg.empty { background: transparent; border: 1px dashed var(--border); box-sizing: border-box; }
.uptime-segs .uptime { margin-left: 6px; }
.check-btn { .check-btn {
background: none; border: 1px solid var(--border); border-radius: 4px; background: none; border: 1px solid var(--border); border-radius: 4px;
@@ -234,13 +240,28 @@ function statusCell(ok) {
return `<div class="status-cell"><span class="status-dot ${cls}"></span>${txt}</div>`; return `<div class="status-cell"><span class="status-dot ${cls}"></span>${txt}</div>`;
} }
function uptimeCell(pct) { function uptimeCell(pct, history) {
if (pct === null || pct === undefined) return `<span class="uptime uptime-none">—</span>`; const hist = Array.isArray(history) ? history : [];
const cls = pct >= 90 ? 'uptime-high' : pct >= 70 ? 'uptime-mid' : 'uptime-low'; if (pct === null || pct === undefined) {
const barColor = pct >= 90 ? 'var(--green)' : pct >= 70 ? 'var(--yellow)' : 'var(--red)'; if (!hist.length) return `<span class="uptime uptime-none">—</span>`;
return `<div class="uptime-bar-wrap"> }
<div class="uptime-bar"><div class="uptime-bar-fill" style="width:${pct}%;background:${barColor}"></div></div> const cls = pct === null || pct === undefined
<span class="uptime ${cls}">${pct}%</span> ? 'uptime-none'
: (pct >= 90 ? 'uptime-high' : pct >= 70 ? 'uptime-mid' : 'uptime-low');
// Сегменты: каждый — одна проверка. Слева старее, справа свежее.
const segs = hist.map(ok =>
`<span class="uptime-seg ${ok ? 'ok' : 'fail'}" title="${ok ? 'успех' : 'провал'}"></span>`
).join('');
const label = pct === null || pct === undefined ? '—' : `${pct}%`;
const tip = hist.length
? `title="последние ${hist.length} проверок: ${hist.filter(Boolean).length} успех"`
: '';
return `<div class="uptime-segs" ${tip}>
${segs || '<span class="uptime-seg empty"></span>'}
<span class="uptime ${cls}">${label}</span>
</div>`; </div>`;
} }
@@ -258,7 +279,7 @@ function renderRow(m) {
<td>${scoreCell(m.arena_text_score, m.arena_text_rank)}</td> <td>${scoreCell(m.arena_text_score, m.arena_text_rank)}</td>
<td>${scoreCell(m.arena_agent_score, m.arena_agent_rank)}</td> <td>${scoreCell(m.arena_agent_score, m.arena_agent_rank)}</td>
<td>${statusCell(m.last_ok)}</td> <td>${statusCell(m.last_ok)}</td>
<td>${uptimeCell(m.uptime_pct)}</td> <td>${uptimeCell(m.uptime_pct, m.history)}</td>
<td><button class="check-btn" onclick="checkOne('${m.id}', this)">▶ Тест</button></td> <td><button class="check-btn" onclick="checkOne('${m.id}', this)">▶ Тест</button></td>
</tr>`; </tr>`;
} }
@@ -373,6 +394,8 @@ async function checkOne(modelId, btn) {
if (idx >= 0) { if (idx >= 0) {
allData[idx].last_ok = data.ok; allData[idx].last_ok = data.ok;
allData[idx].last_latency_ms = data.latency_ms; allData[idx].last_latency_ms = data.latency_ms;
if (data.history) allData[idx].history = data.history;
if (data.uptime_pct !== undefined) allData[idx].uptime_pct = data.uptime_pct;
} }
applyFilters(); applyFilters();
} finally { } finally {