@@ -52,6 +52,8 @@ const expiredIds=new Set();
5252function fullBridgeKey(){const raw=($('bridgeApiKey')?.value||'').trim(); if(!raw)return ''; return raw.startsWith('sk-')?raw:'sk-'+raw;}
5353function displayBridgeKey(key){return key?.startsWith('sk-')?key.slice(3):(key||'')}
5454function auth(){const key=localStorage.getItem('bridgeApiKey')||''; return key?{'authorization':'Bearer '+key,'content-type':'application/json'}:{'content-type':'application/json'}}
55+ function duplicateCredentialMessage(e){const text=String(e?.message||e||''); return text.includes('duplicate_commandcode_api_key')||text.includes('Duplicate CommandCode API key')?'이미 등록된 키입니다. 다른 CommandCode API key를 입력해주세요.':null;}
56+ function preventDuplicateVisibleKey(input){const value=input.value.trim(); if(!value)return false; const dup=Array.from(document.querySelectorAll('[data-ckey]')).some(el=>el!==input&&el.value.trim()===value); if(!dup)return false; const i=+input.dataset.ckey; input.value=''; if(cfg?.credentials?.[i])cfg.credentials[i].apiKey=''; toast('이미 등록된 키입니다. 입력하지 않았습니다.'); return true;}
5557function syncBridgeKey(){const el=$('bridgeApiKey'); if(!el)return; const configured=cfg?.bridgeApiKey||''; const stored=localStorage.getItem('bridgeApiKey')||''; const key=configured||stored; if(configured&&stored!==configured)localStorage.setItem('bridgeApiKey',configured); if(document.activeElement!==el) el.value=displayBridgeKey(key); const wrap=$('bridgeKeyWrap'); if(wrap) wrap.style.display=$('bindHost')?.value==='0.0.0.0'?'grid':'none';}
5658function setDirty(v=true){dirty=v; $('dirtyText').textContent=v?'pending changes · restart required':'no pending changes'; updateRestart();}
5759function updateRestart(){const online=$('online').textContent==='online'; $('restart').disabled=online&&!dirty; $('restart').classList.toggle('active',!$('restart').disabled)}
@@ -63,10 +65,10 @@ async function load(){if(cfg){if(cfg.bridge) setOnline(cfg.bridge); render(); se
6365function render(){if(!cfg)return; $('bindHost').value=cfg.server.host; $('bindPort').value=cfg.server.port; $('maxPer').value=cfg.routing.maxInFlightPerCredential||4; syncBridgeKey();
6466$('policies').innerHTML=policies.map(p=>'<label class="policy"><input type="radio" name="policy" value="'+esc(p[0])+'" '+(cfg.routing.policy===p[0]?'checked':'')+'><b>'+esc(p[1])+'</b><span class="info" tabindex="0" aria-label="'+esc(p[1])+' 설명">ℹ️<span class="tip">'+esc(p[2])+'<br><span class="token">'+esc(p[0])+'</span></span></span></label>').join(''); document.querySelectorAll('input[name=policy]').forEach(el=>el.onchange=()=>{cfg.routing.policy=el.value;setDirty();});
6567$('creds').innerHTML=cfg.credentials.map((c,i)=>{c.originalId=c.originalId||c.id; if(c.enabled===undefined)c.enabled=true; const m=c.metrics||{}; const b=m.billing||{}; const bm=b.metrics||{}; const bal=Number(bm.currentBalance??b.currentBalance??b.monthlyCredits); const days=Number(bm.daysRemaining??b.daysRemaining); const daily=Number(bm.requiredDailyBurn??b.requiredDailyBurn); const money=Number.isFinite(bal)?'$'+bal.toFixed(2):'unknown'; const expired=Number.isFinite(days)&&days<=0; if(expired)expiredIds.add(c.id); else expiredIds.delete(c.id); const dayText=Number.isFinite(days)?Math.max(0,days).toFixed(1)+' 일':'unknown'; const dailyText=Number.isFinite(daily)?'$'+daily.toFixed(2)+'/일':'unknown'; const status=expired?'expired':(c.enabled===false?'manual off':(m.disabledUntil?'disabled':(m.available===false?'blocked':'ready'))); return '<div class="cred"><div class="cred-head"><input class="cred-name" data-cid="'+i+'" value="'+esc(c.id||('key'+(i+1)))+'"><label class="switch" title="사용 여부"><input data-cenabled="'+i+'" type="checkbox" '+(c.enabled!==false&&!expired?'checked':'')+' '+(expired?'disabled':'')+'><span class="slider"></span></label><button class="danger" data-del="'+i+'">Delete</button></div><div class="kv"><span>Status</span><b>'+esc(status)+'</b></div><div class="kv"><span>잔액</span><b>'+esc(money)+'</b></div><div class="kv"><span>기한</span><b>'+esc(dayText)+'</b></div><div class="kv"><span>잔액/기한</span><b>'+esc(dailyText)+'</b></div><div class="field"><label>API Key '+(c.apiKeyConfigured?'('+esc(c.apiKeyPreview)+')':'')+'</label><input data-ckey="'+i+'" type="password" placeholder="leave blank to keep existing"></div></div>'}).join('');
66- document.querySelectorAll('[data-cid]').forEach(e=>e.oninput=()=>{cfg.credentials[+e.dataset.cid].id=e.value;setDirty();}); document.querySelectorAll('[data-cenabled]').forEach(e=>e.onchange=()=>{cfg.credentials[+e.dataset.cenabled].enabled=e.checked;setDirty();}); document.querySelectorAll('[data-ckey]').forEach(e=>e.oninput=()=>{cfg.credentials[+e.dataset.ckey].apiKey=e.value;setDirty();}); document.querySelectorAll('[data-del]').forEach(e=>e.onclick=()=>{cfg.credentials.splice(+e.dataset.del,1);render();setDirty();});
68+ document.querySelectorAll('[data-cid]').forEach(e=>e.oninput=()=>{cfg.credentials[+e.dataset.cid].id=e.value;setDirty();}); document.querySelectorAll('[data-cenabled]').forEach(e=>e.onchange=()=>{cfg.credentials[+e.dataset.cenabled].enabled=e.checked;setDirty();}); document.querySelectorAll('[data-ckey]').forEach(e=>e.oninput=()=>{if(preventDuplicateVisibleKey(e))return; cfg.credentials[+e.dataset.ckey].apiKey=e.value;setDirty();}); document.querySelectorAll('[data-del]').forEach(e=>e.onclick=()=>{cfg.credentials.splice(+e.dataset.del,1);render();setDirty();});
6769$('models').innerHTML=cfg.models.map((m,i)=>'<div class="model"><div class="model-head"><div><b>'+esc(m.label||m.id)+'</b><div class="small">'+esc(m.provider)+' · '+esc(m.id)+'</div></div><label class="switch"><input data-mid="'+i+'" type="checkbox" '+(m.enabled?'checked':'')+'><span class="slider"></span></label></div><div class="small">'+esc(m.notes||'')+'</div></div>').join(''); document.querySelectorAll('[data-mid]').forEach(e=>e.onchange=()=>{cfg.models[+e.dataset.mid].enabled=e.checked;setDirty();});}
6870$('bindHost').onchange=()=>{cfg.server.host=$('bindHost').value;syncBridgeKey();setDirty();}; $('bindPort').oninput=()=>{cfg.server.port=Number($('bindPort').value)||9992;setDirty();}; function saveBridgeKey(){const key=fullBridgeKey(); if(key)localStorage.setItem('bridgeApiKey',key); else localStorage.removeItem('bridgeApiKey'); syncBridgeKey(); toast(key?'Admin API key saved in this browser.':'Admin API key cleared.');} async function copyBridgeKey(){const key=fullBridgeKey()||localStorage.getItem('bridgeApiKey')||''; if(!key){toast('Admin API key is empty.');return;} try{await navigator.clipboard.writeText(key);toast('Admin API key copied.');}catch{toast('Copy failed. Select and copy manually.');}} $('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();};
69- $('save').onclick=async()=>{try{if(!cfg)throw new Error('config not loaded'); const payload={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('Save failed: '+(e?.message||e));}};
71+ $('save').onclick=async()=>{try{if(!cfg)throw new Error('config not loaded'); const payload={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) ));}};
7072$('restart').onclick=async()=>{try{await api('/admin/restart',{method:'POST',body:'{}'}); toast('Restart requested'); setTimeout(load,1800);}catch(e){toast('Restart failed: '+(e?.message||e));}};
7173load();
7274</script>
0 commit comments