diff --git a/static/billing-dashboard.html b/static/billing-dashboard.html index 413b14f..71eaea9 100644 --- a/static/billing-dashboard.html +++ b/static/billing-dashboard.html @@ -25,16 +25,13 @@ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} html{-webkit-font-smoothing:antialiased} body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14px;line-height:1.5;min-height:100vh} - -/* TOPBAR — Google Cloud style */ .topbar{height:56px;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 16px;gap:0;position:sticky;top:0;z-index:200} .tb-logo{display:flex;align-items:center;gap:10px;padding:0 16px;border-right:1px solid var(--border)} .tb-logo-icon{width:28px;height:28px;background:linear-gradient(135deg,#4285f4,#8ab4f8);border-radius:6px;display:flex;align-items:center;justify-content:center} .tb-logo-icon svg{fill:#fff;width:16px;height:16px} .tb-logo-name{font-size:15px;font-weight:500;letter-spacing:-.01em} .tb-logo-name span{color:var(--blue);font-weight:700} -.tb-project{display:flex;align-items:center;gap:6px;padding:6px 12px;margin:0 8px;border-radius:6px;cursor:pointer;transition:background var(--tr);font-size:13px;color:var(--muted)} -.tb-project:hover{background:var(--surface-2)} +.tb-project{display:flex;align-items:center;gap:6px;padding:6px 12px;margin:0 8px;border-radius:6px;font-size:13px;color:var(--muted)} .tb-project .material-symbols-outlined{font-size:16px} .tb-right{margin-left:auto;display:flex;align-items:center;gap:8px} .tb-badge{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:500;padding:4px 10px;border-radius:16px;border:1px solid} @@ -49,8 +46,6 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .pulse{width:7px;height:7px;border-radius:50%;background:var(--green);flex-shrink:0;position:relative} .pulse::after{content:'';position:absolute;inset:-3px;border-radius:50%;background:var(--green);opacity:.4;animation:pulse 2s ease-out infinite} @keyframes pulse{0%{transform:scale(1);opacity:.4}100%{transform:scale(2.5);opacity:0}} - -/* LAYOUT */ .layout{display:flex;min-height:calc(100vh - 56px)} .sidebar{width:240px;flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);padding:12px 0} .nav-section{padding:8px 16px 4px;font-size:11px;font-weight:500;color:var(--muted);letter-spacing:.06em;text-transform:uppercase} @@ -58,9 +53,7 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .nav-item:hover{background:var(--surface-2);color:var(--text)} .nav-item.active{background:var(--blue-bg);color:var(--blue);font-weight:500} .nav-item .material-symbols-outlined{font-size:18px} -.content{flex:1;padding:24px;max-width:1200px;overflow:hidden} - -/* ONBOARDING BANNER */ +.content{flex:1;padding:24px;overflow:hidden} .onboarding-banner{background:linear-gradient(135deg,rgba(66,133,244,0.08),rgba(138,180,248,0.05));border:1px solid var(--blue-border);border-radius:12px;padding:16px 20px;display:flex;align-items:flex-start;gap:14px;margin-bottom:20px} .onboarding-banner.hidden{display:none} .ob-icon{width:36px;height:36px;border-radius:50%;background:var(--blue-bg);display:flex;align-items:center;justify-content:center;flex-shrink:0} @@ -69,8 +62,6 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .ob-body p{font-size:13px;color:var(--muted);line-height:1.6} .ob-close{margin-left:auto;cursor:pointer;color:var(--muted);transition:color var(--tr);background:none;border:none;padding:4px} .ob-close:hover{color:var(--text)} - -/* GEMINI INSIGHT CARD */ .gemini-card{background:linear-gradient(135deg,rgba(138,180,248,0.06),rgba(197,138,249,0.06),rgba(129,201,149,0.04));border:1px solid rgba(138,180,248,0.2);border-radius:12px;padding:16px 20px;margin-bottom:20px;display:flex;align-items:flex-start;gap:14px} .gem-icon{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,#4285f4,#c58af9);display:flex;align-items:center;justify-content:center;flex-shrink:0} .gem-icon svg{fill:#fff;width:16px;height:16px} @@ -80,13 +71,9 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .gem-chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} .gem-chip{background:rgba(138,180,248,0.1);border:1px solid rgba(138,180,248,0.2);color:var(--gem-a);font-size:12px;padding:4px 10px;border-radius:16px;cursor:pointer;transition:all var(--tr)} .gem-chip:hover{background:rgba(138,180,248,0.2)} - -/* PAGE HEADER */ .page-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px} .page-title{font-size:22px;font-weight:400;letter-spacing:-.01em} .page-title strong{font-weight:500} - -/* KPI CARDS */ .kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px} .kpi{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px;transition:box-shadow var(--tr),border-color var(--tr)} .kpi:hover{border-color:var(--border-h);box-shadow:0 1px 8px rgba(0,0,0,.3)} @@ -100,18 +87,12 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .ki-red{background:var(--red-bg)}.ki-red .material-symbols-outlined{color:var(--red)} .kpi-val{font-family:var(--mono);font-size:24px;font-weight:500;letter-spacing:-.02em;margin-bottom:6px} .kpi-delta{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:4px} -.kpi-delta .material-symbols-outlined{font-size:14px} -.delta-up{color:var(--yellow)}.delta-dn{color:var(--green)} - -/* CHARTS */ .charts-row{display:grid;grid-template-columns:2fr 1fr;gap:12px;margin-bottom:20px} .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px} .card-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px} .card-title{font-size:14px;font-weight:500} .card-sub{font-size:12px;color:var(--muted)} .chart-wrap{position:relative;height:200px} - -/* BOTTOM ROW */ .bottom-row{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px} .anomaly-row{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:8px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px} .anomaly-row:last-child{border-bottom:none} @@ -128,23 +109,15 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .svc-bar-w{width:80px;height:2px;background:var(--surface-3);border-radius:2px;flex-shrink:0} .svc-bar{height:100%;border-radius:2px;transition:width .8s cubic-bezier(.16,1,.3,1)} .svc-cost{font-family:var(--mono);font-size:12px;color:var(--muted);width:60px;text-align:right;flex-shrink:0} - -/* BUDGET GAUGE */ .gauge-wrap{display:flex;flex-direction:column;align-items:center;gap:12px;padding:8px 0} .gauge-ring{position:relative;width:130px;height:72px;overflow:hidden} .gauge-ring svg{position:absolute;top:0;left:0} .gauge-num{position:absolute;bottom:0;left:50%;transform:translateX(-50%);font-family:var(--mono);font-size:20px;font-weight:500;letter-spacing:-.02em} .gauge-meta{font-size:12px;color:var(--muted);text-align:center} - -/* EMPTY STATE */ .empty-state{color:var(--muted);font-size:13px;padding:24px 0;text-align:center;display:flex;flex-direction:column;align-items:center;gap:8px} .empty-state .material-symbols-outlined{font-size:32px;opacity:.3} - -/* ERROR BANNER */ .err-banner{background:var(--red-bg);border:1px solid rgba(234,67,53,0.2);border-radius:8px;padding:10px 14px;font-size:12px;color:var(--red);display:none;align-items:center;gap:8px;margin-bottom:16px} .err-banner.show{display:flex} - -/* CHAT PANEL */ .chat-fab{position:fixed;bottom:24px;right:24px;width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg,#4285f4,#8ab4f8);box-shadow:0 4px 16px rgba(66,133,244,0.4);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:300;transition:transform var(--tr),box-shadow var(--tr);border:none} .chat-fab:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(66,133,244,0.5)} .chat-fab svg{fill:#fff;width:24px;height:24px} @@ -173,14 +146,12 @@ body{font-family:var(--font);background:var(--bg);color:var(--text);font-size:14 .chat-send{width:36px;height:36px;border-radius:50%;background:var(--blue);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background var(--tr)} .chat-send:hover{background:#2d6fd6} .chat-send .material-symbols-outlined{font-size:18px;color:#fff} - footer{padding:12px 24px;font-size:11px;color:var(--faint);border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between} @media(max-width:1024px){.sidebar{display:none}.kpi-row{grid-template-columns:repeat(2,1fr)}.charts-row,.bottom-row{grid-template-columns:1fr}.chat-panel{width:100%}} -
- -
- -
+ - -
+
Gemini Insight
-
Analyserer dine GCP-kostnader...
+
Henter GCP-data...
Hva koster mest? - Prognose neste måned + Prognose neste måned Anomalier? + Budsjett-status
warning - API utilgjengelig — viser demo-data. + Kan ikke nå API.
-

Billing Oversikt

- Laster... + Henter data...
-
Totalkostnad MTD
payments
-
kr —
-
arrow_upwardhittil denne måneden
+
+
hittil denne måneden
Daglig snitt (7d)
today
-
kr —
-
snitt siste 7 dager
+
+
snitt siste 7 dager
Prognose måneden
trending_up
-
kr —
-
calendar_month— dager gjenstår
+
+
— dager gjenstår
@@ -314,48 +274,62 @@ footer{padding:12px 24px;font-size:11px;color:var(--faint);border-top:1px solid
warning
-
siste 24t · ingen avvik
+
siste 24t
-
Daglig kostnad 30 dager · netto etter kreditter
-
+
+
+ bar_chart + Ingen daglig data tilgjengelig ennå +
+ +
Fordeling per tjeneste Andel av total
-
+
+
+ donut_large + Ingen tjenestedata tilgjengelig ennå +
+ +
-
Anomali-deteksjon OK
-
check_circleIngen anomalier
+
+
check_circleIngen anomalier
+
Kostnad per tjeneste Rangert
-
+
+
listIngen data
+
Budsjett-status - Mai 2026 +
@@ -368,7 +342,7 @@ footer{padding:12px 24px;font-size:11px;color:var(--faint);border-top:1px solid
kr 0,00
-
av kr 500 budsjett
+
av kr 500 budsjett
— gjenstår
@@ -378,18 +352,15 @@ footer{padding:12px 24px;font-size:11px;color:var(--faint);border-top:1px solid
- - -
@@ -402,10 +373,8 @@ footer{padding:12px 24px;font-size:11px;color:var(--faint);border-top:1px solid
-
- -
-
Hei! Jeg er din GCP-kostnadsassistent. Spør meg om kostnader, anomalier eller prognoser for prosjektet ditt.
+
+
Hei! Jeg er din GCP-kostnadsassistent. Spør meg om kostnader, anomalier eller prognoser.
@@ -423,23 +392,18 @@ const BUDGET_NOK=500; const REFRESH_MS=5*60*1000; const SVC_COLORS=['#4285f4','#34a853','#fbbc04','#ea4335','#8ab4f8','#81c995','#c58af9','#ff8c42']; -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},{service:'Pub/Sub',total_cost:0.04}, - {service:'Cloud Storage',total_cost:0.03} -]; -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:1}; -const DEMO_ANOMALIES=[]; - let barChart,donutChart,period=30; -let currentData={summary:DEMO_SUMMARY,forecast:DEMO_FORECAST,anomalies:DEMO_ANOMALIES}; -let isLiveData=false; +let currentData={summary:[],forecast:{},anomalies:[]}; -function fmtNOK(v,d=2){return 'kr '+Number(v).toLocaleString('no-NO',{minimumFractionDigits:d,maximumFractionDigits:d})} +function fmtNOK(v,d=2){ + const n=Math.abs(Number(v)||0); + return 'kr '+n.toLocaleString('no-NO',{minimumFractionDigits:d,maximumFractionDigits:d}); +} +function fmtDager(n){return n===1?'1 dag gjenstår':n+' dager gjenstår';} function animN(el,end,fmt,dur=700){ + const abs=Math.abs(end); 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*end);if(p<1)requestAnimationFrame(go);}; + const go=now=>{const p=Math.min((now-t0)/dur,1),e=1-Math.pow(1-p,3);el.textContent=fmt(e*abs);if(p<1)requestAnimationFrame(go);}; requestAnimationFrame(go); } @@ -455,82 +419,101 @@ async function fetchAll(){ if(!sr.ok||!fr.ok||!ar.ok)throw new Error('HTTP '+sr.status); const s=await sr.json(),f=await fr.json(),a=await ar.json(); if(s.error||f.error)throw new Error(s.error||f.error); - const summary=s.summary||[]; - const hasLiveData=summary.some(x=>(x.total_cost||0)!==0); - // Onboarding banner - if(s.onboarding_status||!hasLiveData){ + + // Onboarding: vis banner hvis API sier awaiting_data + if(s.onboarding_status&&s.onboarding_status.state==='awaiting_data'){ document.getElementById('onboarding-banner').classList.remove('hidden'); } else { document.getElementById('onboarding-banner').classList.add('hidden'); } - isLiveData=hasLiveData; - currentData={summary:hasLiveData?summary:DEMO_SUMMARY,forecast:f,anomalies:a.anomalies||[]}; + + currentData={summary:s.summary||[],forecast:f,anomalies:a.anomalies||[]}; document.getElementById('err-banner').classList.remove('show'); dot.style.background='var(--green)'; updateGeminiInsight(); }catch(err){ - console.warn('API feil:',err.message); - currentData={summary:DEMO_SUMMARY,forecast:DEMO_FORECAST,anomalies:DEMO_ANOMALIES}; - document.getElementById('err-msg').textContent='Tilkobling feilet: '+err.message+' — viser demo-data.'; + console.error('API feil:',err.message); + document.getElementById('err-msg').textContent='Kan ikke nå API: '+err.message; document.getElementById('err-banner').classList.add('show'); dot.style.background='var(--red)'; - setTimeout(()=>dot.style.background='var(--green)',3000); } renderAll(); } function updateGeminiInsight(){ const{summary,forecast,anomalies}=currentData; - const top=summary[0]; - let msg='Ditt GCP-prosjekt har ingen vesentlige kostnader registrert ennå. BigQuery billing export tar 24–48 timer å populere første gang.'; - if(top&&top.total_cost>0){ - msg=`Høyeste kostnad: ${top.service} (${fmtNOK(top.total_cost)}). Månedsprognose: ${fmtNOK(forecast.total_monthly_forecast||0)}. ${anomalies.length?anomalies.length+' anomali(er) oppdaget — sjekk listen.':'Ingen anomalier detektert.'}`; + const top=summary.find(s=>(s.total_cost||0)>0); + let msg; + if(!top){ + msg='Ingen vesentlige GCP-kostnader registrert ennå. BigQuery billing export tar 24–48 timer å populere første gang.'; + } else { + const parts=[`Høyeste kostnad: ${top.service} (${fmtNOK(top.total_cost)})`]; + if(forecast.total_monthly_forecast)parts.push(`Månedsprognose: ${fmtNOK(forecast.total_monthly_forecast)}`); + parts.push(anomalies.length?anomalies.length+' anomali(er) oppdaget — sjekk listen.':'Ingen anomalier detektert.'); + msg=parts.join('. '); } document.getElementById('gem-text').textContent=msg; } -function genDailyFromMTD(mtd,days){ - const base=Math.max(mtd/days,0.001); - const arr=Array.from({length:days},()=>Math.max(0,parseFloat((base*(0.7+Math.random()*0.6)).toFixed(4)))); - arr[arr.length-4]*=2.1; - const sum=arr.reduce((a,b)=>a+b,0); - return arr.map(v=>v*(mtd||0.1)/sum); -} function genLabels(n){ return Array.from({length:n},(_,i)=>{const d=new Date();d.setDate(d.getDate()-(n-1-i));return d.toLocaleDateString('no-NO',{day:'numeric',month:'short'});}); } function renderAll(){ const{summary,forecast,anomalies}=currentData; - const costs=genDailyFromMTD(forecast.month_to_date_cost||3,period); - buildBar(costs);buildDonut(summary);buildSvc(summary);buildAnom(anomalies);updateKPIs(forecast,anomalies.length); + buildDonut(summary); + buildSvc(summary); + buildAnom(anomalies); + updateKPIs(forecast,anomalies.length); + // Bar chart: bare vis hvis vi har MTD-data + const mtd=forecast.month_to_date_cost||0; + if(Math.abs(mtd)>0.000001){ + buildBar(mtd,period); + } else { + // Vis empty state + document.getElementById('bar-empty').style.display='flex'; + document.getElementById('bar-chart').style.display='none'; + if(barChart){barChart.destroy();barChart=null;} + } const now=new Date().toLocaleTimeString('no-NO',{hour:'2-digit',minute:'2-digit'}); - document.getElementById('footer-txt').textContent='Oppdatert '+now+(isLiveData?' · live GCP-data':' · demo-data'); + document.getElementById('footer-txt').textContent='Oppdatert '+now+' · live GCP-data'; document.getElementById('last-updated').textContent='Sist oppdatert: '+now; - // Budget month const mn=['Januar','Februar','Mars','April','Mai','Juni','Juli','August','September','Oktober','November','Desember']; document.getElementById('budget-month').textContent=mn[new Date().getMonth()]+' '+new Date().getFullYear(); } -function buildBar(costs){ - const labels=genLabels(costs.length); - const avg7=costs.map((_,i)=>{const s=costs.slice(Math.max(0,i-6),i+1);return s.reduce((a,b)=>a+b,0)/s.length;}); - const avgC=costs.reduce((a,b)=>a+b,0)/costs.length; +function buildBar(mtd,days){ + // Fordel MTD jevnt over dagene — reell fordeling + // Siden BigQuery ikke gir per-dag historikk via dette endepunktet, + // viser vi MTD som én bar på dagens dato og tomme for resten + const costs=Array(days).fill(0); + costs[costs.length-1]=Math.abs(mtd); + const labels=genLabels(days); + document.getElementById('bar-empty').style.display='none'; + document.getElementById('bar-chart').style.display='block'; const ctx=document.getElementById('bar-chart').getContext('2d'); if(barChart)barChart.destroy(); barChart=new Chart(ctx,{type:'bar',data:{labels,datasets:[ - {label:'Kostnad',data:costs,backgroundColor:costs.map(v=>v>avgC*1.8?'rgba(234,67,53,0.6)':v>avgC*1.3?'rgba(251,188,4,0.55)':'rgba(66,133,244,0.45)'),borderRadius:4,borderSkipped:false,order:2}, - {label:'7d snitt',data:avg7,type:'line',borderColor:'rgba(138,180,248,0.5)',borderWidth:1.5,pointRadius:0,tension:.4,fill:false,order:1} + {label:'Kostnad',data:costs,backgroundColor:'rgba(66,133,244,0.5)',borderRadius:4,borderSkipped:false} ]},options:{responsive:true,maintainAspectRatio:false, - plugins:{legend:{display:false},tooltip:{backgroundColor:'rgba(22,27,34,.97)',borderColor:'rgba(139,148,158,.15)',borderWidth:1,titleColor:'#8b949e',bodyColor:'#e6edf3',titleFont:{family:'Google Sans',size:11},bodyFont:{family:'Google Sans Mono',size:12,weight:'600'},callbacks:{label:c=>' '+fmtNOK(c.raw,4)}}}, - scales:{x:{grid:{display:false},ticks:{color:'#30363d',font:{size:10},maxTicksLimit:8}},y:{grid:{color:'rgba(139,148,158,.06)'},ticks:{color:'#30363d',font:{family:'Google Sans Mono',size:10},callback:v=>'kr '+v.toFixed(3)},border:{display:false}}}}}); + plugins:{legend:{display:false},tooltip:{backgroundColor:'rgba(22,27,34,.97)',borderColor:'rgba(139,148,158,.15)',borderWidth:1,titleColor:'#8b949e',bodyColor:'#e6edf3',callbacks:{label:c=>' '+fmtNOK(c.raw,4)}}}, + scales:{x:{grid:{display:false},ticks:{color:'#30363d',font:{size:10},maxTicksLimit:8}},y:{grid:{color:'rgba(139,148,158,.06)'},ticks:{color:'#30363d',font:{family:'Google Sans Mono',size:10},callback:v=>'kr '+v.toFixed(4)},border:{display:false}}}}}); } function buildDonut(summary){ + const hasData=summary.some(s=>(s.total_cost||0)>0); + if(!hasData){ + document.getElementById('donut-empty').style.display='flex'; + document.getElementById('donut-chart').style.display='none'; + if(donutChart){donutChart.destroy();donutChart=null;} + return; + } + document.getElementById('donut-empty').style.display='none'; + document.getElementById('donut-chart').style.display='block'; const ctx=document.getElementById('donut-chart').getContext('2d'); if(donutChart)donutChart.destroy(); - const top=summary.slice(0,7); - donutChart=new Chart(ctx,{type:'doughnut',data:{labels:top.map(s=>s.service),datasets:[{data:top.map(s=>Math.max(s.total_cost||0,0.001)),backgroundColor:SVC_COLORS,borderColor:'#161b22',borderWidth:2,hoverBorderWidth:3}]}, + const top=summary.filter(s=>(s.total_cost||0)>0).slice(0,7); + donutChart=new Chart(ctx,{type:'doughnut',data:{labels:top.map(s=>s.service),datasets:[{data:top.map(s=>s.total_cost||0),backgroundColor:SVC_COLORS,borderColor:'#161b22',borderWidth:2,hoverBorderWidth:3}]}, options:{responsive:true,maintainAspectRatio:false,cutout:'70%', plugins:{legend:{display:false},tooltip:{backgroundColor:'rgba(22,27,34,.97)',borderColor:'rgba(139,148,158,.15)',borderWidth:1,titleColor:'#8b949e',bodyColor:'#e6edf3',callbacks:{label:c=>' '+fmtNOK(c.raw)}}}}}); } @@ -538,13 +521,18 @@ function buildDonut(summary){ function buildSvc(summary){ const el=document.getElementById('svc-list'); const top=summary.slice(0,8); - const max=Math.max(...top.map(s=>s.total_cost||0),0.001); + if(!top.length){ + el.innerHTML='
listIngen data
'; + return; + } + const max=Math.max(...top.map(s=>Math.abs(s.total_cost||0)),0.000001); el.innerHTML=top.map((s,i)=>{ - const c=Math.max(s.total_cost||0,0); + const c=s.total_cost||0; + const pct=(Math.abs(c)/max*100).toFixed(1); return`
${s.service} -
+
${fmtNOK(c)}
`; }).join(''); @@ -554,7 +542,7 @@ function buildAnom(anomalies){ const el=document.getElementById('anom-list'); const tb=document.getElementById('top-badge'),ab=document.getElementById('anom-badge'); if(!anomalies.length){ - el.innerHTML=`
check_circleIngen anomalier detektert
`; + el.innerHTML='
check_circleIngen anomalier detektert
'; tb.className='tb-badge ok';tb.innerHTML='check_circle Ingen anomalier'; ab.className='tb-badge ok';ab.textContent='OK'; return; @@ -565,13 +553,14 @@ function buildAnom(anomalies){ } function buildGauge(mtd){ - const pct=Math.min(Math.max(mtd/BUDGET_NOK,0),1); + const abs=Math.abs(mtd); + const pct=Math.min(abs/BUDGET_NOK,1); const arc=document.getElementById('gauge-arc'); arc.style.stroke=pct>.8?'var(--red)':pct>.6?'var(--yellow)':'var(--blue)'; setTimeout(()=>arc.style.strokeDashoffset=173*(1-pct),80); document.getElementById('gauge-pct').textContent=Math.round(pct*100)+'%'; - document.getElementById('gauge-used').textContent=fmtNOK(mtd); - const r=BUDGET_NOK-mtd; + document.getElementById('gauge-used').textContent=fmtNOK(abs); + const r=BUDGET_NOK-abs; const rel=document.getElementById('gauge-remain'); rel.textContent=r>0?fmtNOK(r)+' gjenstår':'Budsjett overskredet'; rel.style.color=r<0?'var(--red)':'var(--muted)'; @@ -585,8 +574,8 @@ function updateKPIs(forecast,anomCount){ animN(document.getElementById('k-mtd'),mtd,v=>'kr '+v.toFixed(2)); animN(document.getElementById('k-daily'),daily,v=>'kr '+v.toFixed(4)); animN(document.getElementById('k-fc'),fc,v=>'kr '+v.toFixed(2)); - animN(document.getElementById('k-anom'),anomCount,v=>Math.round(v).toString()); - document.getElementById('k-fc-d').textContent=left+' dager gjenstår'; + document.getElementById('k-anom').textContent=anomCount.toString(); + document.getElementById('k-fc-d').textContent=fmtDager(left); const kd=document.getElementById('k-anom-d'); kd.textContent=anomCount?anomCount+' krever gjennomgang':'siste 24t · ingen avvik'; kd.style.color=anomCount?'var(--red)':'var(--green)'; @@ -596,60 +585,54 @@ function updateKPIs(forecast,anomCount){ function updatePeriod(n){ period=n; document.getElementById('period-lbl').textContent=n+' dager · netto etter kreditter'; - buildBar(genDailyFromMTD(currentData.forecast.month_to_date_cost||3,n)); + const mtd=currentData.forecast.month_to_date_cost||0; + if(Math.abs(mtd)>0.000001)buildBar(mtd,n); } -// CHAT let chatOpen=false; function toggleChat(){ chatOpen=!chatOpen; document.getElementById('chat-panel').classList.toggle('open',chatOpen); if(chatOpen)document.getElementById('chat-input').focus(); } - function askGemini(q){ if(!chatOpen)toggleChat(); document.getElementById('chat-input').value=q; sendChat(); } - function sendChat(){ const inp=document.getElementById('chat-input'); const msg=inp.value.trim(); if(!msg)return; inp.value=''; addChatMsg(msg,'user'); - // Simulate AI response based on current data setTimeout(()=>{ const{summary,forecast,anomalies}=currentData; - let resp='Jeg analyserer dataene dine...'; + let resp; const ml=msg.toLowerCase(); - if(ml.includes('koster')&&summary.length){ - const top=summary.slice(0,3); - resp='Topp 3 kostnadsdrivere: '+top.map(s=>`${s.service} (${fmtNOK(s.total_cost)})`).join(', ')+'.'; + if(ml.includes('koster')||ml.includes('kostnad')){ + const top=summary.filter(s=>(s.total_cost||0)>0).slice(0,3); + resp=top.length?'Topp kostnadsdrivere: '+top.map(s=>`${s.service} (${fmtNOK(s.total_cost)})`).join(', ')+'.':'Ingen kostnader registrert ennå.'; } else if(ml.includes('prognose')||ml.includes('neste')){ - resp=`Månedsprognose: ${fmtNOK(forecast.total_monthly_forecast||0)}. Daglig snitt: ${fmtNOK(forecast.daily_average_last_7_days||0,4)}. ${forecast.remaining_days_in_month||0} dager gjenstår.`; + const fc=forecast.total_monthly_forecast||0; + resp=fc?`Månedsprognose: ${fmtNOK(fc)}. Daglig snitt: ${fmtNOK(forecast.daily_average_last_7_days||0,4)}. ${fmtDager(forecast.remaining_days_in_month||0)}.`:'Ikke nok data for prognose ennå.'; } else if(ml.includes('anomali')){ - resp=anomalies.length?`${anomalies.length} anomali(er) oppdaget: `+anomalies.map(a=>`${a.service} (${a.ratio.toFixed(1)}x normalt)`).join(', '):"Ingen anomalier detektert i dag. Alle tjenester innenfor forventet forbruk."; + resp=anomalies.length?`${anomalies.length} anomali(er): `+anomalies.map(a=>`${a.service} (${a.ratio.toFixed(1)}x normalt)`).join(', '):'Ingen anomalier i dag.'; } else if(ml.includes('budsjett')){ - const mtd=forecast.month_to_date_cost||0; - const pct=((mtd/BUDGET_NOK)*100).toFixed(0); - resp=`Du har brukt ${fmtNOK(mtd)} av ${fmtNOK(BUDGET_NOK)} budsjett (${pct}%). ${mtd${isAi?'':'C'}
-
${text}
`; + div.innerHTML=`
${isAi?'':'C'}
${text}
`; el.appendChild(div); el.scrollTop=el.scrollHeight; }