mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
feat: add provider quota refresh control
This commit is contained in:
+58
-2
@@ -5485,13 +5485,20 @@ function _buildPluginCard(plugin){
|
||||
|
||||
const _providerCardEls = new Map(); // providerId → {card, statusDot, input, saveBtn, removeBtn}
|
||||
|
||||
async function _fetchProviderQuotaStatus(force=false){
|
||||
const endpoint=force?`/api/provider/quota?refresh=1&ts=${Date.now()}`:'/api/provider/quota';
|
||||
const status=await api(endpoint,{cache:'no-store'});
|
||||
if(status&&typeof status==='object') status.client_fetched_at=new Date().toISOString();
|
||||
return status;
|
||||
}
|
||||
|
||||
async function loadProvidersPanel(){
|
||||
const list=$('providersList');
|
||||
const empty=$('providersEmpty');
|
||||
if(!list) return;
|
||||
try{
|
||||
const data=await api('/api/providers');
|
||||
const quota=await api('/api/provider/quota').catch(e=>({ok:false,status:'unavailable',quota:null,message:e.message||'Quota status unavailable'}));
|
||||
const quota=await _fetchProviderQuotaStatus(false).catch(e=>({ok:false,status:'unavailable',quota:null,message:e.message||'Quota status unavailable',client_fetched_at:new Date().toISOString()}));
|
||||
const providers=(data.providers||[]).filter(p=>p.configurable||p.is_oauth);
|
||||
list.innerHTML='';
|
||||
_providerCardEls.clear();
|
||||
@@ -5512,6 +5519,40 @@ async function loadProvidersPanel(){
|
||||
}
|
||||
}
|
||||
|
||||
async function _refreshProviderQuota(card,button){
|
||||
if(!card) return;
|
||||
if(button){
|
||||
button.disabled=true;
|
||||
button.textContent='Refreshing…';
|
||||
button.setAttribute('aria-busy','true');
|
||||
}
|
||||
let failed=false;
|
||||
let next;
|
||||
try{
|
||||
next=await _fetchProviderQuotaStatus(true);
|
||||
failed=next&&next.ok===false;
|
||||
}catch(e){
|
||||
failed=true;
|
||||
next={ok:false,status:'unavailable',quota:null,message:e.message||'Quota status unavailable',client_fetched_at:new Date().toISOString()};
|
||||
}
|
||||
try{
|
||||
const fresh=_buildProviderQuotaCard(next);
|
||||
if(fresh){
|
||||
card.replaceWith(fresh);
|
||||
if(typeof showToast==='function') showToast(failed?'Provider usage refresh failed':'Provider usage refreshed');
|
||||
return;
|
||||
}
|
||||
}catch(e){
|
||||
failed=true;
|
||||
}
|
||||
if(button){
|
||||
button.disabled=false;
|
||||
button.textContent='Refresh usage';
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
if(typeof showToast==='function') showToast('Provider usage refresh failed');
|
||||
}
|
||||
|
||||
function _formatProviderQuotaMoney(value){
|
||||
if(value===null||value===undefined||value==='') return '—';
|
||||
const n=Number(value);
|
||||
@@ -5543,6 +5584,15 @@ function _formatProviderQuotaWindowLabel(accountLimits,w){
|
||||
return raw||'Window';
|
||||
}
|
||||
|
||||
function _formatProviderQuotaLastChecked(status){
|
||||
const accountLimits=status&&status.account_limits;
|
||||
const value=(accountLimits&&accountLimits.fetched_at)||status&&status.client_fetched_at;
|
||||
if(!value) return 'Last checked after refresh';
|
||||
const d=new Date(value);
|
||||
if(Number.isNaN(d.getTime())) return 'Last checked after refresh';
|
||||
try{return 'Last checked '+d.toLocaleString();}catch(e){return 'Last checked '+value;}
|
||||
}
|
||||
|
||||
function _buildProviderQuotaCard(status){
|
||||
if(!status) return null;
|
||||
const card=document.createElement('div');
|
||||
@@ -5590,11 +5640,17 @@ function _buildProviderQuotaCard(status){
|
||||
<div>
|
||||
<div class="provider-quota-title">Active provider quota</div>
|
||||
<div class="provider-quota-subtitle">${esc(provider)}</div>
|
||||
<div class="provider-quota-checked">${esc(_formatProviderQuotaLastChecked(status))}</div>
|
||||
</div>
|
||||
<div class="provider-quota-actions">
|
||||
<span class="provider-quota-badge">${esc(state.replace(/_/g,' '))}</span>
|
||||
<button class="provider-quota-refresh" type="button" data-provider-quota-refresh title="Refresh provider usage limits now">Refresh usage</button>
|
||||
</div>
|
||||
<span class="provider-quota-badge">${esc(state.replace(/_/g,' '))}</span>
|
||||
</div>
|
||||
<div class="provider-quota-body">${body}</div>
|
||||
`;
|
||||
const refreshBtn=card.querySelector('[data-provider-quota-refresh]');
|
||||
if(refreshBtn) refreshBtn.addEventListener('click',()=>_refreshProviderQuota(card,refreshBtn));
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
@@ -2494,7 +2494,12 @@ main.main.showing-logs > #mainLogs{display:flex;}
|
||||
.provider-quota-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px;}
|
||||
.provider-quota-title{font-size:13px;font-weight:650;color:var(--text);line-height:1.2;}
|
||||
.provider-quota-subtitle{font-size:11px;color:var(--muted);line-height:1.3;margin-top:2px;}
|
||||
.provider-quota-checked{font-size:10.5px;color:var(--muted);line-height:1.3;margin-top:2px;}
|
||||
.provider-quota-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}
|
||||
.provider-quota-badge{font-size:10.5px;font-weight:650;text-transform:capitalize;padding:2px 8px;border-radius:999px;background:var(--accent-bg);color:var(--accent-text);white-space:nowrap;}
|
||||
.provider-quota-refresh{appearance:none;border:1px solid var(--border);border-radius:999px;background:var(--sidebar);color:var(--text);font-size:11px;font-weight:650;line-height:1;padding:5px 9px;cursor:pointer;white-space:nowrap;}
|
||||
.provider-quota-refresh:hover:not(:disabled){border-color:var(--accent);color:var(--accent);}
|
||||
.provider-quota-refresh:disabled{opacity:.65;cursor:wait;}
|
||||
.provider-quota-body{display:flex;flex-wrap:wrap;gap:8px;}
|
||||
.provider-quota-metric{flex:1;min-width:88px;border:1px solid var(--border);border-radius:8px;background:var(--sidebar);padding:8px 10px;}
|
||||
.provider-quota-metric span{display:block;font-size:10.5px;color:var(--muted);margin-bottom:2px;}
|
||||
|
||||
@@ -541,7 +541,8 @@ def test_provider_quota_route_is_registered():
|
||||
def test_provider_quota_card_is_rendered_in_providers_panel():
|
||||
"""The Providers panel should show active provider quota/status before cards."""
|
||||
panels = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
assert "api('/api/provider/quota')" in panels
|
||||
assert "_fetchProviderQuotaStatus(false)" in panels
|
||||
assert "'/api/provider/quota'" in panels
|
||||
assert "function _buildProviderQuotaCard" in panels
|
||||
assert "Active provider quota" in panels
|
||||
assert "provider-quota-card" in panels
|
||||
@@ -551,6 +552,20 @@ def test_provider_quota_card_is_rendered_in_providers_panel():
|
||||
assert "5-hour limit" in panels
|
||||
|
||||
|
||||
def test_provider_quota_card_has_manual_refresh_control():
|
||||
"""The quota card should let users force an immediate fresh usage lookup."""
|
||||
panels = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
assert "function _refreshProviderQuota" in panels
|
||||
assert "function _fetchProviderQuotaStatus" in panels
|
||||
assert "refresh=1" in panels
|
||||
assert "cache:'no-store'" in panels
|
||||
assert "data-provider-quota-refresh" in panels
|
||||
assert "Refresh usage" in panels
|
||||
assert "Provider usage refreshed" in panels
|
||||
assert "Provider usage refresh failed" in panels
|
||||
assert "Last checked" in panels
|
||||
|
||||
|
||||
def test_provider_quota_styles_exist():
|
||||
"""Quota UI should have visible supported/unavailable/invalid states."""
|
||||
css = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
@@ -562,6 +577,9 @@ def test_provider_quota_styles_exist():
|
||||
".provider-quota-card-invalid_key",
|
||||
".provider-quota-details",
|
||||
".provider-quota-window",
|
||||
".provider-quota-actions",
|
||||
".provider-quota-refresh",
|
||||
".provider-quota-checked",
|
||||
):
|
||||
assert token in css
|
||||
|
||||
|
||||
Reference in New Issue
Block a user