OSVauco/static/onboard.html

431 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>CostGuard — Kom i gang</title>
<style>
:root{
--bg:#0a0a0f;--bg-2:#11131a;--bg-3:#161922;
--border:rgba(255,255,255,0.08);--border-2:rgba(255,255,255,0.14);
--text:#e8eaf0;--text-dim:#8b909c;--text-faint:#5a5f6b;
--accent:#7c5cff;--accent-2:#00d4a8;
--green:#3ecf8e;--amber:#ffb547;--danger:#ff5c7c;
--radius:12px;
--sans:-apple-system,BlinkMacSystemFont,'Inter',system-ui,sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);-webkit-font-smoothing:antialiased;display:flex;align-items:center;justify-content:center;padding:24px}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
.card{
width:100%;max-width:560px;
background:var(--bg-2);border:1px solid var(--border-2);
border-radius:20px;overflow:hidden;
animation:fadeIn .35s ease;
}
.card-head{
padding:32px 36px 24px;
border-bottom:1px solid var(--border);
background:linear-gradient(135deg,rgba(124,92,255,.08),rgba(0,212,168,.05));
}
.logo{display:flex;align-items:center;gap:12px;margin-bottom:20px}
.logo-icon{
width:40px;height:40px;border-radius:12px;
background:linear-gradient(135deg,var(--accent),var(--accent-2));
display:flex;align-items:center;justify-content:center;
font-weight:900;font-size:14px;color:#050509;
}
.logo-name{font-size:18px;font-weight:800;letter-spacing:-.02em}
.logo-sub{font-size:11px;color:var(--text-faint)}
h1{font-size:22px;font-weight:800;letter-spacing:-.02em;margin-bottom:6px}
.subtitle{font-size:14px;color:var(--text-dim)}
/* STEPS */
.steps{display:flex;gap:0;padding:20px 36px 0;border-bottom:1px solid var(--border)}
.step{
flex:1;text-align:center;padding-bottom:16px;
border-bottom:2px solid transparent;
font-size:11px;font-weight:700;color:var(--text-faint);
text-transform:uppercase;letter-spacing:.06em;
transition:color .2s,border-color .2s;
cursor:default;
}
.step.active{color:var(--accent);border-color:var(--accent)}
.step.done{color:var(--green);border-color:var(--green)}
.step-num{
display:inline-flex;align-items:center;justify-content:center;
width:22px;height:22px;border-radius:50%;
background:var(--bg-3);font-size:11px;font-weight:700;
margin-bottom:4px;margin-right:0;
border:1px solid var(--border);
}
.step.active .step-num{background:var(--accent);color:#fff;border-color:var(--accent)}
.step.done .step-num{background:var(--green);color:#050509;border-color:var(--green)}
/* FORM */
.card-body{padding:28px 36px 32px}
.panel{display:none}
.panel.active{display:block;animation:fadeIn .2s ease}
.field{margin-bottom:18px}
label{display:block;font-size:12px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
input,select{
width:100%;padding:10px 14px;
background:var(--bg-3);border:1px solid var(--border);
border-radius:9px;color:var(--text);font-size:14px;
font-family:inherit;outline:none;
transition:border-color .15s;
}
input:focus,select:focus{border-color:var(--accent)}
input::placeholder{color:var(--text-faint)}
select option{background:var(--bg-2)}
.tier-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.tier-opt{
padding:12px 14px;border-radius:10px;
background:var(--bg-3);border:1px solid var(--border);
cursor:pointer;transition:border-color .15s,background .15s;
}
.tier-opt:hover{border-color:var(--border-2)}
.tier-opt.selected{border-color:var(--accent);background:rgba(124,92,255,.08)}
.tier-name{font-size:13px;font-weight:700}
.tier-price{font-size:11px;color:var(--text-faint);margin-top:2px}
.tier-limit{font-size:10px;color:var(--text-faint);margin-top:3px;font-style:italic}
.ch-toggle{display:flex;flex-direction:column;gap:8px}
.ch-row{
display:flex;align-items:center;gap:12px;
background:var(--bg-3);border:1px solid var(--border);
border-radius:10px;padding:12px 14px;
cursor:pointer;transition:border-color .15s;
}
.ch-row.selected{border-color:var(--accent);background:rgba(124,92,255,.06)}
.ch-emoji{font-size:18px;flex-shrink:0}
.ch-label{flex:1}
.ch-label-name{font-size:13px;font-weight:600}
.ch-label-desc{font-size:11px;color:var(--text-faint);margin-top:1px}
.ch-check{
width:18px;height:18px;border-radius:5px;
border:1px solid var(--border);background:var(--bg-2);
display:flex;align-items:center;justify-content:center;
font-size:10px;flex-shrink:0;
}
.ch-row.selected .ch-check{background:var(--accent);border-color:var(--accent);color:#fff}
.ch-extra{margin-top:10px;display:none}
.ch-extra.show{display:block}
/* NAV */
.nav{display:flex;gap:10px;margin-top:24px}
.btn{
padding:11px 22px;border-radius:10px;
font-size:14px;font-weight:700;cursor:pointer;
border:none;font-family:inherit;
transition:background .15s,opacity .15s;
}
.btn-primary{background:var(--accent);color:#fff;flex:1}
.btn-primary:hover{background:#6a4ce8}
.btn-primary:disabled{opacity:.45;cursor:not-allowed}
.btn-ghost{
background:var(--bg-3);color:var(--text-dim);
border:1px solid var(--border);
}
.btn-ghost:hover{background:var(--bg-2)}
/* SUCCESS */
.success-wrap{text-align:center;padding:12px 0}
.success-icon{font-size:52px;margin-bottom:16px}
.success-title{font-size:22px;font-weight:800;margin-bottom:8px}
.success-sub{font-size:14px;color:var(--text-dim);line-height:1.5}
.success-channels{display:flex;justify-content:center;gap:8px;margin-top:16px;flex-wrap:wrap}
.ch-pill{
padding:5px 12px;border-radius:99px;
background:rgba(62,207,142,.12);color:var(--green);
border:1px solid rgba(62,207,142,.25);font-size:12px;font-weight:600;
}
.dashboard-btn{
display:inline-block;margin-top:24px;
padding:12px 28px;border-radius:10px;
background:var(--accent);color:#fff;
font-size:14px;font-weight:700;cursor:pointer;
border:none;font-family:inherit;
transition:background .15s;
}
.dashboard-btn:hover{background:#6a4ce8}
/* ERROR */
.err-box{
background:rgba(255,92,124,.08);border:1px solid rgba(255,92,124,.2);
border-radius:8px;padding:10px 14px;
font-size:13px;color:var(--danger);
margin-top:14px;display:none;
}
.err-box.show{display:block}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:8px}
</style>
</head>
<body>
<div class="card">
<div class="card-head">
<div class="logo">
<div class="logo-icon">CG</div>
<div>
<div class="logo-name">CostGuard</div>
<div class="logo-sub">av Vauco AS</div>
</div>
</div>
<h1>Kom i gang på 2 minutter</h1>
<p class="subtitle">Koble CostGuard til ditt GCP-prosjekt og velg varslings-kanaler.</p>
</div>
<div class="steps">
<div class="step active" id="step-1"><div class="step-num">1</div><div>Bedrift</div></div>
<div class="step" id="step-2"><div class="step-num">2</div><div>Tier</div></div>
<div class="step" id="step-3"><div class="step-num">3</div><div>Varsler</div></div>
<div class="step" id="step-4"><div class="step-num">4</div><div>Ferdig</div></div>
</div>
<div class="card-body">
<!-- PANEL 1: Bedrift -->
<div class="panel active" id="panel-1">
<div class="field">
<label>Bedriftsnavn</label>
<input id="f-company" type="text" placeholder="Acme AS" autocomplete="organization">
</div>
<div class="field">
<label>Kontaktperson</label>
<input id="f-contact" type="text" placeholder="Ola Nordmann" autocomplete="name">
</div>
<div class="field">
<label>E-postadresse</label>
<input id="f-email" type="email" placeholder="ola@acme.no" autocomplete="email">
</div>
<div class="field">
<label>GCP Project ID</label>
<input id="f-project" type="text" placeholder="mitt-prosjekt-123456" autocomplete="off" spellcheck="false">
</div>
<div class="err-box" id="err-1"></div>
<div class="nav">
<button class="btn btn-primary" onclick="goTo(2)">Neste &rarr;</button>
</div>
</div>
<!-- PANEL 2: Tier -->
<div class="panel" id="panel-2">
<div class="tier-grid">
<div class="tier-opt selected" data-tier="starter" onclick="selectTier(this)">
<div class="tier-name">Starter</div>
<div class="tier-price">$499 / mnd</div>
<div class="tier-limit">GCP-spend $0$3k/mnd</div>
</div>
<div class="tier-opt" data-tier="guard" onclick="selectTier(this)">
<div class="tier-name">Guard</div>
<div class="tier-price">$999 / mnd</div>
<div class="tier-limit">GCP-spend $3k$15k/mnd</div>
</div>
<div class="tier-opt" data-tier="shield" onclick="selectTier(this)">
<div class="tier-name">Shield</div>
<div class="tier-price">$1.999 / mnd</div>
<div class="tier-limit">GCP-spend $15k$50k/mnd</div>
</div>
<div class="tier-opt" data-tier="enterprise" onclick="selectTier(this)">
<div class="tier-name">Enterprise</div>
<div class="tier-price">$3.500+ / mnd</div>
<div class="tier-limit">GCP-spend &gt;$50k/mnd</div>
</div>
</div>
<div class="nav">
<button class="btn btn-ghost" onclick="goTo(1)">&larr; Tilbake</button>
<button class="btn btn-primary" onclick="goTo(3)">Neste &rarr;</button>
</div>
</div>
<!-- PANEL 3: Kanaler -->
<div class="panel" id="panel-3">
<div class="ch-toggle">
<div class="ch-row selected" data-ch="webhook" onclick="toggleChannel(this)">
<div class="ch-emoji">🔗</div>
<div class="ch-label">
<div class="ch-label-name">Webhook <span style="font-size:10px;color:var(--text-faint);font-weight:400">(Slack/Teams/Chat)</span></div>
<div class="ch-label-desc">Øyeblikkelige varsler i din eksisterende kanal</div>
</div>
<div class="ch-check"></div>
</div>
<div class="ch-extra show" id="ex-webhook">
<div class="field" style="margin-bottom:0">
<label>Webhook URL</label>
<input id="f-webhook" type="url" placeholder="https://hooks.slack.com/services/...">
</div>
</div>
<div class="ch-row selected" data-ch="email" onclick="toggleChannel(this)">
<div class="ch-emoji">📧</div>
<div class="ch-label">
<div class="ch-label-name">E-post</div>
<div class="ch-label-desc">Daglig kostnadsoppsummering + spike-varsler</div>
</div>
<div class="ch-check"></div>
</div>
<div class="ch-extra show" id="ex-email">
<div class="field" style="margin-bottom:0">
<label>Varsel-e-post</label>
<input id="f-email-to" type="email" placeholder="alerts@acme.no">
</div>
</div>
<div class="ch-row" data-ch="sms" onclick="toggleChannel(this)" id="sms-row">
<div class="ch-emoji">📱</div>
<div class="ch-label">
<div class="ch-label-name">SMS <span style="font-size:10px;color:var(--amber);font-weight:600">Guard+</span></div>
<div class="ch-label-desc">Kritiske spike-varsler rett på mobil</div>
</div>
<div class="ch-check"></div>
</div>
</div>
<div class="err-box" id="err-3"></div>
<div class="nav">
<button class="btn btn-ghost" onclick="goTo(2)">&larr; Tilbake</button>
<button class="btn btn-primary" id="submit-btn" onclick="submit()">Aktiver CostGuard</button>
</div>
</div>
<!-- PANEL 4: Success -->
<div class="panel" id="panel-4">
<div class="success-wrap">
<div class="success-icon">🎉</div>
<div class="success-title">CostGuard er aktivert!</div>
<div class="success-sub">Jason overvåker nå <strong id="s-project"></strong>.<br>Varsler sendes via:</div>
<div class="success-channels" id="s-channels"></div>
<button class="dashboard-btn" onclick="window.location='/static/costguard-dashboard.html'">Gå til dashboard &rarr;</button>
</div>
</div>
</div>
</div>
<script>
let selectedTier = 'starter'
let selectedChannels = new Set(['webhook','email'])
function selectTier(el){
document.querySelectorAll('.tier-opt').forEach(e=>e.classList.remove('selected'))
el.classList.add('selected')
selectedTier = el.dataset.tier
// SMS kun Guard+
const smsRow = document.getElementById('sms-row')
if(['guard','shield','enterprise'].includes(selectedTier)){
smsRow.style.opacity='1';smsRow.style.pointerEvents='auto'
} else {
smsRow.style.opacity='.4';smsRow.style.pointerEvents='none'
smsRow.classList.remove('selected');selectedChannels.delete('sms')
}
}
function toggleChannel(el){
const ch = el.dataset.ch
const extra = document.getElementById('ex-'+ch)
if(el.classList.contains('selected')){
el.classList.remove('selected')
el.querySelector('.ch-check').textContent=''
selectedChannels.delete(ch)
if(extra){ extra.classList.remove('show') }
} else {
el.classList.add('selected')
el.querySelector('.ch-check').textContent='✓'
selectedChannels.add(ch)
if(extra){ extra.classList.add('show') }
}
}
function showErr(id, msg){
const el = document.getElementById(id)
el.textContent = msg; el.classList.add('show')
}
function clearErr(id){ document.getElementById(id).classList.remove('show') }
function goTo(n){
// Validate step 1
if(n===2){
clearErr('err-1')
const c = v('f-company'), ct = v('f-contact'), e = v('f-email'), p = v('f-project')
if(!c||!ct||!e||!p){ showErr('err-1','Fyll ut alle felt.'); return }
if(!e.includes('@')){ showErr('err-1','Ugyldig e-postadresse.'); return }
}
// Validate step 3
if(n===4 && !doSubmit) return
for(let i=1;i<=4;i++){
document.getElementById('panel-'+i).classList.toggle('active', i===n)
const s = document.getElementById('step-'+i)
s.classList.remove('active','done')
if(i===n) s.classList.add('active')
else if(i<n) s.classList.add('done')
}
}
function v(id){ return document.getElementById(id).value.trim() }
async function submit(){
clearErr('err-3')
const channels = [...selectedChannels]
if(!channels.length){ showErr('err-3','Velg minst én varslingskanal.'); return }
const webhookUrl = selectedChannels.has('webhook') ? v('f-webhook') : null
const emailTo = selectedChannels.has('email') ? v('f-email-to') : null
if(selectedChannels.has('webhook') && !webhookUrl){
showErr('err-3','Lim inn Webhook URL.'); return
}
if(selectedChannels.has('email') && !emailTo){
showErr('err-3','Fyll inn varsel-e-post.'); return
}
const btn = document.getElementById('submit-btn')
btn.disabled = true
btn.innerHTML = '<span class="spinner"></span>Aktiverer...'
const body = {
company: v('f-company'),
contact: v('f-contact'),
email: v('f-email'),
gcp_project: v('f-project'),
tier: selectedTier,
channels,
webhook_url: webhookUrl,
email_to: emailTo,
}
try{
const r = await fetch('/onboard/complete', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
})
const data = await r.json()
if(!r.ok){
showErr('err-3', data.detail || 'Feil fra server (' + r.status + ').')
btn.disabled=false; btn.textContent='Aktiver CostGuard'
return
}
// Success
document.getElementById('s-project').textContent = body.gcp_project
const pillHtml = channels.map(ch => {
const icons = {webhook:'🔗',email:'📧',sms:'📱'}
return `<div class="ch-pill">${icons[ch]||''} ${ch}</div>`
}).join('')
document.getElementById('s-channels').innerHTML = pillHtml
goTo(4)
} catch(e){
showErr('err-3', 'Nettverksfeil: ' + e.message)
btn.disabled=false; btn.textContent='Aktiver CostGuard'
}
}
</script>
</body>
</html>