From 3f6b5768d0d3ff1f70391fee186ecfe80d071fb2 Mon Sep 17 00:00:00 2001 From: ai_coder Date: Wed, 21 Oct 2026 21:53:14 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D1=81=D1=81?= =?UTF-8?q?-=D0=B1=D0=B0=D1=80=20uptime=20=D0=BD=D0=B0=20=D1=81=D0=B5?= =?UTF-8?q?=D0=B3=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Каждая сегментная полоска — одна проверка модели за последние 24ч (зелёная = успех, красная = провал), слева старее, справа свежее. Бэкенд отдаёт историю проверок в /api/models и обновляет её после ручного теста. --- app/main.py | 18 +++++++++++++++-- app/static/index.html | 47 ++++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/app/main.py b/app/main.py index 4a8cf27..b7dd60f 100644 --- a/app/main.py +++ b/app/main.py @@ -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", "") 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") +# Сколько последних проверок показывать сегментами в колонке Uptime. +HISTORY_LIMIT = int(os.environ.get("HISTORY_LIMIT", "40")) # ─── DB setup ─────────────────────────────────────────────────────────────── @@ -352,7 +354,7 @@ def model_stats(db: sqlite3.Connection, model_id: str, hours: int = 24) -> dict: ).fetchone() 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) ).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"]) pct = round(100 * success / total) if total else None + # История последних проверок (старые → новые) для сегментной витрины. + history = [bool(r["ok"]) for r in rows][-HISTORY_LIMIT:] + return { "last_ok": bool(last["ok"]) if last else None, "last_latency_ms": last["latency_ms"] if last else None, "last_checked": last["checked_at"] if last else None, "uptime_pct": pct, "checks_count": total, + "history": history, } @@ -499,8 +505,16 @@ def api_check(model_id: str): (model_id, now, int(ok), latency), ) db.commit() + stats = model_stats(db, model_id) 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) diff --git a/app/static/index.html b/app/static/index.html index 6cb0edb..8ab1bca 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -134,11 +134,17 @@ .uptime-low { color: var(--red); } .uptime-none { color: var(--border); } - .uptime-bar-wrap { display: flex; align-items: center; gap: 6px; } - .uptime-bar { - width: 60px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; + .uptime-segs { + display: flex; align-items: center; gap: 2px; flex-wrap: nowrap; } - .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 { background: none; border: 1px solid var(--border); border-radius: 4px; @@ -234,13 +240,28 @@ function statusCell(ok) { return `
${txt}
`; } -function uptimeCell(pct) { - if (pct === null || pct === undefined) return `—`; - const cls = pct >= 90 ? 'uptime-high' : pct >= 70 ? 'uptime-mid' : 'uptime-low'; - const barColor = pct >= 90 ? 'var(--green)' : pct >= 70 ? 'var(--yellow)' : 'var(--red)'; - return `
-
- ${pct}% +function uptimeCell(pct, history) { + const hist = Array.isArray(history) ? history : []; + if (pct === null || pct === undefined) { + if (!hist.length) return `—`; + } + const cls = pct === null || pct === undefined + ? 'uptime-none' + : (pct >= 90 ? 'uptime-high' : pct >= 70 ? 'uptime-mid' : 'uptime-low'); + + // Сегменты: каждый — одна проверка. Слева старее, справа свежее. + const segs = hist.map(ok => + `` + ).join(''); + + const label = pct === null || pct === undefined ? '—' : `${pct}%`; + const tip = hist.length + ? `title="последние ${hist.length} проверок: ${hist.filter(Boolean).length} успех"` + : ''; + + return `
+ ${segs || ''} + ${label}
`; } @@ -258,7 +279,7 @@ function renderRow(m) { ${scoreCell(m.arena_text_score, m.arena_text_rank)} ${scoreCell(m.arena_agent_score, m.arena_agent_rank)} ${statusCell(m.last_ok)} - ${uptimeCell(m.uptime_pct)} + ${uptimeCell(m.uptime_pct, m.history)} `; } @@ -373,6 +394,8 @@ async function checkOne(modelId, btn) { if (idx >= 0) { allData[idx].last_ok = data.ok; 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(); } finally {