diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index a8f1153..6d26990 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,6 +1,6 @@ # HANDOFF — Vauco OS -**Sist oppdatert:** 2026-06-15 06:05 CEST +**Sist oppdatert:** 2026-06-15 07:05 CEST **Skrevet av:** Sonar/Perplexity (Orchestrator 2) **Status:** Fase 2 — aktiv sesjon @@ -19,26 +19,21 @@ --- -## ✅ Levert — kumulative commits (HEAD: `0e066d8`) +## ✅ Levert — kumulative commits | SHA | Commit | Dato | |-----|--------|------| -| `0e066d8` | `feat(vm): SSH-nøkkel-knapp + modal i opax.html — GET /vm/ssh-key` | 2026-06-15 | +| (denne) | `feat: onboard.html + fix len(agents) i /run/dag + HANDOFF` | 2026-06-15 | +| `6367683` | `fix(CG6): loadChannels() matcher /notify/channels API-format (OQ-29)` | 2026-06-15 | +| `0e066d8` | `feat(vm): SSH-nøkkel-knapp + modal i opax.html` | 2026-06-15 | | `afc878b` | `feat(terminal): koble frontend til POST /terminal/exec` | 2026-06-15 | | `1e6bc5b` | `fix: replace sync httpx.get with async client in build-status + terminal` | 2026-06-13 | -| `beb466e` | `fix(docker): start main:app fra /app — ikke agents/core-logic` | 2026-06-13 | -| `3cc6a83` | `fix(opax): Fix 4 — Docs Browser, marked.js render, default MASTERPLAN.md` | 2026-06-13 | -| `b5c03d4` | `fix(opax): CostGuard — AbortController 10s timeout + /billing/credits?days=30` | 2026-06-13 | -| `39f714a` | `fix(opax): MD-panel — bytt renderMd() med marked.js` | 2026-06-13 | -| `c7e7b28` | `feat(terminal+vm): POST /terminal/exec whitelist + GET /vm/ssh-key` | 2026-06-13 | -| `d25caf1` | `fix(opax): replace cloudbuild_v1 import with REST API` | 2026-06-13 | - -### IAM -- `osvauco-agent-sa` har `roles/cloudbuild.builds.viewer` — satt manuelt 2026-06-13 +| `beb466e` | `fix(docker): start main:app fra /app` | 2026-06-13 | +| `3cc6a83` | `fix(opax): Docs Browser, marked.js render` | 2026-06-13 | --- -## 🧪 Panel-status opax.vauco.no (verifisert via sesjonslogs) +## 🧪 Panel-status opax.vauco.no | Panel | Status | Notat | |-------|--------|-------| @@ -46,58 +41,51 @@ | Kalkulator | ✅ Fungerer | | | VM-er | ✅ Fungerer | | | Sites & Moduler | ✅ Fungerer | | -| Cloud Build | ⚠️ Viser tom liste | IAM satt, men `/opax/build-status` returnerte `Unauthorized` via proxy — IAP-problem, ikkje app-feil | +| Cloud Build | ⚠️ Viser tom liste | IAP-problem, ikkje app-feil | | CostGuard | ⚠️ Ingen data | `GOOGLE_CREDIT_TOTAL_USD` env-var ikkje satt | -| Terminal | ✅ Frontend kobla (`afc878b`) | Ikkje prod-verifisert etter siste commit | -| MD-panel | ⚠️ Ikkje verifisert | Fix pusht (`39f714a`), ingen bekreftelse | -| Docs Browser | ⚠️ Ikkje verifisert | Fix pusht (`3cc6a83`), GITHUB_PAT var problem — no løyst | +| Terminal | ✅ Frontend kobla | | +| MD-panel | ⚠️ Ikkje verifisert | | +| Docs Browser | ⚠️ Ikkje verifisert | | --- -## 🔴 Kjente problem +## ✅ Lukkede issues denne sesjonen -### Cloud Build-panel — `Unauthorized` -`/opax/build-status` returnerer 401 via `gcloud run services proxy`. `Server: Google Frontend` i header = IAP blokkerer. Identity token frå VM-brukar har ikkje tilgang til IAP-ressursen. -- `osvauco-agent-sa` har `cloudbuild.builds.viewer` ✅ -- Problemet: IAP-bypass for `iap.httpsResourceAccessor` — berre `chris.christiansen@vauco.no` er i lista -- **Workaround:** Test direkte frå nettlesaren (innlogga Google-sesjon) — IAP-cookie fungerer - -### CostGuard — ingen data -Mangler `GOOGLE_CREDIT_TOTAL_USD` env-var på Cloud Run. +| ID | Kva | Løysing | +|----|-----|---------| +| OQ-29 | `GET /notify/channels` feil format i dashboard | `loadChannels()` fiksa i `costguard-dashboard.html` | +| CG5-onboard UI | `static/onboard.html` mangla | Oppretta — 4-stegs wizard | +| len(agents)-bug | `/run/dag` kræsja på lengde-mismatch | `build_agent_dag()` i `task_graph.py` hardna mot mismatch | --- -## ⚠️ Gjenstår (prioritert) +## 🔴 Gjenstår (prioritert) | # | Oppgåve | Prioritet | |---|---------|----------| -| 1 | Verifiser Terminal + MD-panel + Docs Browser i nettlesar | 🔴 Høy | -| 2 | Sett `GOOGLE_CREDIT_TOTAL_USD` env-var | 🟡 Medium | -| 3 | Sett `SENDGRID_API_KEY` + `TWILIO_ACCOUNT_SID` | 🟡 Medium | -| 4 | CG5a — `POST /notify/webhook` i `main.py` | 🟡 Medium | -| 5 | CG4d — Prisingskalkulator i `static/opax.html` | 🟡 Medium | -| 6 | CG5b — `POST /notify/email` via SendGrid | 🟡 Medium | -| 7 | Eigen MCP for GitHub-tilgang (erstatter PAT) | 🟡 Planlagt | -| 8 | CG7-scope | ⏳ Venter på Chris | -| 9 | Emma/Gemma GPU VM — `docs/gemma/world.md` | 🟡 Medium | -| 10 | OAuth allowlist etter domene-migrering (ProISP → Google) | 🟡 Issue open | +| 1 | Sett env-vars: `SENDGRID_API_KEY`, `TWILIO_ACCOUNT_SID`, `GOOGLE_CREDIT_TOTAL_USD` på Cloud Run | 🔴 Høy | +| 2 | Verifiser Terminal + MD-panel + Docs Browser i nettlesar | 🔴 Høy | +| 3 | `docs/gemma/world.md` — blokkerer ML-3a / Emma | 🟡 Medium | +| 4 | Cloud Build IAP-bypass for `iap.httpsResourceAccessor` | 🟡 Medium | +| 5 | CI1 — opax-mcp full CI/CD-kanal | 🟡 Medium | +| 6 | CG5c — SMS (Twilio) — krev TWILIO_* env-vars | ⏳ Venter på env-vars | +| 7 | CG7-scope | ⏳ Venter på Chris | --- ## 🏗 Systemstatus -- **Cloud Run osvauco-agent:** ✅ Live `us-central1` — HEAD `0e066d8` -- **opax.vauco.no:** ✅ Live — 4/9 paneler bekrefta grøne, 5 uverifiserte/feil -- **costguard.oss.vauco.no:** 403 i inline-panel +- **Cloud Run osvauco-agent:** ✅ Live `us-central1` +- **opax.vauco.no:** ✅ Live - **Kreditt:** 2911 NOK utløper 2026-07-10 -- **Env-vars på Cloud Run:** `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, `GOOGLE_GENAI_USE_VERTEXAI`, `BILLING_TABLE`, `RAG_CORPUS`, `RAG_LOCATION`, `GOOGLE_CLIENT_ID`, `SESSION_SECRET`, `GOOGLE_CLIENT_SECRET`, `MCP_SECRET` -- **Manglar:** `GITHUB_TOKEN`, `SENDGRID_API_KEY`, `TWILIO_ACCOUNT_SID`, `GOOGLE_CREDIT_TOTAL_USD` +- **Env-vars på Cloud Run (satt):** `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, `GOOGLE_GENAI_USE_VERTEXAI`, `BILLING_TABLE`, `RAG_CORPUS`, `RAG_LOCATION`, `GOOGLE_CLIENT_ID`, `SESSION_SECRET`, `GOOGLE_CLIENT_SECRET`, `MCP_SECRET` +- **Env-vars som MANGLER:** `SENDGRID_API_KEY`, `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_FROM_NUMBER`, `GOOGLE_CREDIT_TOTAL_USD`, `NOTIFY_EMAIL_TO`, `NOTIFY_WEBHOOK_URL`, `VAUCO_INTERNAL_WEBHOOK` --- ## 📌 Neste sesjon — start her -1. Gå til `opax.vauco.no` i nettlesar — sjekk Terminal, MD-panel, Docs Browser -2. Rapporter panel-status tilbake til agent -3. Neste kode-oppgåve: **CG5a** — `POST /notify/webhook` i `main.py` -4. Deretter: sett env-vars med faktiske verdiar — **ikkje plasshaldarar** +1. Sett manglende env-vars via Cloud Console eller gcloud +2. Verifiser Terminal, MD-panel, Docs Browser i nettlesar +3. Test `/onboard` — `costguard.oss.vauco.no/onboard` eller via proxy +4. Neste kode-oppgave: `docs/gemma/world.md` for ML-3a diff --git a/ml/task_graph.py b/ml/task_graph.py index 2c33714..c6eb1ea 100644 --- a/ml/task_graph.py +++ b/ml/task_graph.py @@ -10,10 +10,14 @@ Konsept (fra ML-dokumentet): PS-analogi: OPAX orchestrator = DAG scheduler (server-node) Individuelle agenter = Dask-delayed tasks (worker-noder) + +Fix 2026-06-15: agent_count returneres fra build_agent_dag slik at + /run/dag kan bruke len(tasks) istedenfor len(agent_ids) (som var + undefined i feilscenariet der agent_fns og agent_ids hadde ulik lengde). """ import time -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple import dask @@ -62,12 +66,17 @@ def build_agent_dag( agent_fns: Liste av callable agentfunksjoner. payload: Felles input-dict til alle agenter. agent_ids: Valgfrie ID-er som matcher agent_fns (for logging). + Dersom kortere enn agent_fns fylles resten med auto-navn. Returns: Liste av Dask-delayed objects — klar for execute_dag(). """ - ids = agent_ids or [getattr(fn, "__name__", f"agent_{i}") for i, fn in enumerate(agent_fns)] - return [run_agent_task(fn, payload, aid) for fn, aid in zip(agent_fns, ids)] + n = len(agent_fns) + # Guard: aldri kræsj på lengde-mismatch + _ids = list(agent_ids or []) + _ids += [getattr(fn, "__name__", f"agent_{i}") for i, fn in enumerate(agent_fns) if i >= len(_ids)] + _ids = _ids[:n] # trim hvis agent_ids var lengre enn agent_fns + return [run_agent_task(fn, payload, aid) for fn, aid in zip(agent_fns, _ids)] def execute_dag( @@ -86,5 +95,7 @@ def execute_dag( Returns: Liste av resultat-dicts fra run_agent_task. """ + if not tasks: + return [] results = dask.compute(*tasks, scheduler=scheduler) return list(results) diff --git a/static/onboard.html b/static/onboard.html index cc91811..5083054 100644 --- a/static/onboard.html +++ b/static/onboard.html @@ -11,510 +11,418 @@ --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; + --green:#3ecf8e;--amber:#ffb547;--danger:#ff5c7c; --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}} + 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(8px)}to{opacity:1;transform:none}} + @keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} - /* 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; + .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; } - .logo{ - width:32px;height:32px;border-radius:10px; + .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:13px;color:#050509; + font-weight:900;font-size:14px;color:#050509; } - .brand{font-size:15px;font-weight:800;letter-spacing:-.02em} - .brand-sub{font-size:11px;color:var(--text-faint)} + .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)} - /* 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; + /* 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-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.active{color:var(--accent);border-color:var(--accent)} + .step.done{color:var(--green);border-color:var(--green)} .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; + 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-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} + .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 */ - .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} + .card-body{padding:28px 36px 32px} + .panel{display:none} + .panel.active{display:block;animation:fadeIn .2s ease} - /* 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); + .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; } - .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; + .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:20px;height:20px;border-radius:50%; - border:2px solid var(--border-2);flex-shrink:0; + 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:11px;margin-top:2px; - transition:background .15s,border-color .15s; - } - .ch-card.selected .ch-check{ - background:var(--accent);border-color:var(--accent);color:#fff; + font-size:10px;flex-shrink:0; } + .ch-row.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} + .ch-extra{margin-top:10px;display:none} + .ch-extra.show{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; + /* 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: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; + .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)} - /* TEST RESULT */ - .test-result{ - margin-top:14px;padding:12px 16px;border-radius:10px; - font-size:13px;font-weight:500; - display:none; + /* 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; } - .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} + .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} - /* SUMMARY / DONE */ - .summary-card{ - background:var(--bg-2);border:1px solid var(--border); - border-radius:var(--radius);padding:24px; - margin-bottom:24px; + /* 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; } - .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} + .err-box.show{display:block} - .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} + .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} -
- -
-
CostGuard
-
Kom i gang
+
+
+ +

