fix(opax): CostGuard — AbortController 10s timeout + /billing/credits?days=30 + fallback (Fix 3)
This commit is contained in:
parent
39f714a621
commit
b5c03d4d44
|
|
@ -688,22 +688,16 @@ function renderTierGrid(spend){
|
||||||
function updatePricing(val){
|
function updatePricing(val){
|
||||||
const spend = Number(val)
|
const spend = Number(val)
|
||||||
const tier = activeTier(spend)
|
const tier = activeTier(spend)
|
||||||
|
|
||||||
document.getElementById('spend-display').textContent = fmtUSD(spend)
|
document.getElementById('spend-display').textContent = fmtUSD(spend)
|
||||||
|
|
||||||
renderTierGrid(spend)
|
renderTierGrid(spend)
|
||||||
|
|
||||||
const savings = spend * 0.15
|
const savings = spend * 0.15
|
||||||
const net_monthly = savings - tier.price
|
const net_monthly = savings - tier.price
|
||||||
const net_annual = net_monthly * 12
|
const net_annual = net_monthly * 12
|
||||||
const multiplier = tier.price > 0 ? (savings / tier.price) : 0
|
const multiplier = tier.price > 0 ? (savings / tier.price) : 0
|
||||||
|
|
||||||
document.getElementById('roi-savings').textContent = fmtUSD(Math.round(savings))
|
document.getElementById('roi-savings').textContent = fmtUSD(Math.round(savings))
|
||||||
document.getElementById('roi-savings-sub').textContent = '15% av ' + fmtUSD(spend) + ' via optimalisering og varsler'
|
document.getElementById('roi-savings-sub').textContent = '15% av ' + fmtUSD(spend) + ' via optimalisering og varsler'
|
||||||
|
|
||||||
document.getElementById('roi-multiplier').textContent = multiplier.toFixed(2) + '×'
|
document.getElementById('roi-multiplier').textContent = multiplier.toFixed(2) + '×'
|
||||||
document.getElementById('roi-multiplier-sub').textContent = 'per måned · basert på ' + fmtUSD(tier.price) + '/mnd'
|
document.getElementById('roi-multiplier-sub').textContent = 'per måned · basert på ' + fmtUSD(tier.price) + '/mnd'
|
||||||
|
|
||||||
const annualEl = document.getElementById('roi-annual')
|
const annualEl = document.getElementById('roi-annual')
|
||||||
if(net_annual >= 0){
|
if(net_annual >= 0){
|
||||||
annualEl.textContent = fmtUSD(Math.round(net_annual))
|
annualEl.textContent = fmtUSD(Math.round(net_annual))
|
||||||
|
|
@ -762,9 +756,7 @@ function browserRefresh(){
|
||||||
const f = document.getElementById('browser-frame')
|
const f = document.getElementById('browser-frame')
|
||||||
if(f.src) f.src = f.src
|
if(f.src) f.src = f.src
|
||||||
}
|
}
|
||||||
function browserFrameLoad(){
|
function browserFrameLoad(){ document.getElementById('browser-error').classList.remove('show') }
|
||||||
document.getElementById('browser-error').classList.remove('show')
|
|
||||||
}
|
|
||||||
function browserFrameError(){
|
function browserFrameError(){
|
||||||
document.getElementById('browser-error-msg').textContent = 'Siden kunne ikke lastes — sjekk URL eller /docs/-proxy'
|
document.getElementById('browser-error-msg').textContent = 'Siden kunne ikke lastes — sjekk URL eller /docs/-proxy'
|
||||||
document.getElementById('browser-error').classList.add('show')
|
document.getElementById('browser-error').classList.add('show')
|
||||||
|
|
@ -789,24 +781,45 @@ async function pollBuild(){
|
||||||
pollBuild()
|
pollBuild()
|
||||||
setInterval(pollBuild,30000)
|
setInterval(pollBuild,30000)
|
||||||
|
|
||||||
// BILLING
|
// ─── Fix 3: CostGuard — fetchWithTimeout helper (AbortController, 10s) ───
|
||||||
|
function fetchWithTimeout(url, ms=10000){
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
const timer = setTimeout(()=>ctrl.abort(), ms)
|
||||||
|
return fetch(url, { signal: ctrl.signal })
|
||||||
|
.then(r=>{ clearTimeout(timer); return r })
|
||||||
|
.catch(e=>{ clearTimeout(timer); throw e })
|
||||||
|
}
|
||||||
|
|
||||||
|
// BILLING — Fix 3: /billing/credits?days=30 + AbortController 10s + fallback
|
||||||
async function loadBilling(){
|
async function loadBilling(){
|
||||||
const el=document.getElementById('billing-body')
|
const el=document.getElementById('billing-body')
|
||||||
el.innerHTML='<div class="empty-state"><div class="empty-icon">⏳</div><div class="empty-title">Laster data...</div></div>'
|
el.innerHTML='<div class="empty-state"><div class="empty-icon">⏳</div><div class="empty-title">Laster data...</div></div>'
|
||||||
try{
|
try{
|
||||||
const [sum,anm,mod] = await Promise.all([
|
const [sum, anm, mod, cred] = await Promise.all([
|
||||||
fetch('/billing/summary').then(r=>r.json()).catch(()=>null),
|
fetchWithTimeout('/billing/summary')
|
||||||
fetch('/billing/anomalies').then(r=>r.json()).catch(()=>null),
|
.then(r=>r.ok?r.json():null).catch(()=>null),
|
||||||
fetch('/billing/tokens/by-module').then(r=>r.json()).catch(()=>null),
|
fetchWithTimeout('/billing/anomalies')
|
||||||
|
.then(r=>r.ok?r.json():null).catch(()=>null),
|
||||||
|
fetchWithTimeout('/billing/tokens/by-module')
|
||||||
|
.then(r=>r.ok?r.json():null).catch(()=>null),
|
||||||
|
fetchWithTimeout('/billing/credits?days=30')
|
||||||
|
.then(r=>r.ok?r.json():null).catch(()=>null),
|
||||||
])
|
])
|
||||||
|
|
||||||
const total = sum?.total_tokens ?? '—'
|
const total = sum?.total_tokens ?? '—'
|
||||||
const cost = sum?.estimated_cost_usd!=null ? '$'+Number(sum.estimated_cost_usd).toFixed(4) : '—'
|
const cost = sum?.estimated_cost_usd!=null ? '$'+Number(sum.estimated_cost_usd).toFixed(4) : '—'
|
||||||
const budg = sum?.budget_limit!=null ? '$'+sum.budget_limit : '—'
|
const budg = sum?.budget_limit!=null ? '$'+sum.budget_limit : '—'
|
||||||
|
const credVal = cred?.credits_remaining!=null
|
||||||
|
? '$'+Number(cred.credits_remaining).toFixed(2)
|
||||||
|
: 'Ingen data tilgjengelig'
|
||||||
|
|
||||||
let h = `<div class="stat-grid">
|
let h = `<div class="stat-grid">
|
||||||
<div class="stat-card c-accent"><div class="stat-label">Totale tokens (30d)</div><div class="stat-value">${typeof total==='number'?total.toLocaleString('no'):total}</div><div class="stat-sub">Alle moduler</div></div>
|
<div class="stat-card c-accent"><div class="stat-label">Totale tokens (30d)</div><div class="stat-value">${typeof total==='number'?total.toLocaleString('no'):total}</div><div class="stat-sub">Alle moduler</div></div>
|
||||||
<div class="stat-card c-green"><div class="stat-label">Estimert kostnad</div><div class="stat-value">${cost}</div><div class="stat-sub">Vertex AI · Gemini 2.5 Pro</div></div>
|
<div class="stat-card c-green"><div class="stat-label">Estimert kostnad</div><div class="stat-value">${cost}</div><div class="stat-sub">Vertex AI · Gemini 2.5 Pro</div></div>
|
||||||
<div class="stat-card c-amber"><div class="stat-label">Budsjettgrense</div><div class="stat-value">${budg}</div><div class="stat-sub">Månedlig maks</div></div>
|
<div class="stat-card c-amber"><div class="stat-label">Budsjettgrense</div><div class="stat-value">${budg}</div><div class="stat-sub">Månedlig maks</div></div>
|
||||||
|
<div class="stat-card" style="border-color:rgba(0,212,168,.3);background:rgba(0,212,168,.06)"><div class="stat-label">Credits igjen (30d)</div><div class="stat-value" style="font-size:${credVal.length>8?'18px':'26px'}">${esc(credVal)}</div><div class="stat-sub">/billing/credits?days=30</div></div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
const anoms = anm?.anomalies||[]
|
const anoms = anm?.anomalies||[]
|
||||||
if(anoms.length){
|
if(anoms.length){
|
||||||
h+='<div class="sec-title">⚠️ Anomalier</div><div class="anomaly-list">'
|
h+='<div class="sec-title">⚠️ Anomalier</div><div class="anomaly-list">'
|
||||||
|
|
@ -816,6 +829,7 @@ async function loadBilling(){
|
||||||
})
|
})
|
||||||
h+='</div>'
|
h+='</div>'
|
||||||
}else{ h+='<div class="sec-title" style="color:var(--green)">✅ Ingen anomalier</div>' }
|
}else{ h+='<div class="sec-title" style="color:var(--green)">✅ Ingen anomalier</div>' }
|
||||||
|
|
||||||
const mods = mod?.modules||mod?.data||[]
|
const mods = mod?.modules||mod?.data||[]
|
||||||
const cols = ['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e','#5c8fff','#ff9f7c','#c084fc']
|
const cols = ['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e','#5c8fff','#ff9f7c','#c084fc']
|
||||||
if(mods.length){
|
if(mods.length){
|
||||||
|
|
@ -823,15 +837,15 @@ async function loadBilling(){
|
||||||
h+='<table class="mod-table"><thead><tr><th>Modul</th><th>Tokens</th><th>Input</th><th>Output</th></tr></thead><tbody>'
|
h+='<table class="mod-table"><thead><tr><th>Modul</th><th>Tokens</th><th>Input</th><th>Output</th></tr></thead><tbody>'
|
||||||
mods.forEach((m,i)=>{ h+=`<tr><td><span class="mod-dot" style="background:${cols[i%cols.length]}"></span>${esc(m.module||m.name||'—')}</td><td>${(m.total_tokens||0).toLocaleString('no')}</td><td>${(m.input_tokens||0).toLocaleString('no')}</td><td>${(m.output_tokens||0).toLocaleString('no')}</td></tr>` })
|
mods.forEach((m,i)=>{ h+=`<tr><td><span class="mod-dot" style="background:${cols[i%cols.length]}"></span>${esc(m.module||m.name||'—')}</td><td>${(m.total_tokens||0).toLocaleString('no')}</td><td>${(m.input_tokens||0).toLocaleString('no')}</td><td>${(m.output_tokens||0).toLocaleString('no')}</td></tr>` })
|
||||||
h+='</tbody></table>'
|
h+='</tbody></table>'
|
||||||
h+=`<div style="display:flex;justify-content:center;margin-top:28px">
|
h+=`<div style="display:flex;justify-content:center;margin-top:28px"><canvas id="mod-donut" width="260" height="260"></canvas></div>`
|
||||||
<canvas id="mod-donut" width="260" height="260"></canvas>
|
|
||||||
</div>`
|
|
||||||
el.innerHTML=h
|
el.innerHTML=h
|
||||||
renderDonut(mods,cols)
|
renderDonut(mods,cols)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
el.innerHTML=h
|
el.innerHTML=h
|
||||||
}catch(e){ el.innerHTML=`<div class="empty-state"><div class="empty-icon">❌</div><div class="empty-title">Feil</div><div class="empty-desc">${e.message}</div></div>` }
|
}catch(e){
|
||||||
|
el.innerHTML=`<div class="empty-state"><div class="empty-icon">❌</div><div class="empty-title">Feil</div><div class="empty-desc">${esc(e.message)}</div></div>`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDonut(mods, cols) {
|
function renderDonut(mods, cols) {
|
||||||
|
|
@ -858,15 +872,11 @@ function renderDonut(mods, cols) {
|
||||||
labels: {
|
labels: {
|
||||||
color: '#8b909c',
|
color: '#8b909c',
|
||||||
font: { size: 11, family: '-apple-system,BlinkMacSystemFont,Inter,system-ui,sans-serif' },
|
font: { size: 11, family: '-apple-system,BlinkMacSystemFont,Inter,system-ui,sans-serif' },
|
||||||
padding: 14,
|
padding: 14, boxWidth: 10, boxHeight: 10,
|
||||||
boxWidth: 10,
|
|
||||||
boxHeight: 10,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: { label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString('no')} tokens` }
|
||||||
label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString('no')} tokens`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -966,7 +976,6 @@ async function openMd(path, name){
|
||||||
const res = await fetch('/docs/'+path+'?t='+Date.now())
|
const res = await fetch('/docs/'+path+'?t='+Date.now())
|
||||||
if(!res.ok) throw new Error('HTTP '+res.status)
|
if(!res.ok) throw new Error('HTTP '+res.status)
|
||||||
const raw = await res.text()
|
const raw = await res.text()
|
||||||
// Fix 2: bruk marked.js i stedet for hjemmelaget renderMd()
|
|
||||||
viewer.innerHTML = marked.parse(raw)
|
viewer.innerHTML = marked.parse(raw)
|
||||||
}catch(e){
|
}catch(e){
|
||||||
viewer.innerHTML=`<div class="md-viewer-loading">❌ Kunne ikke laste: ${esc(e.message)}</div>`
|
viewer.innerHTML=`<div class="md-viewer-loading">❌ Kunne ikke laste: ${esc(e.message)}</div>`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user