Spaces:
Sleeping
Sleeping
Commit ·
dae8de8
1
Parent(s): 57babb2
feat: live time-since / time-until-next-probe counter in admin panel
Browse filesThe "Updated 13:11:51" label was a static wall-clock string set once at
fetch time. Hard to tell at a glance how stale the snapshot is.
Now the label updates every second and shows three things:
Updated <wall-clock> · <Xm Ys ago> · next auto-probe in <Ym Ss>
- "since" uses STATE.health.updated_at (the actual backend probe time
via /api/admin/health), falling back to local fetch time if missing.
- "next auto-probe in" = last-probe-time + 5min cadence (the value
defined by PROBE_INTERVAL_MS, which mirrors backend/llm_health.py's
background_probe_loop interval).
- setInterval(renderUpdatedLabel, 1000) keeps both counters live.
- Brace balance verified (281/281).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontend/public/admin/llm-control.html
CHANGED
|
@@ -389,7 +389,8 @@
|
|
| 389 |
password: '',
|
| 390 |
health: null, // { models: [ { model, status, latency_ms, last_error, last_success } ], counters: { healthy, degraded, down } }
|
| 391 |
chains: null, // { brain: [...], fast_brain: [...], judge: [...] }
|
| 392 |
-
usage: null
|
|
|
|
| 393 |
};
|
| 394 |
var ROLES = ['brain', 'fast_brain', 'judge'];
|
| 395 |
var ROLE_COLORS = {
|
|
@@ -896,13 +897,56 @@
|
|
| 896 |
return null;
|
| 897 |
}
|
| 898 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
function setLastUpdated() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 900 |
var el = $('last-updated');
|
| 901 |
if (!el) return;
|
| 902 |
-
var
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
}
|
| 905 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
// ----- Fetchers -----
|
| 907 |
function fetchHealth() {
|
| 908 |
return apiGet('/api/admin/health').then(function (data) {
|
|
|
|
| 389 |
password: '',
|
| 390 |
health: null, // { models: [ { model, status, latency_ms, last_error, last_success } ], counters: { healthy, degraded, down } }
|
| 391 |
chains: null, // { brain: [...], fast_brain: [...], judge: [...] }
|
| 392 |
+
usage: null, // { brain: {...}, fast_brain: {...}, judge: {...} } from /api/admin/usage
|
| 393 |
+
lastUpdatedAt: null // ms-since-epoch of the most recent successful snapshot fetch
|
| 394 |
};
|
| 395 |
var ROLES = ['brain', 'fast_brain', 'judge'];
|
| 396 |
var ROLE_COLORS = {
|
|
|
|
| 897 |
return null;
|
| 898 |
}
|
| 899 |
|
| 900 |
+
// Backend's background probe runs on this cadence (see
|
| 901 |
+
// backend/llm_health.py::background_probe_loop — currently 5 min).
|
| 902 |
+
var PROBE_INTERVAL_MS = 5 * 60 * 1000;
|
| 903 |
+
|
| 904 |
function setLastUpdated() {
|
| 905 |
+
STATE.lastUpdatedAt = Date.now();
|
| 906 |
+
renderUpdatedLabel();
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
function formatMinSec(ms) {
|
| 910 |
+
if (ms < 0) ms = 0;
|
| 911 |
+
var s = Math.floor(ms / 1000);
|
| 912 |
+
if (s < 60) return s + 's';
|
| 913 |
+
var m = Math.floor(s / 60);
|
| 914 |
+
var rem = s % 60;
|
| 915 |
+
if (m < 60) return rem > 0 ? (m + 'm ' + rem + 's') : (m + 'm');
|
| 916 |
+
var h = Math.floor(m / 60);
|
| 917 |
+
return h + 'h ' + (m % 60) + 'm';
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
// Source-of-truth for "when the probe data was last current" is the
|
| 921 |
+
// backend's updated_at (ISO string from /api/admin/health). Falls back
|
| 922 |
+
// to local fetch time if the field is missing.
|
| 923 |
+
function probeTimestampMs() {
|
| 924 |
+
if (STATE.health && STATE.health.updated_at) {
|
| 925 |
+
var t = Date.parse(STATE.health.updated_at);
|
| 926 |
+
if (!isNaN(t)) return t;
|
| 927 |
+
}
|
| 928 |
+
return STATE.lastUpdatedAt;
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
function renderUpdatedLabel() {
|
| 932 |
var el = $('last-updated');
|
| 933 |
if (!el) return;
|
| 934 |
+
var probeAt = probeTimestampMs();
|
| 935 |
+
if (!probeAt) return;
|
| 936 |
+
var now = Date.now();
|
| 937 |
+
var since = formatMinSec(now - probeAt);
|
| 938 |
+
// Next auto-probe = last probe + cadence. If that's already in the
|
| 939 |
+
// past (probe loop running slow or paused), clamp to "due now".
|
| 940 |
+
var nextAt = probeAt + PROBE_INTERVAL_MS;
|
| 941 |
+
var untilNext = nextAt > now ? formatMinSec(nextAt - now) : 'due now';
|
| 942 |
+
var wallClock = new Date(probeAt).toLocaleTimeString();
|
| 943 |
+
el.textContent = 'Updated ' + wallClock + ' · ' + since + ' ago · next auto-probe in ' + untilNext;
|
| 944 |
}
|
| 945 |
|
| 946 |
+
// Tick the relative portion every second so the user sees how stale
|
| 947 |
+
// the snapshot is in real time without having to click Refresh.
|
| 948 |
+
setInterval(renderUpdatedLabel, 1000);
|
| 949 |
+
|
| 950 |
// ----- Fetchers -----
|
| 951 |
function fetchHealth() {
|
| 952 |
return apiGet('/api/admin/health').then(function (data) {
|