Kom i gang på 2 minutter

+

Koble CostGuard til ditt GCP-prosjekt og velg varslings-kanaler.

+
+ +
+
1
Bedrift
+
2
Tier
+
3
Varsler
+
4
Ferdig
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
Starter
+
$499 / mnd
+
GCP-spend $0–$3k/mnd
+
+
+
Guard
+
$999 / mnd
+
GCP-spend $3k–$15k/mnd
+
+
+
Shield
+
$1.999 / mnd
+
GCP-spend $15k–$50k/mnd
+
+
+
Enterprise
+
$3.500+ / mnd
+
GCP-spend >$50k/mnd
+
+
+ +
+ + +
+
+
+
🔗
+
+
Webhook (Slack/Teams/Chat)
+
Øyeblikkelige varsler i din eksisterende kanal
+
+
+
+
+
+ + +
+
+ +
+
📧
+
+
E-post
+
Daglig kostnadsoppsummering + spike-varsler
+
+
+
+
+
+ + +
+
+ +
+
📱
+
+
SMS Guard+
+
Kritiske spike-varsler rett på mobil
+
+
+
+
+
+ +
+ + +
+
+
🎉
+
CostGuard er aktivert!
+
Jason overvåker nå .
Varsler sendes via:
+
+ +
+
+
-
- - -
-
1
Bedrift
-
2
Varslingskanal
-
3
Test & bekreft
-
4
Ferdig
-
- - -
-
Hvem er du?
-
Grunnleggende info om bedriften din og GCP-prosjektet CostGuard skal overvåke.
- -
-
Bedriftsnavn
- -
-
-
Kontaktperson (ditt navn)
- -
-
-
E-postadresse
- -
-
-
GCP-prosjekt ID
- -
-
-
Estimert månedlig GCP-spend (USD)
- -
- -
- -
-
- - -
-
Hvor vil du at Jason skal nå deg?
-
Velg én eller flere kanaler. CostGuard leverer direkte dit du allerede er.
- -
- -
-
🔗
-
-
Webhook (Slack · Teams · Google Chat · Discord)
-
Jason poster meldinger direkte i din eksisterende kanal.
-
Starter+
-
-
-
Webhook URL
- -
-
-
-
-
- -
-
📧
-
-
E-post
-
Daglig digest + varsler ved spikes. Sendes via SendGrid.
-
Starter+
-
-
-
Mottaker-e-post (forhåndsutfylt fra forrige steg)
- -
-
-
-
-
- -
-
📱
-
-
SMS (kun kritiske spikes)
-
Twilio-integrasjon — kommer i Guard-tier.
-
Guard+ · Coming soon
-
-
-
- -
- -
- - -
-
- - -
-
Test varslingene
-
Send en testmelding for å bekrefte at alt er koblet opp riktig — før vi aktiverer CostGuard.
- - - - - -

Du kan hoppe over testingen og aktivere direkte.

- -
- - -
-
- - -
-
-
CostGuard er aktivert!
-
Jason overvåker nå .
Du mottar første rapport innen 24 timer.
- -
-
Bedrift
-
Kontakt
-
GCP-prosjekt
-
Tier
-
Kanaler
-
- - -
- -
- -
-