mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
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:
@ -107,11 +107,17 @@ pre.configbox { background:var(--card2); border:1px solid var(--line); border-ra
|
||||
.kpi .k-val { font-size:26px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; letter-spacing:.3px; }
|
||||
.kpi .k-val .u { font-size:13px; font-weight:600; color:var(--muted); margin-left:3px; }
|
||||
.kpi .k-sub { font-size:11.5px; color:var(--muted); margin-top:3px; }
|
||||
.kpi.kpi-exit { cursor:pointer; border-color:var(--err); }
|
||||
.kpi.kpi-exit:hover { border-color:var(--err); box-shadow:0 0 0 3px rgba(244,67,54,.15); }
|
||||
.kpi.kpi-exit .k-lab::after { content:'×'; color:var(--err); font-weight:800; font-size:15px; margin-left:auto; }
|
||||
.dot { width:7px; height:7px; border-radius:50%; background:var(--ok); display:inline-block; animation:blink 1.6s infinite; }
|
||||
.dot.err { background:var(--err); }
|
||||
@keyframes blink { 50% { opacity:.25; } }
|
||||
.dash-row { display:flex; gap:12px; margin-bottom:18px; }
|
||||
.dash-row .card { flex:1; min-width:0; }
|
||||
.dash-row .card { flex:1; min-width:0; display:flex; flex-direction:column; max-height:440px; }
|
||||
.dash-row .card h2 { flex:0 0 auto; }
|
||||
.dash-row .card > div { flex:1 1 auto; min-height:0; overflow:auto; -webkit-overflow-scrolling:touch; }
|
||||
.dash-row .card > div table thead th { position:sticky; top:0; background:var(--card); z-index:1; }
|
||||
td.num, th.num { text-align:right; font-variant-numeric:tabular-nums; }
|
||||
td .okc { color:var(--ok); font-weight:600; }
|
||||
td .errc { color:var(--err); font-weight:600; }
|
||||
@ -482,7 +488,7 @@ const STR = {
|
||||
sortCooling:'冷却', sortFail:'失败', sortHealthTip:'冷却 / 失败次数 / 偏好分 实时状态', sortHealthReset:'链上冷却已复位',
|
||||
sortSource:'源', sortPrio:'优先级 %s', sortEmpty:'该源暂无模型',
|
||||
kpiActive:'活跃请求', kpiReqs:'总请求', kpiOk:'成功率', kpiTokens:'Tokens', kpiLat:'平均延迟', kpiMaxLat:'最大延迟',
|
||||
dashModel:'模型用量', dashSrc:'源用量与延迟', dashKey:'密钥用量', dashRecs:'请求记录', exportCsv:'导出 CSV', expWeek:'近一周', expMonth:'近一月', expYear:'近一年', expRange:'自定义范围', expStart:'开始日期', expEnd:'结束日期', expDownload:'下载', expKeysCsv:'导出密钥用量',
|
||||
dashModel:'模型用量', dashSrc:'源用量与延迟', dashKey:'密钥用量', dashRecs:'请求记录', exportCsv:'导出 CSV', expWeek:'近一周', expMonth:'近一月', expYear:'近一年', expRange:'自定义范围', expStart:'开始日期', expEnd:'结束日期', expDownload:'下载', expKeysCsv:'导出密钥用量', exitKeyView:'退出该密钥视图',
|
||||
dashStatus:'状态码分布', thCode:'状态码', statusTag:'状态码分类统计(含 402 欠费 / 400 schema 错误;两者不计入上游退避但单独计数)',
|
||||
thModel:'模型', thSrc:'源', thKey:'密钥', thReqs:'请求', thOk:'成功', thErr:'失败',
|
||||
thPrompt:'输入 Tokens', thCompl:'输出 Tokens', thAvgLat:'平均延迟', thMaxLat:'最长延迟',
|
||||
@ -538,7 +544,7 @@ kMeTitle:'My key', kMeRole:'Role', kMeModels:'Models I can use', kMeHint:'Keys c
|
||||
sortCooling:'cooling', sortFail:'fail', sortHealthTip:'live cooldown / failures / preference score', sortHealthReset:'chain cooldowns reset',
|
||||
sortSource:'source', sortPrio:'priority %s', sortEmpty:'no models in this source',
|
||||
kpiActive:'Active requests', kpiReqs:'Requests', kpiOk:'Success rate', kpiTokens:'Tokens', kpiLat:'Avg latency', kpiMaxLat:'Max latency',
|
||||
dashModel:'Model usage', dashSrc:'Source usage & latency', dashKey:'Key usage', dashRecs:'Request records', exportCsv:'Export CSV', expWeek:'Last week', expMonth:'Last month', expYear:'Last year', expRange:'Custom range', expStart:'Start date', expEnd:'End date', expDownload:'Download',
|
||||
dashModel:'Model usage', dashSrc:'Source usage & latency', dashKey:'Key usage', dashRecs:'Request records', exportCsv:'Export CSV', expWeek:'Last week', expMonth:'Last month', expYear:'Last year', expRange:'Custom range', expStart:'Start date', expEnd:'End date', expDownload:'Download', expKeysCsv:'Export key usage', exitKeyView:'Exit this key view',
|
||||
dashStatus:'Status codes', thCode:'Code', statusTag:'Per-status aggregates — 402 quota / 400 schema errors are counted here but never back off the provider',
|
||||
thModel:'Model', thSrc:'Source', thKey:'Key', thReqs:'Requests', thOk:'OK', thErr:'Err',
|
||||
thPrompt:'Prompt Tokens', thCompl:'Completion Tokens', thAvgLat:'Avg latency', thMaxLat:'Max latency',
|
||||
@ -672,7 +678,7 @@ async function renderStatus() {
|
||||
</div>
|
||||
<div class="card"><h2>${t('dashStatus')} <span class="muted" style="font-weight:400;font-size:12px">${t('statusTag')}</span></h2><div id="tb-status"></div></div>
|
||||
<div class="card"><h2>${t('dashKey')}<span class="grow"></span><button class="ghost small" onclick="openExportModal()">${t('exportCsv')}</button></h2><div id="tb-key"></div></div>
|
||||
<div class="card"><h2><span>${t('dashRecs')}</span><span class="grow"></span><button class="ghost small" onclick="openExportModal()">${t('exportCsv')}</button></h2>
|
||||
<div class="card"><h2><span>${t('dashRecs')}</span><span class="grow"></span><span id="rec-exit"></span><button class="ghost small" onclick="openExportModal()">${t('exportCsv')}</button></h2>
|
||||
<div class="filter-line">
|
||||
<span class="muted">${t('recFilter')}</span>
|
||||
<select id="rec-key" onchange="renderKeyF(this.value)"></select>
|
||||
@ -690,15 +696,16 @@ async function renderStatus() {
|
||||
await paintStats();
|
||||
statsTimerId = setInterval(paintStats, 3000);
|
||||
}
|
||||
function renderKeySelect(keys) {
|
||||
function renderKeySelect(keys, keyNames) {
|
||||
const sel = $('#rec-key');
|
||||
if (!sel) return;
|
||||
const cur = sel.value;
|
||||
const nm = (k) => (keyNames && keyNames[k] ? keyNames[k] + ' · ' + k : k);
|
||||
sel.innerHTML = `<option value="">${esc(t('allKeys'))}</option>` +
|
||||
keys.map(k => `<option value="${escAttr(k)}">${esc(k)}</option>`).join('');
|
||||
keys.map(k => `<option value="${escAttr(k)}">${esc(nm(k))}</option>`).join('');
|
||||
if (cur) sel.value = cur;
|
||||
}
|
||||
function renderKeyF(v) { statsKeyF = v; paintStats(); }
|
||||
function renderKeyF(v) { statsKeyF = (statsKeyF === v) ? '' : v; paintStats(); }
|
||||
function openExportModal() {
|
||||
const now = new Date();
|
||||
const ago = d => { const x = new Date(now); x.setDate(x.getDate() - d); return x.toISOString().slice(0, 10); };
|
||||
@ -731,14 +738,14 @@ function downloadKeysCsv() {
|
||||
}
|
||||
async function paintStats() {
|
||||
try {
|
||||
const q = '/api/stats?limit=500' + (statsKeyF ? '&key=' + encodeURIComponent(statsKeyF) : '');
|
||||
const q = '/api/stats?limit=2000' + (statsKeyF ? '&key=' + encodeURIComponent(statsKeyF) : '');
|
||||
const st = await api(q);
|
||||
const tot = st.total || {};
|
||||
const okr = tot.reqs ? Math.round(tot.ok * 100 / tot.reqs) : 0;
|
||||
const avg = fmtLat(tot.latency_sum_ms, tot.reqs);
|
||||
const kpi = $('#kpi-row');
|
||||
if (kpi) kpi.innerHTML = `
|
||||
<div class="kpi"><div class="k-lab">${t('kpiActive')} <span class="dot"></span></div><div class="k-val">${st.active || 0}</div><div class="k-sub">${statsKeyF ? esc(statsKeyF) : ''}</div></div>
|
||||
<div class="kpi${statsKeyF ? ' kpi-exit' : ''}" title="${statsKeyF ? t('exitKeyView') : ''}" onclick="${statsKeyF ? `renderKeyF('${escAttr(statsKeyF)}')` : ''}"><div class="k-lab">${t('kpiActive')} <span class="dot"></span></div><div class="k-val">${st.active || 0}</div><div class="k-sub">${statsKeyF ? esc(st.key_names ? (st.key_names[statsKeyF] ? st.key_names[statsKeyF] + ' · ' : '') + statsKeyF : statsKeyF) : ''}</div></div>
|
||||
<div class="kpi"><div class="k-lab">${t('kpiReqs')}</div><div class="k-val">${fmtN(tot.reqs)}</div><div class="k-sub">${t('kpiOk')} <b class="${okr >= 90 ? 'okc' : 'errc'}">${okr}%</b></div></div>
|
||||
<div class="kpi"><div class="k-lab">${t('kpiTokens')}</div><div class="k-val">${fmtTok(tot.tokens)}</div>
|
||||
<div class="k-sub">${t('thPrompt')} ${fmtTok(tot.prompt_tokens)} · ${t('thCompl')} ${fmtTok(tot.completion_tokens)}</div></div>
|
||||
@ -748,7 +755,9 @@ async function paintStats() {
|
||||
paintStatusTable(st.by_status || []);
|
||||
paintKeyTable(st.by_key || [], st.key_names || {});
|
||||
paintRecords(st.records || [], st.key_names || {});
|
||||
renderKeySelect((st.by_key || []).map(k => k.name));
|
||||
const rex = $('#rec-exit');
|
||||
if (rex) rex.innerHTML = statsKeyF ? `<button class="ghost small" onclick="renderKeyF('')">${t('exitKeyView')} ${esc(st.key_names && st.key_names[statsKeyF] ? st.key_names[statsKeyF] : statsKeyF)}</button>` : '';
|
||||
renderKeySelect((st.by_key || []).map(k => k.name), st.key_names || {});
|
||||
} catch (e) { console.error('[stats]', e); }
|
||||
}
|
||||
function paintModelTable(rows) {
|
||||
|
||||
Reference in New Issue
Block a user