feat(CG4d): Prisingskalkulator panel — GCP-spend slider, tier-highlight, ROI-estimat
This commit is contained in:
parent
4a1e23e077
commit
21efdc4e95
164
static/opax.html
164
static/opax.html
|
|
@ -252,6 +252,37 @@
|
||||||
.md-viewer blockquote{border-left:3px solid var(--accent);padding:8px 16px;margin:12px 0;background:rgba(124,92,255,.06);border-radius:0 8px 8px 0;color:var(--text-dim)}
|
.md-viewer blockquote{border-left:3px solid var(--accent);padding:8px 16px;margin:12px 0;background:rgba(124,92,255,.06);border-radius:0 8px 8px 0;color:var(--text-dim)}
|
||||||
.md-viewer hr{border:none;border-top:1px solid var(--border);margin:20px 0}
|
.md-viewer hr{border:none;border-top:1px solid var(--border);margin:20px 0}
|
||||||
.md-viewer-loading{display:flex;align-items:center;justify-content:center;height:200px;color:var(--text-faint);font-family:var(--mono);font-size:13px;gap:10px}
|
.md-viewer-loading{display:flex;align-items:center;justify-content:center;height:200px;color:var(--text-faint);font-family:var(--mono);font-size:13px;gap:10px}
|
||||||
|
|
||||||
|
/* PRICING PANEL (CG4d) */
|
||||||
|
.pricing-hero{text-align:center;padding:32px 0 28px}
|
||||||
|
.pricing-hero-title{font-size:22px;font-weight:800;letter-spacing:-0.02em;margin-bottom:6px}
|
||||||
|
.pricing-hero-sub{font-size:13px;color:var(--text-dim)}
|
||||||
|
.slider-wrap{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:24px 28px;margin-bottom:28px}
|
||||||
|
.slider-label-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
||||||
|
.slider-label{font-size:12px;font-weight:700;color:var(--text-faint);text-transform:uppercase;letter-spacing:0.06em}
|
||||||
|
.slider-value{font-size:22px;font-weight:800;letter-spacing:-0.02em;color:var(--accent-2)}
|
||||||
|
#spend-slider{width:100%;-webkit-appearance:none;appearance:none;height:6px;border-radius:3px;background:var(--bg-3);outline:none;cursor:pointer}
|
||||||
|
#spend-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;background:var(--accent);cursor:pointer;border:3px solid var(--bg);box-shadow:0 0 0 2px var(--accent)}
|
||||||
|
#spend-slider::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:var(--accent);cursor:pointer;border:3px solid var(--bg)}
|
||||||
|
.slider-ticks{display:flex;justify-content:space-between;margin-top:8px}
|
||||||
|
.slider-tick{font-size:10px;color:var(--text-faint);font-family:var(--mono)}
|
||||||
|
.tier-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px}
|
||||||
|
@media(max-width:800px){.tier-grid{grid-template-columns:repeat(2,1fr)}}
|
||||||
|
.tier-card{background:var(--bg-2);border:2px solid var(--border);border-radius:var(--radius);padding:18px 16px;transition:border-color .18s,background .18s,box-shadow .18s;cursor:default}
|
||||||
|
.tier-card.active{border-color:var(--accent);background:rgba(124,92,255,.08);box-shadow:0 0 0 1px var(--accent),0 4px 24px rgba(124,92,255,.18)}
|
||||||
|
.tier-name{font-size:13px;font-weight:800;letter-spacing:0.02em;margin-bottom:4px}
|
||||||
|
.tier-range{font-size:11px;color:var(--text-faint);font-family:var(--mono);margin-bottom:12px}
|
||||||
|
.tier-price{font-size:24px;font-weight:800;letter-spacing:-0.03em}
|
||||||
|
.tier-price-sub{font-size:10px;color:var(--text-faint);margin-top:2px}
|
||||||
|
.tier-card.active .tier-name{color:var(--accent)}
|
||||||
|
.tier-card.active .tier-price{color:var(--text)}
|
||||||
|
.roi-box{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:24px 28px;display:flex;gap:28px;align-items:center;flex-wrap:wrap}
|
||||||
|
.roi-stat{flex:1;min-width:140px}
|
||||||
|
.roi-label{font-size:11px;color:var(--text-faint);text-transform:uppercase;letter-spacing:0.06em;font-weight:600;margin-bottom:6px}
|
||||||
|
.roi-value{font-size:32px;font-weight:800;letter-spacing:-0.03em;color:var(--green)}
|
||||||
|
.roi-sub{font-size:12px;color:var(--text-dim);margin-top:4px;line-height:1.5}
|
||||||
|
.roi-divider{width:1px;height:60px;background:var(--border);flex-shrink:0}
|
||||||
|
@media(max-width:600px){.roi-divider{display:none}.roi-box{gap:16px}}
|
||||||
</style>
|
</style>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -278,6 +309,9 @@
|
||||||
<div class="nav-item" onclick="go('billing')" id="nav-billing">
|
<div class="nav-item" onclick="go('billing')" id="nav-billing">
|
||||||
<span class="nav-icon">💰</span> CostGuard
|
<span class="nav-icon">💰</span> CostGuard
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" onclick="go('pricing')" id="nav-pricing">
|
||||||
|
<span class="nav-icon">🧮</span> Kalkulator
|
||||||
|
</div>
|
||||||
<div class="nav-item" onclick="go('build')" id="nav-build">
|
<div class="nav-item" onclick="go('build')" id="nav-build">
|
||||||
<span class="nav-icon">🔨</span> Cloud Build
|
<span class="nav-icon">🔨</span> Cloud Build
|
||||||
<div class="build-dot" id="sb-build-dot"></div>
|
<div class="build-dot" id="sb-build-dot"></div>
|
||||||
|
|
@ -377,6 +411,60 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PRICING (CG4d) -->
|
||||||
|
<div class="panel" id="panel-pricing">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div class="p-avatar" style="background:linear-gradient(135deg,#7c5cff,#00d4a8)">🧮</div>
|
||||||
|
<div>
|
||||||
|
<div class="p-title">Prisingskalkulator</div>
|
||||||
|
<div class="p-sub">CostGuard · ROI-estimat basert på GCP-spend</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="pricing-hero">
|
||||||
|
<div class="pricing-hero-title">Hva koster CostGuard deg?</div>
|
||||||
|
<div class="pricing-hero-sub">Dra slideren til din månedlige GCP-spend — se tier og ROI øyeblikkelig</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slider-wrap">
|
||||||
|
<div class="slider-label-row">
|
||||||
|
<span class="slider-label">Månedlig GCP-spend</span>
|
||||||
|
<span class="slider-value" id="spend-display">$5 000</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" id="spend-slider" min="0" max="100000" step="500" value="5000" oninput="updatePricing(this.value)">
|
||||||
|
<div class="slider-ticks">
|
||||||
|
<span class="slider-tick">$0</span>
|
||||||
|
<span class="slider-tick">$25k</span>
|
||||||
|
<span class="slider-tick">$50k</span>
|
||||||
|
<span class="slider-tick">$75k</span>
|
||||||
|
<span class="slider-tick">$100k</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tier-grid" id="tier-grid"></div>
|
||||||
|
|
||||||
|
<div class="roi-box">
|
||||||
|
<div class="roi-stat">
|
||||||
|
<div class="roi-label">Estimert månedlig besparelse</div>
|
||||||
|
<div class="roi-value" id="roi-savings">$750</div>
|
||||||
|
<div class="roi-sub" id="roi-savings-sub">15% av GCP-spend via optimalisering og varsler</div>
|
||||||
|
</div>
|
||||||
|
<div class="roi-divider"></div>
|
||||||
|
<div class="roi-stat">
|
||||||
|
<div class="roi-label">ROI — CostGuard betaler seg</div>
|
||||||
|
<div class="roi-value" id="roi-multiplier">0.75×</div>
|
||||||
|
<div class="roi-sub" id="roi-multiplier-sub">per måned · basert på $999/mnd</div>
|
||||||
|
</div>
|
||||||
|
<div class="roi-divider"></div>
|
||||||
|
<div class="roi-stat">
|
||||||
|
<div class="roi-label">Netto gevinst per år</div>
|
||||||
|
<div class="roi-value" id="roi-annual">$3 012</div>
|
||||||
|
<div class="roi-sub">Besparelse minus abonnement · 12 mnd</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- BUILD -->
|
<!-- BUILD -->
|
||||||
<div class="panel" id="panel-build">
|
<div class="panel" id="panel-build">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
|
|
@ -504,6 +592,7 @@ const MD_DOCS = [
|
||||||
const PANEL_META = {
|
const PANEL_META = {
|
||||||
jason: ['Jason', 'Gemini 2.5 Pro · ADK'],
|
jason: ['Jason', 'Gemini 2.5 Pro · ADK'],
|
||||||
billing: ['CostGuard', 'Token & kostnad · live'],
|
billing: ['CostGuard', 'Token & kostnad · live'],
|
||||||
|
pricing: ['Prisingskalkulator', 'CostGuard · ROI-estimat'],
|
||||||
build: ['Cloud Build', 'Pipeline · propane-will-491900-m5'],
|
build: ['Cloud Build', 'Pipeline · propane-will-491900-m5'],
|
||||||
vms: ['VM-er', 'Compute Engine · us-central1-b'],
|
vms: ['VM-er', 'Compute Engine · us-central1-b'],
|
||||||
sites: ['Sites & Moduler', 'Alle Vauco-flater'],
|
sites: ['Sites & Moduler', 'Alle Vauco-flater'],
|
||||||
|
|
@ -520,6 +609,67 @@ const SITES_DATA = [
|
||||||
{icon:'🔧', name:'Jason Chat', url:'https://opax.vauco.no/static/jason.html', desc:'Jason standalone UI', status:'live'},
|
{icon:'🔧', name:'Jason Chat', url:'https://opax.vauco.no/static/jason.html', desc:'Jason standalone UI', status:'live'},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// PRICING TIERS (CG4d — locked from MASTERPLAN)
|
||||||
|
const TIERS = [
|
||||||
|
{ name:'Starter', min:0, max:3000, price:499, color:'var(--accent-2)' },
|
||||||
|
{ name:'Guard', min:3000, max:15000, price:999, color:'var(--accent)' },
|
||||||
|
{ name:'Shield', min:15000, max:50000, price:1999, color:'var(--amber)' },
|
||||||
|
{ name:'Enterprise', min:50000, max:Infinity,price:3500, color:'var(--danger)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function fmtUSD(n){
|
||||||
|
if(n>=1000) return '$'+Math.round(n).toLocaleString('en-US')
|
||||||
|
return '$'+n
|
||||||
|
}
|
||||||
|
function activeTier(spend){
|
||||||
|
return TIERS.find(t => spend >= t.min && spend < t.max) || TIERS[TIERS.length-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTierGrid(spend){
|
||||||
|
const active = activeTier(spend)
|
||||||
|
const grid = document.getElementById('tier-grid')
|
||||||
|
grid.innerHTML = TIERS.map(t => {
|
||||||
|
const isActive = t.name === active.name
|
||||||
|
const rangeStr = t.max === Infinity ? fmtUSD(t.min)+'+' : fmtUSD(t.min)+'–'+fmtUSD(t.max)
|
||||||
|
return `<div class="tier-card${isActive?' active':''}">
|
||||||
|
<div class="tier-name" style="color:${isActive?t.color:'var(--text-faint)'}">${t.name}</div>
|
||||||
|
<div class="tier-range">${rangeStr}/mnd</div>
|
||||||
|
<div class="tier-price" style="color:${isActive?t.color:'var(--text-dim)'}">${fmtUSD(t.price)}</div>
|
||||||
|
<div class="tier-price-sub">per måned</div>
|
||||||
|
</div>`
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePricing(val){
|
||||||
|
const spend = Number(val)
|
||||||
|
const tier = activeTier(spend)
|
||||||
|
|
||||||
|
document.getElementById('spend-display').textContent = fmtUSD(spend)
|
||||||
|
|
||||||
|
renderTierGrid(spend)
|
||||||
|
|
||||||
|
// ROI: 15% of spend saved via CostGuard optimisation
|
||||||
|
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))
|
||||||
|
annualEl.style.color = 'var(--green)'
|
||||||
|
} else {
|
||||||
|
annualEl.textContent = '−' + fmtUSD(Math.round(Math.abs(net_annual)))
|
||||||
|
annualEl.style.color = 'var(--danger)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SESSION
|
// SESSION
|
||||||
function sid(){
|
function sid(){
|
||||||
let s = sessionStorage.getItem(SESSION_KEY)
|
let s = sessionStorage.getItem(SESSION_KEY)
|
||||||
|
|
@ -547,6 +697,13 @@ function go(name){
|
||||||
if(name==='sites') renderSites()
|
if(name==='sites') renderSites()
|
||||||
if(name==='terminal') initTerminal()
|
if(name==='terminal') initTerminal()
|
||||||
if(name==='md') renderMdGrid()
|
if(name==='md') renderMdGrid()
|
||||||
|
if(name==='pricing') initPricing()
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPricing(){
|
||||||
|
const slider = document.getElementById('spend-slider')
|
||||||
|
renderTierGrid(Number(slider.value))
|
||||||
|
updatePricing(slider.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUILD STATUS POLL
|
// BUILD STATUS POLL
|
||||||
|
|
@ -589,7 +746,7 @@ async function loadBilling(){
|
||||||
h+='<div class="sec-title">⚠️ Anomalier</div><div class="anomaly-list">'
|
h+='<div class="sec-title">⚠️ Anomalier</div><div class="anomaly-list">'
|
||||||
anoms.slice(0,5).forEach(a=>{
|
anoms.slice(0,5).forEach(a=>{
|
||||||
const sev=(a.severity||'').toLowerCase()
|
const sev=(a.severity||'').toLowerCase()
|
||||||
h+=`<div class="anom"><div class="anom-icon">${sev==='high'?'🔴':'🟡'}</div><div class="anom-info"><div class="anom-title">${esc(a.service||a.type||'Ukjent')}</div><div class="anom-desc">${esc(a.description||a.message||'')}</div></div><div class="anom-badge ${sev==='high'?'high':''}">${esc(a.severity||'medium')}</div></div>`
|
h+=`<div class="anom"><div class="anom-icon">${sev==='high'?'🔴':'🟡'}</div><div class="anom-info"><div class="anom-title">${esc(a.service||a.type||'Ukjent')}</div><div class="anom-desc">${esc(a.description||a.message||'')}</div></div><div class="anom-badge ${sev==='high'?'high':''}"">${esc(a.severity||'medium')}</div></div>`
|
||||||
})
|
})
|
||||||
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>' }
|
||||||
|
|
@ -890,6 +1047,9 @@ function exportHistory(){
|
||||||
}
|
}
|
||||||
;(function(){ const h=lh();if(!h.length)return;h.forEach(m=>appendMsg(m.role,m.text,m.ts));document.getElementById('messages').scrollTop=99999 })()
|
;(function(){ const h=lh();if(!h.length)return;h.forEach(m=>appendMsg(m.role,m.text,m.ts));document.getElementById('messages').scrollTop=99999 })()
|
||||||
inp.focus()
|
inp.focus()
|
||||||
|
// Init pricing on load so slider is ready if user navigates directly
|
||||||
|
renderTierGrid(5000)
|
||||||
|
updatePricing(5000)
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user