feat(webui): scroll-paged record table, pool metrics, probe-aware health tags

Front-end half of the on-demand log loading plus the observability for the two
new scheduling mechanisms.

Records table:
  * the first screen comes from the dashboard poll, and an IntersectionObserver
    sentinel below the last row pulls the next page from /api/stats/records as
    the user scrolls.
  * rendered rows are capped at RECS_MAX_DOM=1000 (oldest rendered rows are
    dropped) so a long scroll cannot grow the DOM without bound, with a "back
    to newest" button to reset cheaply.
  * releaseRecords() drops the buffer, disconnects the observer and aborts the
    in-flight fetch (AbortController) on tab switch, on key-filter change and
    on pagehide/beforeunload — leaving the page releases everything at once.
  * the poll no longer rebuilds the table once extra pages are loaded, so the
    5s refresh cannot throw away scrolled history.
  * when the server reports replay_partial, the filter row states that the
    aggregates cover the recent audit tail and points at CSV for full history.

Adapters page shows each pool as created/max plus in_use/idle and the live
grow/shrink steps, with the sizing rule in the hover text.

Priority page health tags now distinguish cooling / probe-ready / probing
instead of a flat "cooling", and the tooltip spells out when the window opened,
when the single probe is allowed through and when the slot clears completely.
core.AutoSlotState carries cooldown_from / probe_after / probing / probeable to
feed this.
This commit is contained in:
JianFeeeee
2026-08-30 08:06:11 +08:00
parent d42c02b15d
commit 6575058556
2 changed files with 321 additions and 38 deletions

View File

@ -26,12 +26,12 @@ import (
// request paths look keys up concurrently; the AUTO chain itself is swapped
// atomically and needs no lock.
type Core struct {
mu sync.Mutex
cfg *config.Config
vm *lua.VM
store *config.Store
scheduler *scheduler.Scheduler
registry *provider.Registry
mu sync.Mutex
cfg *config.Config
vm *lua.VM
store *config.Store
scheduler *scheduler.Scheduler
registry *provider.Registry
autoChain atomic.Pointer[scheduler.Chain] // chat AUTO chain
autoImageChain atomic.Pointer[scheduler.Chain] // image-generation AUTO chain
}
@ -565,6 +565,16 @@ type AutoSlotState struct {
FailCount int64 `json:"fail_count"`
CooldownUntil int64 `json:"cooldown_until"`
Cooling bool `json:"cooling"`
// CooldownFrom is when the current cooldown window opened and ProbeAfter is
// its midpoint: from ProbeAfter on, a single probe request is allowed
// through so a recovered upstream does not have to sit out the rest of the
// window. Probing reports whether such a probe is in flight right now.
CooldownFrom int64 `json:"cooldown_from"`
ProbeAfter int64 `json:"probe_after"`
Probing bool `json:"probing"`
// Probeable marks a cooling slot that is past its midpoint, i.e. the next
// AUTO request may use it as a probe.
Probeable bool `json:"probeable"`
}
// AutoSlotStates returns per-slot health for every slot of the current chain.
@ -582,6 +592,7 @@ func (c *Core) AutoSlotStates() []AutoSlotState {
continue
}
pref, fail, until := pp.ModelHealthInfo(sl.Model)
from, probeAfter, probing := pp.ModelProbeInfo(sl.Model)
out = append(out, AutoSlotState{
Model: sl.Model,
Source: sl.Source,
@ -589,6 +600,10 @@ func (c *Core) AutoSlotStates() []AutoSlotState {
FailCount: fail,
CooldownUntil: until,
Cooling: until > now,
CooldownFrom: from,
ProbeAfter: probeAfter,
Probing: probing,
Probeable: until > now && probeAfter > 0 && now >= probeAfter,
})
}
}
@ -740,4 +755,4 @@ func (c *Core) Close() {
if c.vm != nil {
c.vm.Stop()
}
}
}