mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(plugins): ccquota 页面首次打开是空的
The plugin page rendered nothing until the operator clicked refresh, and even the click handler was never wired — while every unit test was green and the API returned correct numbers. Four separate mistakes stacked here: 1. The script waited on DOMContentLoaded, an event that had already been dispatched by the time the kernel injects a plugin mount. Replaced with pluginAPI.onTabShown (the kernel's own mechanism, fires with the DOM in place) PLUS an immediate call, because onTabShown alone leaves the page blank when the tab is already open at boot. The billing page does both; I had reduced it to one and broke first paint. 2. The re-entry guard flag lived in a closure variable. The tab's DOM is rebuilt on re-entry, so the handler was wired against a node that no longer existed. The flag now lives on the NODE (__ccqWired), which is what makes re-wiring correct. 3. refresh() swallowed its fetch failure and rendered "no sources", so a broken page and an empty account list looked identical. Failures now surface in the header line, including when pluginAPI.call is missing (an old kernel). 4. THE ACTUAL REASON the page stayed blank: there were TWO ccquota plugins. The embed (go:embed plugins/*.lua) version and the on-disk one both loaded, and the UI injected the EMBED copy — 5182 characters, predating every fix — while every edit I made and every hot reload went to the disk copy. The running binary was serving stale plugin code no amount of reloading could fix. Only a rebuild surfaced it, and only measuring the served <script> length (7053 vs 5182) showed which copy the browser had. Lesson worth keeping: "the file on disk is what runs" is false for anything under go:embed. Compare what the SERVER sends before editing the same file twice and concluding the edit did nothing. Verified in the browser: 2 account cards, 4 window bars, real balances, no overflow, no console errors, refresh button 48x29 and functional. Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
This commit is contained in:
@ -299,7 +299,10 @@ PLUGIN.ui = {
|
||||
try{
|
||||
var r=await fetch("/api/sources",{credentials:"same-origin"});
|
||||
srcs=(await r.json()).sources||[];
|
||||
}catch(e){ render([]); return; }
|
||||
}catch(e){
|
||||
var s0=document.getElementById("ccq-sub"); if(s0) s0.textContent="拉取源列表失败: "+e.message;
|
||||
render([]); return;
|
||||
}
|
||||
// Every source on the Command Code host is one account, keyed by its own
|
||||
// API key — two keys on that host are two seats, and both must be listed.
|
||||
var accts=srcs.filter(function(s){
|
||||
@ -310,6 +313,11 @@ PLUGIN.ui = {
|
||||
if(btn) btn.disabled=true;
|
||||
var auto=document.getElementById("ccq-auto")||document.querySelector(".ccq-auto");
|
||||
if(auto) auto.textContent="拉取中…";
|
||||
if(!window.pluginAPI || typeof pluginAPI.call!=="function"){
|
||||
var sb=document.getElementById("ccq-sub");
|
||||
if(sb) sb.textContent="pluginAPI.call 不可用(内核版本过旧)";
|
||||
render([]); return;
|
||||
}
|
||||
var out=[];
|
||||
var errs=0;
|
||||
// One account at a time: the endpoints are independent, and a sequential
|
||||
@ -331,11 +339,36 @@ PLUGIN.ui = {
|
||||
if(auto) auto.textContent="更新于 "+new Date().toLocaleTimeString();
|
||||
try{ await pluginAPI.postState("ccquota",{reports:out,updated_at:Date.now()}); }catch(e){}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded",function(){
|
||||
// The mount is injected long after DOMContentLoaded has already fired, so
|
||||
// listening for that event here registered a handler for an event that had
|
||||
// already been dispatched — the page rendered but stayed empty until a manual
|
||||
// refresh. Wiring the button and fetching immediately is what makes the page
|
||||
// populate on first open.
|
||||
// The plugin mount is injected by the kernel and its <script> evaluated after
|
||||
// injection, but that still happens during the UI's own boot — and the page
|
||||
// body is rebuilt whenever the tab is opened. Two consequences:
|
||||
//
|
||||
// - A DOMContentLoaded listener never fires (the event already passed), so
|
||||
// the page rendered empty until a manual refresh.
|
||||
// - Wiring the refresh button once at script-eval time silently loses the
|
||||
// handler when the tab's DOM is replaced.
|
||||
//
|
||||
// pluginAPI.onTabShown is the kernel's answer to both: the callback runs when
|
||||
// the tab is actually shown, with the DOM in place. It also runs once for the
|
||||
// already-visible tab at boot, so this single registration covers first paint
|
||||
// AND every later re-entry.
|
||||
function mount(){
|
||||
var b=document.getElementById("ccq-refresh");
|
||||
if(b) b.addEventListener("click",refresh);
|
||||
refresh();
|
||||
});
|
||||
// The tab's DOM is rebuilt on re-entry, so a handler wired against the old
|
||||
// node is gone. The flag lives on the NODE, not in a closure variable, which
|
||||
// is what makes re-wiring safe.
|
||||
if(b && !b.__ccqWired){ b.__ccqWired = true; b.addEventListener("click",refresh); }
|
||||
}
|
||||
// Run once immediately — the billing page does the same, and waiting only for
|
||||
// onTabShown leaves the page blank when the tab is already open at boot.
|
||||
mount();
|
||||
refresh();
|
||||
if (window.pluginAPI && pluginAPI.onTabShown) pluginAPI.onTabShown(mount);
|
||||
window.__ccqRender = render;
|
||||
window.__ccqRefresh = refresh;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user