Skip to content

Commit 166fecc

Browse files
committed
Fix dashboard save payload portability
1 parent f2681f8 commit 166fecc

5 files changed

Lines changed: 20 additions & 3 deletions

File tree

src/dashboard-config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export interface RedactedCommandCodeCredential {
3535

3636
export interface DashboardConfigView {
3737
object: "commandcode.dashboard_config";
38-
configFilePath: string | undefined;
3938
dirty: boolean;
4039
restart_required: boolean;
4140
bridgeApiKey?: string | undefined;

src/dashboard.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ function randomBridgeKey(){const bytes=new Uint8Array(3); if(globalThis.crypto?.
7474
function rememberPendingBridgeKey(key){pendingBridgeKey=authKey(key); if(pendingBridgeKey)localStorage.setItem('pendingBridgeApiKey',pendingBridgeKey); else localStorage.removeItem('pendingBridgeApiKey');}
7575
function generateBridgeKey(){const key=randomBridgeKey(); rememberPendingBridgeKey(key); const el=$('bridgeApiKey'); if(el)el.value=displayBridgeKey(key); setDirty(); toast('Random Client API key generated. Save JSON, then restart to apply.');}
7676
$('bindHost').onchange=()=>{cfg.server.host=$('bindHost').value;syncBridgeKey();setDirty();}; $('bindPort').oninput=()=>{cfg.server.port=Number($('bindPort').value)||9992;setDirty();}; function saveBridgeKey(nextKey){const key=nextKey||fullBridgeKey(); const current=currentBridgeAuthKey(); if(key&&key!==current){rememberPendingBridgeKey(key); syncBridgeKey(); setDirty(); toast('Pending Client API key saved. Save JSON, then restart to apply.'); return;} if(key)localStorage.setItem('bridgeApiKey',key); else localStorage.removeItem('bridgeApiKey'); syncBridgeKey(); toast(key?'Current Client API key saved in this browser.':'Client API key cleared.');} async function copyBridgeKey(){const key=fullBridgeKey()||authKey(localStorage.getItem('bridgeApiKey'))||''; if(!key){toast('Client API key is empty.');return;} try{await navigator.clipboard.writeText(key);toast('Client API key copied.');}catch{toast('Copy failed. Select and copy manually.');}} $('generateBridgeKey').onclick=generateBridgeKey; $('saveBridgeKey').onclick=()=>saveBridgeKey(); $('copyBridgeKey').onclick=copyBridgeKey; $('bridgeApiKey').onkeydown=e=>{if(e.key==='Enter')saveBridgeKey();}; $('maxPer').oninput=()=>{cfg.routing.maxInFlightPerCredential=Number($('maxPer').value)||4; setDirty();}; $('refreshCreds').onclick=async()=>{try{const m=await api('/admin/commandcode/credentials?refresh=true'); const byId=new Map((m.credentials||[]).map(x=>[x.id,x])); cfg.credentials.forEach(c=>c.metrics=byId.get(c.id)); render(); toast('Credentials refreshed.');}catch(e){toast('Credential refresh failed.');}}; $('addCred').onclick=()=>{cfg.credentials.push({id:'key'+(cfg.credentials.length+1),apiKey:'',weight:1,enabled:true});render();setDirty();};
77-
$('save').onclick=async()=>{try{if(!cfg)throw new Error('config not loaded'); const pendingKey=pendingBridgeKey||((fullBridgeKey()&&fullBridgeKey()!==currentBridgeAuthKey())?fullBridgeKey():''); const payload={...(pendingKey?{bridgeApiKey:pendingKey}:{}),server:cfg.server,routing:cfg.routing,models:cfg.models,credentials:cfg.credentials.map(c=>({id:c.id,originalId:c.originalId,apiKey:c.apiKey||undefined,weight:c.weight||1,enabled:expiredIds.has(c.id)?undefined:c.enabled!==false,maxInFlight:c.maxInFlight,allowedModels:c.allowedModels}))}; cfg=await api('/admin/config',{method:'PUT',body:JSON.stringify(payload)}); render(); setDirty(true); toast('JSON saved. Restart required.');}catch(e){toast(duplicateCredentialMessage(e)||('Save failed: '+(e?.message||e)));}};
77+
function credentialPayloads(){document.querySelectorAll('[data-cid]').forEach(e=>{const i=+e.dataset.cid; if(cfg.credentials[i])cfg.credentials[i].id=e.value.trim()||('key'+(i+1));}); document.querySelectorAll('[data-cenabled]').forEach(e=>{const i=+e.dataset.cenabled; if(cfg.credentials[i])cfg.credentials[i].enabled=e.checked;}); document.querySelectorAll('[data-ckey]').forEach(e=>{const i=+e.dataset.ckey; if(cfg.credentials[i]&&e.value.trim())cfg.credentials[i].apiKey=e.value.trim();}); return cfg.credentials.map((c,i)=>{const keyEl=document.querySelector('[data-ckey="'+i+'"]'); const typedKey=keyEl?.value?.trim(); return {id:c.id,originalId:c.originalId||c.id,apiKey:typedKey||c.apiKey||undefined,weight:c.weight||1,enabled:expiredIds.has(c.id)?undefined:c.enabled!==false,maxInFlight:c.maxInFlight,allowedModels:c.allowedModels};});}
78+
$('save').onclick=async()=>{try{if(!cfg)throw new Error('config not loaded'); cfg.server.host=$('bindHost').value; cfg.server.port=Number($('bindPort').value)||9992; cfg.routing.maxInFlightPerCredential=Number($('maxPer').value)||4; const checkedPolicy=document.querySelector('input[name=policy]:checked'); if(checkedPolicy)cfg.routing.policy=checkedPolicy.value; const pendingKey=pendingBridgeKey||((fullBridgeKey()&&fullBridgeKey()!==currentBridgeAuthKey())?fullBridgeKey():''); const payload={...(pendingKey?{bridgeApiKey:pendingKey}:{}),server:cfg.server,routing:cfg.routing,models:cfg.models,credentials:credentialPayloads()}; cfg=await api('/admin/config',{method:'PUT',body:JSON.stringify(payload)}); render(); setDirty(true); toast('JSON saved. Restart required.');}catch(e){toast(duplicateCredentialMessage(e)||('Save failed: '+(e?.message||e)));}};
7879
$('restart').onclick=async()=>{const pendingKey=pendingBridgeKey; try{await api('/admin/restart',{method:'POST',body:'{}'}); if(pendingKey){localStorage.setItem('bridgeApiKey',pendingKey); rememberPendingBridgeKey('');} toast('Restart requested'); setTimeout(load,1800);}catch(e){toast('Restart failed: '+(e?.message||e));}};
7980
load();
8081
</script>

src/server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,6 @@ function dashboardConfigResponse(
272272
};
273273
return {
274274
object: "commandcode.dashboard_config",
275-
configFilePath: config.configFilePath,
276275
dirty,
277276
restart_required: dirty,
278277
bridgeApiKey: config.bridgeApiKey,

tests/admin-config.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ describe("JSON dashboard configuration", () => {
400400
expect(response.body).toContain("CommandCode Bridge Console");
401401
expect(response.body).toContain("Client API Key");
402402
expect(response.body).not.toContain("Admin API Key");
403+
expect(response.body).not.toContain("configFilePath");
404+
expect(response.body).not.toContain(process.env.HOME ?? "__NO_HOME__");
403405
expect(response.headers["content-security-policy"]).toContain(
404406
"script-src 'self' 'unsafe-inline'",
405407
);

tests/dashboard-ui.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,20 @@ describe("dashboard UI", () => {
156156
expect(html).toContain("bridgeApiKey:pendingKey");
157157
expect(html).toContain("Pending Client API key saved");
158158
});
159+
160+
it("builds save payloads from current relative-page DOM inputs instead of stale cfg state", () => {
161+
const html = dashboardHtml({
162+
server: { host: "0.0.0.0", port: 9992 },
163+
routing: { policy: "daily_burn_priority", maxInFlightPerCredential: 4 },
164+
credentials: [{ id: "default", apiKeyConfigured: true }],
165+
models: [],
166+
});
167+
168+
expect(html).toContain("function credentialPayloads");
169+
expect(html).toContain("document.querySelectorAll('[data-cid]')");
170+
expect(html).toContain("document.querySelector('[data-ckey=\"'+i+'\"]')");
171+
expect(html).toContain("api('/admin/config'");
172+
expect(html).toContain("api('/admin/restart'");
173+
expect(html).not.toContain("/Users/yorha");
174+
});
159175
});

0 commit comments

Comments
 (0)