+
-
-
schedule
BigQuery billing export synkroniserer
-GCP eksporterer fakturadata med 24–48 timers forsinkelse første gang. Dashboard viser demo-data inntil eksporten er populert. Kom tilbake i morgen for å se ditt faktiske forbruk.
+GCP eksporterer fakturadata med 24–48 timers forsinkelse første gang. Kom tilbake i morgen for å se ditt faktiske forbruk.
+
-
-
-
+
@@ -378,18 +352,15 @@ footer{padding:12px 24px;font-size:11px;color:var(--faint);border-top:1px solid
-
-
-
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
@@ -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='${isAi?'':'C'}
- 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}
-
`;
}).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=`
+
${fmtNOK(c)}
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${text}
`;
+ div.innerHTML=`${isAi?'':'C'}
${text}
`;
el.appendChild(div);
el.scrollTop=el.scrollHeight;
}