diff --git a/static/opax.html b/static/opax.html index fa29af6..a8676dc 100644 --- a/static/opax.html +++ b/static/opax.html @@ -688,22 +688,16 @@ function renderTierGrid(spend){ function updatePricing(val){ const spend = Number(val) const tier = activeTier(spend) - document.getElementById('spend-display').textContent = fmtUSD(spend) - renderTierGrid(spend) - const savings = spend * 0.15 const net_monthly = savings - tier.price const net_annual = net_monthly * 12 const multiplier = tier.price > 0 ? (savings / tier.price) : 0 - 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-multiplier').textContent = multiplier.toFixed(2) + '×' document.getElementById('roi-multiplier-sub').textContent = 'per måned · basert på ' + fmtUSD(tier.price) + '/mnd' - const annualEl = document.getElementById('roi-annual') if(net_annual >= 0){ annualEl.textContent = fmtUSD(Math.round(net_annual)) @@ -762,9 +756,7 @@ function browserRefresh(){ const f = document.getElementById('browser-frame') if(f.src) f.src = f.src } -function browserFrameLoad(){ - document.getElementById('browser-error').classList.remove('show') -} +function browserFrameLoad(){ document.getElementById('browser-error').classList.remove('show') } function browserFrameError(){ document.getElementById('browser-error-msg').textContent = 'Siden kunne ikke lastes — sjekk URL eller /docs/-proxy' document.getElementById('browser-error').classList.add('show') @@ -789,24 +781,45 @@ async function pollBuild(){ pollBuild() 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(){ const el=document.getElementById('billing-body') el.innerHTML='
Laster data...
' try{ - const [sum,anm,mod] = await Promise.all([ - fetch('/billing/summary').then(r=>r.json()).catch(()=>null), - fetch('/billing/anomalies').then(r=>r.json()).catch(()=>null), - fetch('/billing/tokens/by-module').then(r=>r.json()).catch(()=>null), + const [sum, anm, mod, cred] = await Promise.all([ + fetchWithTimeout('/billing/summary') + .then(r=>r.ok?r.json():null).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 cost = sum?.estimated_cost_usd!=null?'$'+Number(sum.estimated_cost_usd).toFixed(4):'—' - const budg = sum?.budget_limit!=null?'$'+sum.budget_limit:'—' + const cost = sum?.estimated_cost_usd!=null ? '$'+Number(sum.estimated_cost_usd).toFixed(4) : '—' + 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 = `
Totale tokens (30d)
${typeof total==='number'?total.toLocaleString('no'):total}
Alle moduler
Estimert kostnad
${cost}
Vertex AI · Gemini 2.5 Pro
Budsjettgrense
${budg}
Månedlig maks
+
Credits igjen (30d)
${esc(credVal)}
/billing/credits?days=30
` + const anoms = anm?.anomalies||[] if(anoms.length){ h+='
⚠️ Anomalier
' @@ -816,6 +829,7 @@ async function loadBilling(){ }) h+='
' }else{ h+='
✅ Ingen anomalier
' } + const mods = mod?.modules||mod?.data||[] const cols = ['#7c5cff','#00d4a8','#ffb547','#ff5c7c','#3ecf8e','#5c8fff','#ff9f7c','#c084fc'] if(mods.length){ @@ -823,15 +837,15 @@ async function loadBilling(){ h+='' mods.forEach((m,i)=>{ h+=`` }) h+='
ModulTokensInputOutput
${esc(m.module||m.name||'—')}${(m.total_tokens||0).toLocaleString('no')}${(m.input_tokens||0).toLocaleString('no')}${(m.output_tokens||0).toLocaleString('no')}
' - h+=`
- -
` - el.innerHTML = h - renderDonut(mods, cols) + h+=`
` + el.innerHTML=h + renderDonut(mods,cols) return } el.innerHTML=h - }catch(e){ el.innerHTML=`
Feil
${e.message}
` } + }catch(e){ + el.innerHTML=`
Feil
${esc(e.message)}
` + } } function renderDonut(mods, cols) { @@ -858,15 +872,11 @@ function renderDonut(mods, cols) { labels: { color: '#8b909c', font: { size: 11, family: '-apple-system,BlinkMacSystemFont,Inter,system-ui,sans-serif' }, - padding: 14, - boxWidth: 10, - boxHeight: 10, + padding: 14, boxWidth: 10, boxHeight: 10, } }, tooltip: { - callbacks: { - label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString('no')} tokens` - } + callbacks: { 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()) if(!res.ok) throw new Error('HTTP '+res.status) const raw = await res.text() - // Fix 2: bruk marked.js i stedet for hjemmelaget renderMd() viewer.innerHTML = marked.parse(raw) }catch(e){ viewer.innerHTML=`
❌ Kunne ikke laste: ${esc(e.message)}
`