OSVauco/static/onboard.html

523 lines
22 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;--danger:#ff5c7c;--amber:#ffb547;
--radius:12px;
--sans:-apple-system,BlinkMacSystemFont,'Inter',system-ui,sans-serif;
--mono:'SF Mono',ui-monospace,'JetBrains Mono',Menlo,monospace;
}
*{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}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
input,select,textarea{font-family:inherit}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
/* TOP */
.topbar{
display:flex;align-items:center;gap:14px;
padding:14px 32px;border-bottom:1px solid var(--border);
background:rgba(10,10,15,.95);backdrop-filter:blur(16px);
position:sticky;top:0;z-index:10;
}
.logo{
width:32px;height:32px;border-radius:10px;
background:linear-gradient(135deg,var(--accent),var(--accent-2));
display:flex;align-items:center;justify-content:center;
font-weight:900;font-size:13px;color:#050509;
}
.brand{font-size:15px;font-weight:800;letter-spacing:-.02em}
.brand-sub{font-size:11px;color:var(--text-faint)}
/* LAYOUT */
.page{max-width:680px;margin:0 auto;padding:48px 24px 80px;}
/* STEP INDICATOR */
.step-bar{
display:flex;align-items:center;gap:0;margin-bottom:48px;
}
.step-item{
display:flex;flex-direction:column;align-items:center;flex:1;
position:relative;
}
.step-item:not(:last-child)::after{
content:'';position:absolute;top:15px;left:50%;right:-50%;
height:1px;background:var(--border-2);
z-index:0;
}
.step-item.done::after{background:var(--accent)}
.step-num{
width:30px;height:30px;border-radius:50%;
border:1px solid var(--border-2);background:var(--bg-2);
display:flex;align-items:center;justify-content:center;
font-size:12px;font-weight:700;color:var(--text-faint);
position:relative;z-index:1;
transition:background .2s,border-color .2s,color .2s;
}
.step-item.active .step-num{
background:var(--accent);border-color:var(--accent);color:#fff;
box-shadow:0 0 0 4px rgba(124,92,255,.2);
}
.step-item.done .step-num{
background:var(--green);border-color:var(--green);color:#050509;
}
.step-label{
font-size:10px;color:var(--text-faint);margin-top:6px;
font-weight:600;text-transform:uppercase;letter-spacing:.05em;
white-space:nowrap;
}
.step-item.active .step-label,.step-item.done .step-label{color:var(--text-dim)}
/* STEP PANELS */
.step-panel{display:none;animation:fadeIn .22s ease}
.step-panel.visible{display:block}
.step-title{font-size:24px;font-weight:800;letter-spacing:-.02em;margin-bottom:6px}
.step-sub{font-size:14px;color:var(--text-dim);margin-bottom:32px;line-height:1.6}
/* FORM */
.field{display:flex;flex-direction:column;gap:6px;margin-bottom:20px}
.label{font-size:12px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
.label span{font-weight:400;color:var(--text-faint);text-transform:none;letter-spacing:0}
.input{
padding:11px 14px;border-radius:10px;
background:var(--bg-2);border:1px solid var(--border-2);
color:var(--text);font-size:14px;
outline:none;transition:border-color .15s,box-shadow .15s;
width:100%;
}
.input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,92,255,.15)}
.input.err{border-color:var(--danger);box-shadow:0 0 0 3px rgba(255,92,124,.12)}
.field-err{font-size:11px;color:var(--danger);margin-top:-4px}
/* CHANNEL CARDS */
.ch-grid{display:flex;flex-direction:column;gap:10px;margin-bottom:28px}
.ch-card{
display:flex;align-items:flex-start;gap:14px;
padding:16px 18px;border-radius:var(--radius);
background:var(--bg-2);border:1px solid var(--border);
cursor:pointer;transition:border-color .15s,background .15s;
}
.ch-card.selected{
border-color:var(--accent);background:rgba(124,92,255,.07);
}
.ch-card .ch-ico{font-size:22px;flex-shrink:0;margin-top:1px}
.ch-card .ch-info{flex:1}
.ch-card .ch-name{font-size:14px;font-weight:700}
.ch-card .ch-desc{font-size:12px;color:var(--text-dim);margin-top:3px}
.ch-card .ch-tier{
font-size:10px;font-weight:700;padding:2px 7px;border-radius:99px;
background:var(--bg-3);color:var(--text-faint);border:1px solid var(--border);
margin-top:6px;display:inline-block;
}
.ch-check{
width:20px;height:20px;border-radius:50%;
border:2px solid var(--border-2);flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:11px;margin-top:2px;
transition:background .15s,border-color .15s;
}
.ch-card.selected .ch-check{
background:var(--accent);border-color:var(--accent);color:#fff;
}
/* CHANNEL DETAIL FIELDS */
.ch-detail{display:none;margin-top:12px;padding-top:12px;border-top:1px solid var(--border)}
.ch-card.selected .ch-detail{display:block}
/* BUTTONS */
.btn-row{display:flex;gap:12px;align-items:center;margin-top:32px}
.btn-primary{
padding:11px 24px;border-radius:99px;
background:linear-gradient(135deg,var(--accent),var(--accent-2));
color:#050509;font-weight:700;font-size:14px;
display:inline-flex;align-items:center;gap:8px;
box-shadow:0 8px 24px rgba(0,0,0,.4);
transition:opacity .15s;
}
.btn-primary:disabled{opacity:.4;cursor:not-allowed}
.btn-secondary{
padding:10px 20px;border-radius:99px;
border:1px solid var(--border-2);background:var(--bg-2);
color:var(--text-dim);font-size:14px;font-weight:600;
transition:background .14s,color .14s;
}
.btn-secondary:hover{background:var(--bg-3);color:var(--text)}
.spinner{
width:14px;height:14px;border-radius:50%;
border:2px solid rgba(0,0,0,.2);border-top-color:#050509;
animation:spin .6s linear infinite;
}
/* TEST RESULT */
.test-result{
margin-top:14px;padding:12px 16px;border-radius:10px;
font-size:13px;font-weight:500;
display:none;
}
.test-result.ok{background:rgba(62,207,142,.1);border:1px solid rgba(62,207,142,.25);color:var(--green)}
.test-result.err{background:rgba(255,92,124,.1);border:1px solid rgba(255,92,124,.25);color:var(--danger)}
.test-result.show{display:block}
/* SUMMARY / DONE */
.summary-card{
background:var(--bg-2);border:1px solid var(--border);
border-radius:var(--radius);padding:24px;
margin-bottom:24px;
}
.summary-row{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);font-size:13px}
.summary-row:last-child{border-bottom:none}
.summary-key{color:var(--text-dim)}
.summary-val{font-weight:600;text-align:right;max-width:60%;word-break:break-all}
.done-check{
width:72px;height:72px;border-radius:50%;
background:rgba(62,207,142,.12);border:2px solid rgba(62,207,142,.3);
display:flex;align-items:center;justify-content:center;
font-size:34px;margin:0 auto 20px;
}
.done-title{text-align:center;font-size:22px;font-weight:800;margin-bottom:8px}
.done-sub{text-align:center;font-size:14px;color:var(--text-dim);margin-bottom:28px;line-height:1.6}
/* TOAST */
#toast{
position:fixed;bottom:24px;right:24px;
background:var(--bg-2);border:1px solid var(--border-2);
border-radius:10px;padding:12px 18px;font-size:13px;
box-shadow:0 8px 32px rgba(0,0,0,.5);
opacity:0;transform:translateY(8px);
transition:opacity .2s,transform .2s;pointer-events:none;z-index:100;
}
#toast.show{opacity:1;transform:none}
</style>
</head>
<body>
<div class="topbar">
<div class="logo">CG</div>
<div>
<div class="brand">CostGuard</div>
<div class="brand-sub">Kom i gang</div>
</div>
</div>
<main class="page">
<!-- STEP INDICATOR -->
<div class="step-bar" id="step-bar">
<div class="step-item active" data-step="0"><div class="step-num">1</div><div class="step-label">Bedrift</div></div>
<div class="step-item" data-step="1"><div class="step-num">2</div><div class="step-label">Varslingskanal</div></div>
<div class="step-item" data-step="2"><div class="step-num">3</div><div class="step-label">Test & bekreft</div></div>
<div class="step-item" data-step="3"><div class="step-num">4</div><div class="step-label">Ferdig</div></div>
</div>
<!-- STEP 1: BEDRIFT -->
<div class="step-panel visible" id="panel-0">
<div class="step-title">Hvem er du?</div>
<div class="step-sub">Grunnleggende info om bedriften din og GCP-prosjektet CostGuard skal overvåke.</div>
<div class="field">
<div class="label">Bedriftsnavn</div>
<input class="input" id="company" type="text" placeholder="Vauco AS" autocomplete="organization">
</div>
<div class="field">
<div class="label">Kontaktperson <span>(ditt navn)</span></div>
<input class="input" id="contact" type="text" placeholder="Chris Christiansen" autocomplete="name">
</div>
<div class="field">
<div class="label">E-postadresse</div>
<input class="input" id="email" type="email" placeholder="chris@bedrift.no" autocomplete="email">
</div>
<div class="field">
<div class="label">GCP-prosjekt ID</div>
<input class="input" id="gcp_project" type="text" placeholder="mitt-gcp-prosjekt-123" spellcheck="false">
</div>
<div class="field">
<div class="label">Estimert månedlig GCP-spend <span>(USD)</span></div>
<select class="input" id="gcp_spend">
<option value="">Velg range...</option>
<option value="starter">$0 $3 000 (Starter)</option>
<option value="guard">$3 000 $15 000 (Guard)</option>
<option value="shield">$15 000 $50 000 (Shield)</option>
<option value="enterprise">Over $50 000 (Enterprise)</option>
</select>
</div>
<div class="btn-row">
<button class="btn-primary" onclick="nextStep(0)">Neste →</button>
</div>
</div>
<!-- STEP 2: VARSLINGSKANAL -->
<div class="step-panel" id="panel-1">
<div class="step-title">Hvor vil du at Jason skal nå deg?</div>
<div class="step-sub">Velg én eller flere kanaler. CostGuard leverer direkte dit du allerede er.</div>
<div class="ch-grid">
<div class="ch-card" id="ch-webhook" onclick="toggleChannel('webhook')">
<div class="ch-ico">🔗</div>
<div class="ch-info">
<div class="ch-name">Webhook <span style="font-size:11px;font-weight:400;color:var(--text-faint)">(Slack · Teams · Google Chat · Discord)</span></div>
<div class="ch-desc">Jason poster meldinger direkte i din eksisterende kanal.</div>
<div class="ch-tier">Starter+</div>
<div class="ch-detail">
<div class="field" style="margin-bottom:0">
<div class="label">Webhook URL</div>
<input class="input" id="webhook_url" type="url" placeholder="https://hooks.slack.com/services/...">
</div>
</div>
</div>
<div class="ch-check" id="check-webhook"></div>
</div>
<div class="ch-card" id="ch-email" onclick="toggleChannel('email')">
<div class="ch-ico">📧</div>
<div class="ch-info">
<div class="ch-name">E-post</div>
<div class="ch-desc">Daglig digest + varsler ved spikes. Sendes via SendGrid.</div>
<div class="ch-tier">Starter+</div>
<div class="ch-detail">
<div class="field" style="margin-bottom:0">
<div class="label">Mottaker-e-post <span>(forhåndsutfylt fra forrige steg)</span></div>
<input class="input" id="email_to" type="email" placeholder="alert@bedrift.no">
</div>
</div>
</div>
<div class="ch-check" id="check-email"></div>
</div>
<div class="ch-card" style="opacity:.55;cursor:not-allowed" title="Krever Guard-tier">
<div class="ch-ico">📱</div>
<div class="ch-info">
<div class="ch-name">SMS <span style="font-size:11px;font-weight:400;color:var(--text-faint)">(kun kritiske spikes)</span></div>
<div class="ch-desc">Twilio-integrasjon — kommer i Guard-tier.</div>
<div class="ch-tier" style="color:var(--amber);border-color:rgba(255,181,71,.25)">Guard+ · Coming soon</div>
</div>
<div class="ch-check"></div>
</div>
</div>
<div class="btn-row">
<button class="btn-secondary" onclick="prevStep(1)">← Tilbake</button>
<button class="btn-primary" onclick="nextStep(1)">Neste →</button>
</div>
</div>
<!-- STEP 3: TEST & BEKREFT -->
<div class="step-panel" id="panel-2">
<div class="step-title">Test varslingene</div>
<div class="step-sub">Send en testmelding for å bekrefte at alt er koblet opp riktig — før vi aktiverer CostGuard.</div>
<div id="test-webhook-wrap" style="display:none;margin-bottom:20px">
<div class="summary-card" style="margin-bottom:0">
<div class="summary-row"><span class="summary-key">🔗 Webhook</span><span class="summary-val" id="test-webhook-url-disp"></span></div>
</div>
<button class="btn-secondary" style="margin-top:12px" onclick="testChannel('webhook')" id="test-wh-btn">Send testmelding til webhook</button>
<div class="test-result" id="test-webhook-result"></div>
</div>
<div id="test-email-wrap" style="display:none;margin-bottom:20px">
<div class="summary-card" style="margin-bottom:0">
<div class="summary-row"><span class="summary-key">📧 E-post</span><span class="summary-val" id="test-email-to-disp"></span></div>
</div>
<button class="btn-secondary" style="margin-top:12px" onclick="testChannel('email')" id="test-em-btn">Send test-e-post</button>
<div class="test-result" id="test-email-result"></div>
</div>
<p style="font-size:12px;color:var(--text-faint);margin-top:8px">Du kan hoppe over testingen og aktivere direkte.</p>
<div class="btn-row">
<button class="btn-secondary" onclick="prevStep(2)">← Tilbake</button>
<button class="btn-primary" id="activate-btn" onclick="activate()">Aktiver CostGuard ✓</button>
</div>
</div>
<!-- STEP 4: FERDIG -->
<div class="step-panel" id="panel-3">
<div class="done-check"></div>
<div class="done-title">CostGuard er aktivert!</div>
<div class="done-sub">Jason overvåker nå <span id="done-project" style="color:var(--accent);font-weight:700"></span>.<br>Du mottar første rapport innen 24 timer.</div>
<div class="summary-card">
<div class="summary-row"><span class="summary-key">Bedrift</span><span class="summary-val" id="done-company"></span></div>
<div class="summary-row"><span class="summary-key">Kontakt</span><span class="summary-val" id="done-contact"></span></div>
<div class="summary-row"><span class="summary-key">GCP-prosjekt</span><span class="summary-val" id="done-gcp"></span></div>
<div class="summary-row"><span class="summary-key">Tier</span><span class="summary-val" id="done-tier"></span></div>
<div class="summary-row"><span class="summary-key">Kanaler</span><span class="summary-val" id="done-channels"></span></div>
</div>
<div class="btn-row" style="justify-content:center">
<a href="/static/costguard-dashboard.html" class="btn-primary" style="text-decoration:none">Gå til dashboard →</a>
</div>
</div>
</main>
<div id="toast"></div>
<script>
const TIERS = {starter:'Starter ($499/mnd)',guard:'Guard ($999/mnd)',shield:'Shield ($1.999/mnd)',enterprise:'Enterprise ($3.500+/mnd)'}
let selectedChannels = new Set()
let state = {}
function toast(msg, dur=3200){
const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show')
setTimeout(()=>t.classList.remove('show'),dur)
}
function setStep(to){
document.querySelectorAll('.step-panel').forEach(p=>p.classList.remove('visible'))
document.getElementById('panel-'+to).classList.add('visible')
document.querySelectorAll('.step-item').forEach((el,i)=>{
el.classList.toggle('active', i===to)
el.classList.toggle('done', i<to)
})
window.scrollTo({top:0,behavior:'smooth'})
}
function prevStep(from){setStep(from-1)}
function nextStep(from){
if(from===0){
const company=document.getElementById('company').value.trim()
const contact=document.getElementById('contact').value.trim()
const email=document.getElementById('email').value.trim()
const gcp=document.getElementById('gcp_project').value.trim()
const spend=document.getElementById('gcp_spend').value
const errors=[]
if(!company) errors.push('company')
if(!contact) errors.push('contact')
if(!email||!/^[^@]+@[^@]+\.[^@]+$/.test(email)) errors.push('email')
if(!gcp) errors.push('gcp_project')
if(!spend) errors.push('gcp_spend')
document.querySelectorAll('#panel-0 .input').forEach(el=>el.classList.remove('err'))
errors.forEach(id=>document.getElementById(id)?.classList.add('err'))
if(errors.length){toast('⚠️ Fyll ut alle feltene');return}
state={company,contact,email,gcp_project:gcp,tier:spend}
// Pre-fill email channel
document.getElementById('email_to').value=email
setStep(1)
} else if(from===1){
if(selectedChannels.size===0){toast('⚠️ Velg minst én varslingskanal');return}
if(selectedChannels.has('webhook')&&!document.getElementById('webhook_url').value.trim()){
document.getElementById('webhook_url').classList.add('err')
toast('⚠️ Skriv inn webhook URL');return
}
if(selectedChannels.has('email')&&!document.getElementById('email_to').value.trim()){
document.getElementById('email_to').classList.add('err')
toast('⚠️ Skriv inn e-postadresse');return
}
state.webhook_url = selectedChannels.has('webhook')?document.getElementById('webhook_url').value.trim():null
state.email_to = selectedChannels.has('email')?document.getElementById('email_to').value.trim():null
// Populate test step
const wWrap=document.getElementById('test-webhook-wrap')
const eWrap=document.getElementById('test-email-wrap')
if(state.webhook_url){
wWrap.style.display='block'
document.getElementById('test-webhook-url-disp').textContent=state.webhook_url
} else { wWrap.style.display='none' }
if(state.email_to){
eWrap.style.display='block'
document.getElementById('test-email-to-disp').textContent=state.email_to
} else { eWrap.style.display='none' }
setStep(2)
}
}
function toggleChannel(key){
const card=document.getElementById('ch-'+key)
if(selectedChannels.has(key)){selectedChannels.delete(key);card.classList.remove('selected')}
else{selectedChannels.add(key);card.classList.add('selected')}
}
async function testChannel(type){
const resultEl=document.getElementById('test-'+type+'-result')
const btn=document.getElementById('test-'+(type==='webhook'?'wh':'em')+'-btn')
btn.disabled=true;btn.textContent='Sender...'
resultEl.className='test-result';resultEl.textContent=''
try{
let body, url
if(type==='webhook'){
url='/notify/webhook'
body=JSON.stringify({
url: state.webhook_url,
event: 'custom',
title: 'CostGuard — Testmelding',
body: `Hei fra CostGuard! Webhook-integrasjon for ${state.company} (${state.gcp_project}) er koblet opp og fungerer.`,
payload: {bedrift:state.company, tier:TIERS[state.tier]||state.tier, prosjekt:state.gcp_project}
})
} else {
url='/notify/email'
body=JSON.stringify({
to: state.email_to,
event: 'custom',
subject: `✅ CostGuard er koblet opp — ${state.company}`,
body_html: `<div style='font-family:sans-serif;max-width:600px;margin:0 auto;padding:32px'><h2>CostGuard er koblet opp ✅</h2><p>Hei ${state.contact},</p><p>CostGuard overvåker nå <strong>${state.gcp_project}</strong>. Tier: <strong>${TIERS[state.tier]||state.tier}</strong>.</p><p>Du mottar første rapport innen 24 timer.</p><hr><p style='font-size:12px;color:#999'>CostGuard by Vauco AS</p></div>`
})
}
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body})
const data=await r.json()
if(r.ok&&data.status==='ok'){
resultEl.className='test-result ok show'
resultEl.textContent='✅ Testmelding sendt!'
} else {
resultEl.className='test-result err show'
resultEl.textContent='❌ Feil: '+(data.detail||data.error||r.status)
}
}catch(e){
resultEl.className='test-result err show'
resultEl.textContent='❌ Nettverksfeil: '+e.message
}finally{
btn.disabled=false
btn.textContent=type==='webhook'?'Send testmelding til webhook':'Send test-e-post'
}
}
async function activate(){
const btn=document.getElementById('activate-btn')
btn.disabled=true
btn.innerHTML='<div class="spinner"></div> Aktiverer...'
try{
const payload={
company: state.company,
contact: state.contact,
email: state.email,
gcp_project:state.gcp_project,
tier: state.tier,
channels: [...selectedChannels],
webhook_url:state.webhook_url||null,
email_to: state.email_to||null,
}
const r=await fetch('/onboard/complete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
const data=await r.json()
if(!r.ok) throw new Error(data.detail||data.error||r.status)
// Populate done screen
document.getElementById('done-project').textContent=state.gcp_project
document.getElementById('done-company').textContent=state.company
document.getElementById('done-contact').textContent=state.contact+' ('+state.email+')'
document.getElementById('done-gcp').textContent=state.gcp_project
document.getElementById('done-tier').textContent=TIERS[state.tier]||state.tier
document.getElementById('done-channels').textContent=[...selectedChannels].join(' + ')||'—'
setStep(3)
}catch(e){
toast('❌ Aktivering feilet: '+e.message)
btn.disabled=false
btn.textContent='Aktiver CostGuard ✓'
}
}
</script>
</body>
</html>