fix: AUTO tier order ascending + stats/CSV export completeness + UI key-view toggle & exit affordance

- scheduler: BuildChain sorts tiers ascending so tier 1 (highest priority) is tried first; previously descending inverted the chain (P10-1..P10-5)
- stats/api: handleStatsAPI limit→20000; CSV reads full audit via new Stats.AuditRecords (includes *.old rotation); key_names mapped by keyID() masked key; non-admin filter and keys-csv use masked keys
- server: ring buffer NewStats(10000)
- ui: dash-row card scroll area moved to table container (fix overflow below card); key view toggle (re-click same key returns to global) + kpi-exit affordance; rec-exit span
- chat: chain-failure record now surfaces first failed tier; AUTO comment sync
This commit is contained in:
root
2026-08-11 13:36:23 +08:00
parent 32e2fd521a
commit 854b3e2e37
7 changed files with 104 additions and 43 deletions

View File

@ -92,7 +92,7 @@ func (tn *TierNode) NextStart() int64 {
// requests (rotation state resets when the chain is rebuilt, e.g. after
// editing the rules — acceptable, the swap also resets cooldowns).
type Chain struct {
Tiers []*TierNode // descending tier order
Tiers []*TierNode // ascending tier order (tier 1 = highest priority, tried first)
}
// TierErrors is the per-tier failure summary carried by ChainErr. Errors
@ -133,10 +133,10 @@ func (e *ChainErr) Error() string {
return b.String()
}
// BuildChain groups rules into descending tiers and resolves each slot's
// provider via prov. Rules whose provider resolves to nil are dropped (the
// source no longer serves the model). Slot order within a tier follows the
// configured rule order.
// BuildChain groups rules into ascending tiers (tier 1 = highest priority,
// tried first) and resolves each slot's provider via prov. Rules whose
// provider resolves to nil are dropped (the source no longer serves the
// model). Slot order within a tier follows the configured rule order.
func BuildChain(rules []Rule, prov func(model, source string) Provider) *Chain {
byTier := map[int][]*Slot{}
var tiers []int
@ -157,7 +157,7 @@ func BuildChain(rules []Rule, prov func(model, source string) Provider) *Chain {
Prov: p,
})
}
sort.Slice(tiers, func(i, j int) bool { return tiers[i] > tiers[j] })
sort.Slice(tiers, func(i, j int) bool { return tiers[i] < tiers[j] })
ch := &Chain{}
for _, t := range tiers {
tn := &TierNode{Tier: t, Slots: byTier[t]}
@ -221,12 +221,12 @@ func runTier(ctx context.Context, tn *TierNode, cands []*Slot, base int64, req *
return tierResult{hard: hard}
}
// chainDrive runs a request down the chain (plan 2.3): tiers descending,
// per-tier round-robin starting at the tier cursor, same-tier runs ordered by
// preference (negative prefs sink but stay reachable). Quota-exhausted and
// cooling slots are filtered up front; a fully busy tier is polled for a
// bounded time before falling through. Failures are summarized in *ChainErr
// for the caller to map to HTTP 503.
// chainDrive runs a request down the chain (plan 2.3): tiers ascending (tier
// 1, the highest priority, first), per-tier round-robin starting at the tier
// cursor, same-tier runs ordered by preference (negative prefs sink but stay
// reachable). Quota-exhausted and cooling slots are filtered up front; a
// fully busy tier is polled for a bounded time before falling through.
// Failures are summarized in *ChainErr for the caller to map to HTTP 503.
func (s *Scheduler) chainDrive(ctx context.Context, chain *Chain, req *types.ChatRequest, exhausted func(*Slot) bool, stream bool) (*types.UnifiedResponse, <-chan types.UnifiedChunk, string, string, error) {
if chain == nil || len(chain.Tiers) == 0 {
return nil, nil, "", "", fmt.Errorf("no auto slot configured")