diff --git a/static/billing-dashboard.html b/static/billing-dashboard.html index 3a88ba5..0bc4615 100644 --- a/static/billing-dashboard.html +++ b/static/billing-dashboard.html @@ -69,12 +69,15 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .dropdown-content a:hover{background:var(--surface-3)} .dropdown:hover .dropdown-content{display:block} -/* ── module grid (CG6 drag) ── */ +/* ── module grid ── */ .module-grid{display:flex;flex-direction:column;gap:12px} .kpi-row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} .charts-row{display:grid;grid-template-columns:2fr 1fr;gap:12px} .bottom-row{display:grid;grid-template-columns:2fr 1fr;gap:12px} +/* ── credits kpi row ── */ +.credits-kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px} + /* ── draggable module ── */ .module{ position:relative; @@ -118,9 +121,20 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .ki-blue{background:var(--blue-bg)}.ki-blue .material-symbols-outlined{color:var(--blue)} .ki-green{background:var(--green-bg)}.ki-green .material-symbols-outlined{color:var(--green)} .ki-yellow{background:var(--yellow-bg)}.ki-yellow .material-symbols-outlined{color:var(--yellow)} +.ki-red{background:var(--red-bg)}.ki-red .material-symbols-outlined{color:var(--red)} .kpi-val{font-size:22px;font-weight:700;font-family:var(--mono);letter-spacing:-.03em} .kpi-delta{font-size:12px;color:var(--muted)} +/* ── credits runway bar ── */ +.runway-bar-wrap{margin-top:14px} +.runway-label{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:6px} +.runway-bar-bg{height:8px;background:var(--surface-3);border-radius:4px;overflow:hidden} +.runway-bar-fill{height:100%;border-radius:4px;transition:width 1.2s cubic-bezier(.16,1,.3,1)} +.credits-warning{display:none;margin-top:12px;padding:10px 14px;border-radius:8px;font-size:12px;align-items:center;gap:8px} +.credits-warning.show{display:flex} +.credits-warning.crit{background:var(--red-bg);color:var(--red);border:1px solid rgba(255,92,124,.2)} +.credits-warning.med{background:var(--yellow-bg);color:var(--yellow);border:1px solid rgba(255,180,84,.2)} + /* ── chart ── */ .chart-wrap{position:relative;height:200px} .empty-state{color:var(--muted);font-size:13px;padding:24px 0;text-align:center;display:flex;flex-direction:column;align-items:center;gap:8px} @@ -165,7 +179,7 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:center} /* ── responsive ── */ -@media(max-width:900px){.kpi-row{grid-template-columns:repeat(2,1fr)}.charts-row,.bottom-row{grid-template-columns:1fr}} +@media(max-width:900px){.kpi-row{grid-template-columns:repeat(2,1fr)}.charts-row,.bottom-row{grid-template-columns:1fr}.credits-kpi-row{grid-template-columns:repeat(2,1fr)}} @media(max-width:720px){ .topbar{height:auto;flex-wrap:wrap;padding:8px 12px;gap:8px} .tb-project{display:none} @@ -178,13 +192,14 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente .nav-item.active{border-left:none;border:1px solid var(--blue)} .content{padding:16px 12px} .kpi-row{grid-template-columns:1fr 1fr;gap:8px} + .credits-kpi-row{grid-template-columns:1fr 1fr;gap:8px} .page-title{font-size:18px} } -@media(max-width:420px){.kpi-row{grid-template-columns:1fr}} +@media(max-width:420px){.kpi-row{grid-template-columns:1fr}.credits-kpi-row{grid-template-columns:1fr}} @media print{ .topbar,.sidebar,#budget-modal{display:none!important} .module{border:1px solid #ddd} - .kpi-row,.charts-row,.bottom-row{grid-template-columns:1fr!important} + .kpi-row,.charts-row,.bottom-row,.credits-kpi-row{grid-template-columns:1fr!important} } @@ -257,7 +272,6 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente Henter data... -
@@ -303,13 +317,98 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente
trending_up
-
— dager gjenbstår
+
— dager gjenstår
+ +
+
+
+ drag_indicator + 💳 GCP Kreditter + Laster... +
+
+ + +
+
+
+ +
+
+
+
+ Kreditter brukt +
credit_card
+
+
+
av total
+
+
+
+
+
+ Gjenstår +
savings
+
+
+
estimert saldo
+
+
+
+
+
+ Daglig burn +
local_fire_department
+
+
+
USD/dag (snitt 7d)
+
+
+
+
+
+ Runway +
schedule
+
+
+
dager igjen
+
+
+
+ + +
+
+ Kredittforbruk + $0 / $— +
+
+
+
+
+ + +
+ warning + +
+ + +
+
+
+ +
@@ -382,7 +481,7 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente
kr 0,00
av kr 500,00 budsjett
-
— gjenbstår
+
— gjenstår
@@ -390,7 +489,7 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente - +
@@ -411,7 +510,7 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente
- +
@@ -432,7 +531,7 @@ footer{padding:10px 20px 16px;font-size:11px;color:var(--faint);text-align:cente
- +
@@ -488,14 +587,13 @@ let currentBudget = parseFloat(localStorage.getItem(LS_BUDGET) || '500'); let barChart, donutChart, period = 30; let currentData = null; -// TODO Fase-C: erstatt med Firestore per-bruker - const DEMO_SUMMARY = [{service:'Cloud Run',total_cost:1.82},{service:'Cloud Build',total_cost:0.61},{service:'Artifact Registry',total_cost:0.23},{service:'BigQuery',total_cost:0.18},{service:'Secret Manager',total_cost:0.09}]; const DEMO_FORECAST = {month_to_date_cost:3.00,daily_average_last_7_days:0.13,total_monthly_forecast:3.52,remaining_days_in_month:21}; const DEMO_ANOMALIES = []; const SVC_COLORS = ['#7c5cff','#00d4a8','#ffb454','#ff5c7c','#a78bfa','#5eead4','#c4b5fd']; function fmtNOK(v,d=2){return 'kr '+Math.abs(Number(v)||0).toLocaleString('no-NO',{minimumFractionDigits:d,maximumFractionDigits:d});} +function fmtUSD(v,d=2){return '$'+(Math.abs(Number(v)||0)).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});} function animN(el,end,fmt,dur=700){const t0=performance.now();const go=now=>{const p=Math.min((now-t0)/dur,1),e=1-Math.pow(1-p,3);el.textContent=fmt(e*Math.abs(end));if(p<1)requestAnimationFrame(go);};requestAnimationFrame(go);} function serviceColor(i){return SVC_COLORS[i%SVC_COLORS.length];} @@ -516,7 +614,7 @@ async function checkAuth(){ setInterval(fetchAll,REFRESH_MS); } -// ── budget (localStorage) ───────────────────────────────────────────────── +// ── budget ──────────────────────────────────────────────────────────────── function openBudgetModal(){document.getElementById('budget-input').value=currentBudget;document.getElementById('budget-modal').style.display='flex';} function closeBudgetModal(){document.getElementById('budget-modal').style.display='none';} function saveBudget(){ @@ -528,7 +626,7 @@ function saveBudget(){ if(currentData)buildGauge(currentData.forecast.month_to_date_cost||0); } -// ── module drag & drop ──────────────────────────────────────────────────── +// ── drag & drop ─────────────────────────────────────────────────────────── let dragSrc=null; function dragStart(e,id){dragSrc=id;e.dataTransfer.effectAllowed='move';document.getElementById(id).classList.add('dragging');} document.addEventListener('dragend',()=>{document.querySelectorAll('.module').forEach(m=>{m.classList.remove('dragging','drag-over');});saveOrder();}); @@ -552,10 +650,8 @@ function saveOrder(){ localStorage.setItem(LS_ORDER,JSON.stringify(ids)); } function restoreLayout(){ - // Restore collapsed state const collapsed=JSON.parse(localStorage.getItem(LS_COLLAPSED)||'[]'); collapsed.forEach(id=>{const m=document.getElementById(id);if(m)m.classList.add('collapsed');}); - // Restore order const order=JSON.parse(localStorage.getItem(LS_ORDER)||'[]'); if(!order.length)return; const grid=document.getElementById('module-grid'); @@ -599,6 +695,111 @@ async function fetchAll(){ renderAll(); fetchTokens(); fetchRecommendations(); + fetchCredits(); // CG4-credits: hentes parallelt med alt annet +} + +// ── CG4-credits: fetchCredits ───────────────────────────────────────────── +async function fetchCredits(){ + document.getElementById('credits-sub').textContent='Oppdaterer...'; + try{ + const r = await apiFetch(API_BASE+'/billing/credits?days=90'); + if(!r.ok) throw new Error('HTTP '+r.status); + const d = await r.json(); + renderCredits(d); + } catch(e){ + document.getElementById('credits-sub').textContent='Ikke tilgjengelig'; + document.getElementById('cr-used').textContent='—'; + document.getElementById('cr-remaining').textContent='—'; + document.getElementById('cr-burn').textContent='—'; + document.getElementById('cr-runway').textContent='—'; + document.getElementById('cr-exhaustion').textContent=e.message; + console.warn('[credits]',e.message); + } +} + +function renderCredits(d){ + // --- KPI-tall --- + const used = d.credits_used_total_usd ?? 0; + const remaining = d.credits_remaining_usd ?? null; + const total = d.credit_total_usd ?? null; + const burn = d.daily_credit_burn_usd ?? 0; + const runway = d.credit_runway_days ?? null; + const exhausted = d.credit_exhaustion_date ?? null; + const pct = total ? Math.min(used/total,1) : null; + + // subtitle + document.getElementById('credits-sub').textContent = + runway != null ? `Runway: ${runway} dager` : 'Sett GOOGLE_CREDIT_TOTAL_USD for runway'; + + // KPI: brukt + animN(document.getElementById('cr-used'), used, v => fmtUSD(v)); + document.getElementById('cr-used-pct').textContent = + (pct!=null) ? `${Math.round(pct*100)}% av ${fmtUSD(total,0)}` : 'total ukjent — sett env-var'; + + // KPI: gjenstår + if(remaining!=null){ + animN(document.getElementById('cr-remaining'), remaining, v => fmtUSD(v)); + } else { + document.getElementById('cr-remaining').textContent = '—'; + } + + // KPI: daglig burn + animN(document.getElementById('cr-burn'), burn, v => fmtUSD(v,4)); + + // KPI: runway + const runwayEl = document.getElementById('cr-runway'); + const iconEl = document.getElementById('cr-runway-icon'); + const exhaustEl = document.getElementById('cr-exhaustion'); + if(runway!=null){ + animN(runwayEl, runway, v => Math.round(v)+' d'); + exhaustEl.textContent = exhausted ? 'tom ca. '+exhausted : 'dager igjen'; + // Ikon-farge basert på kritikalitet + iconEl.className = 'kpi-icon ' + (runway<30 ? 'ki-red' : runway<60 ? 'ki-yellow' : 'ki-green'); + } else { + runwayEl.textContent = '—'; + exhaustEl.textContent = 'sett GOOGLE_CREDIT_TOTAL_USD'; + iconEl.className = 'kpi-icon ki-blue'; + } + + // --- Runway-bar --- + const fill = document.getElementById('cr-bar-fill'); + const barLbl= document.getElementById('cr-bar-label'); + if(pct!=null){ + setTimeout(()=>fill.style.width=(pct*100).toFixed(1)+'%',80); + fill.style.background = pct>0.8 ? 'var(--red)' : pct>0.6 ? 'var(--yellow)' : 'var(--blue)'; + barLbl.textContent = fmtUSD(used)+' / '+fmtUSD(total,0); + } else { + fill.style.width='0%'; + barLbl.textContent = fmtUSD(used)+' brukt'; + } + + // --- Advarsel-banner --- + const warn = document.getElementById('cr-warning'); + const warnMsg = document.getElementById('cr-warning-msg'); + const rawWarn = d.warning ?? ''; + if(rawWarn){ + warnMsg.textContent = rawWarn; + warn.className = 'credits-warning show ' + (runway!=null && runway<30 ? 'crit' : 'med'); + } else { + warn.className = 'credits-warning'; + } + + // --- Kreditter per type (credits_by_type) --- + const types = d.credits_by_type ?? []; + const typesEl = document.getElementById('cr-types'); + if(types.length){ + typesEl.innerHTML = + '
Kreditter per type
' + + types.map(t => + `
+
+ ${t.type??t.credit_type??'Ukjent'} + ${fmtUSD(t.amount??t.total_amount??0)} +
` + ).join(''); + } else { + typesEl.innerHTML = ''; + } } // ── render ──────────────────────────────────────────────────────────────── @@ -627,7 +828,6 @@ function buildBar(history){ } document.getElementById('bar-empty').style.display='none'; document.getElementById('bar-chart').style.display='block'; - // Daglig delta fra MTD const labels=[],costs=[]; history.forEach((e,i)=>{ const d=new Date(e.date); @@ -705,7 +905,7 @@ function buildGauge(mtd){ document.getElementById('gauge-budget-total').textContent=`av ${fmtNOK(currentBudget,0)} budsjett`; const r=currentBudget-mtd; const rel=document.getElementById('gauge-remain'); - rel.textContent=r>0?fmtNOK(r)+' gjenbstår':'Budsjett overskredet'; + rel.textContent=r>0?fmtNOK(r)+' gjenstår':'Budsjett overskredet'; rel.style.color=r<0?'var(--red)':'var(--muted)'; } @@ -718,7 +918,7 @@ function updateKPIs(forecast){ if(daily>0)animN(document.getElementById('k-daily'),daily,v=>fmtNOK(v,4)); else document.getElementById('k-daily').textContent='kr —'; animN(document.getElementById('k-fc'),fc,v=>fmtNOK(v)); - document.getElementById('k-fc-d').textContent=left+' dager gjenbstår'; + document.getElementById('k-fc-d').textContent=left+' dager gjenstår'; buildGauge(mtd); } @@ -732,7 +932,7 @@ function exportCSV(){ document.body.appendChild(a);a.click();document.body.removeChild(a); } -// ── anbefalinger (CG4) ──────────────────────────────────────────────────── +// ── anbefalinger ────────────────────────────────────────────────────────── function fetchRecommendations(){ const ICONS={high:'🔴',medium:'🟡',low:'🟢',anomaly:'⚡',waste:'♻️',budget:'💸'}; fetch(API_BASE+'/billing/recommendations') @@ -757,7 +957,7 @@ function fetchRecommendations(){ .catch(()=>{document.getElementById('recommendations-body').innerHTML='
check_circleIngen anbefalinger tilgjengelig
';}); } -// ── tokenbruk (CG3e) ────────────────────────────────────────────────────── +// ── tokenbruk ───────────────────────────────────────────────────────────── function fetchTokens(){ fetch(API_BASE+'/billing/tokens/summary') .then(r=>r.ok?r.json():Promise.reject(r.status))