58ca4e68db
- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics) - Rust analytics service with parallel report generation - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables, full migrations - Redis cache, sessions, pub/sub - Kafka event streaming with Zookeeper - WebSocket hub for real-time updates - Automation engine with cron jobs, workflows, event triggers - JWT authentication, multi-tenant from start - Docker Compose with all services - Nginx reverse proxy with rate limiting - Integration tests passing - Feature gap analysis against Fortnox/Odoo/Visma Refs: BOC-001
662 lines
23 KiB
HTML
662 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<title>quiXzoom — Launch Status</title>
|
|
<style>
|
|
/* iOS System Font Stack */
|
|
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
|
:root {
|
|
--ios-blue: #007AFF;
|
|
--ios-green: #34C759;
|
|
--ios-orange: #FF9500;
|
|
--ios-red: #FF3B30;
|
|
--ios-purple: #AF52DE;
|
|
--ios-gray: #8E8E93;
|
|
--ios-gray2: #AEAEB2;
|
|
--ios-gray6: #F2F2F7;
|
|
--ios-label: #000000;
|
|
--ios-label2: #3C3C43;
|
|
--ios-bg: #F2F2F7;
|
|
--ios-card: #FFFFFF;
|
|
--ios-sep: rgba(60,60,67,0.12);
|
|
--qx-accent: #FF6D00;
|
|
}
|
|
body {
|
|
font-family: -apple-system, 'SF Pro Display', 'Helvetica Neue', sans-serif;
|
|
background: var(--ios-bg);
|
|
color: var(--ios-label);
|
|
min-height: 100dvh;
|
|
padding-bottom: env(safe-area-inset-bottom, 20px);
|
|
}
|
|
|
|
/* Status Bar */
|
|
.status-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 20px 8px;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
.status-bar .time { font-variant-numeric: tabular-nums; }
|
|
.status-bar .icons { display: flex; gap: 6px; align-items: center; }
|
|
|
|
/* Navigation Bar */
|
|
.nav-bar {
|
|
padding: 6px 16px 12px;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
}
|
|
.nav-bar h1 {
|
|
font-size: 34px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.5px;
|
|
line-height: 1;
|
|
}
|
|
.nav-bar h1 span { color: var(--qx-accent); }
|
|
.launch-badge {
|
|
background: var(--ios-green);
|
|
color: white;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
padding: 5px 10px;
|
|
border-radius: var(--radius-md);
|
|
letter-spacing: 0.3px;
|
|
}
|
|
.launch-badge.warning { background: var(--ios-orange); }
|
|
|
|
/* Scroll Container */
|
|
.scroll { overflow-y: auto; padding: 0 16px 32px; }
|
|
|
|
/* Hero Card */
|
|
.hero-card {
|
|
background: linear-gradient(135deg, #FF6D00 0%, #FF3D00 100%);
|
|
border-radius: var(--radius-xl);
|
|
padding: 24px;
|
|
color: white;
|
|
margin-bottom: 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.hero-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
right: -30px; top: -30px;
|
|
width: 140px; height: 140px;
|
|
border-radius: var(--radius-full);
|
|
background: rgba(255,255,255,0.12);
|
|
}
|
|
.hero-card::after {
|
|
content: '';
|
|
position: absolute;
|
|
right: 30px; top: 40px;
|
|
width: 80px; height: 80px;
|
|
border-radius: var(--radius-full);
|
|
background: rgba(255,255,255,0.08);
|
|
}
|
|
.hero-label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
opacity: 0.85;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.hero-date {
|
|
font-size: 36px;
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.hero-sub {
|
|
font-size: 15px;
|
|
opacity: 0.9;
|
|
margin-bottom: 20px;
|
|
}
|
|
.hero-countdown {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
.countdown-unit {
|
|
background: rgba(255,255,255,0.22);
|
|
border-radius: var(--radius-lg);
|
|
padding: 12px 16px;
|
|
text-align: center;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.countdown-unit .val {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
display: block;
|
|
}
|
|
.countdown-unit .lbl {
|
|
font-size: 10px;
|
|
opacity: 0.75;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* Section Header */
|
|
.section-header {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.3px;
|
|
margin: 24px 0 10px;
|
|
}
|
|
|
|
/* Stats Row */
|
|
.stats-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.stat-card {
|
|
background: var(--ios-card);
|
|
border-radius: var(--radius-lg);
|
|
padding: 18px 16px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
}
|
|
.stat-card .icon {
|
|
font-size: 28px;
|
|
margin-bottom: 8px;
|
|
display: block;
|
|
}
|
|
.stat-card .val {
|
|
font-size: 30px;
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
display: block;
|
|
line-height: 1;
|
|
margin-bottom: 4px;
|
|
}
|
|
.stat-card .lbl {
|
|
font-size: 13px;
|
|
color: var(--ios-gray);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.readiness-card {
|
|
background: var(--ios-card);
|
|
border-radius: var(--radius-lg);
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
}
|
|
.readiness-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
margin-bottom: 12px;
|
|
}
|
|
.readiness-pct {
|
|
font-size: 36px;
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
}
|
|
.readiness-label { font-size: 15px; font-weight: 500; color: var(--ios-gray); }
|
|
.progress-track {
|
|
background: var(--ios-gray6);
|
|
border-radius: var(--radius-md);
|
|
height: 10px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: var(--radius-md);
|
|
background: linear-gradient(90deg, var(--ios-green) 0%, #30D158 100%);
|
|
transition: width 1.2s cubic-bezier(0.16,1,0.3,1);
|
|
}
|
|
.readiness-sub {
|
|
font-size: 13px;
|
|
color: var(--ios-gray);
|
|
}
|
|
|
|
/* Cities */
|
|
.cities-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.city-chip {
|
|
background: var(--ios-card);
|
|
border-radius: var(--radius-xl);
|
|
padding: 8px 14px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
}
|
|
.city-chip .dot {
|
|
width: 8px; height: 8px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--ios-green);
|
|
}
|
|
|
|
/* Checklist */
|
|
.checklist-group {
|
|
background: var(--ios-card);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
margin-bottom: 20px;
|
|
}
|
|
.check-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--ios-sep);
|
|
gap: 12px;
|
|
}
|
|
.check-item:last-child { border-bottom: none; }
|
|
.check-icon {
|
|
width: 32px; height: 32px;
|
|
border-radius: var(--radius-full);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
.check-icon.done { background: rgba(52,199,89,0.15); }
|
|
.check-icon.pending { background: rgba(255,149,0,0.15); }
|
|
.check-body { flex: 1; min-width: 0; }
|
|
.check-title {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
margin-bottom: 2px;
|
|
}
|
|
.check-detail {
|
|
font-size: 12px;
|
|
color: var(--ios-gray);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.check-badge {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 3px 8px;
|
|
border-radius: var(--radius-md);
|
|
letter-spacing: 0.2px;
|
|
flex-shrink: 0;
|
|
}
|
|
.check-badge.done { background: rgba(52,199,89,0.15); color: #27a745; }
|
|
.check-badge.pending { background: rgba(255,149,0,0.15); color: #c87000; }
|
|
|
|
/* Footer */
|
|
.footer {
|
|
text-align: center;
|
|
font-size: 13px;
|
|
color: var(--ios-gray);
|
|
padding: 16px 0 8px;
|
|
}
|
|
.footer strong { color: var(--ios-label); }
|
|
|
|
/* Skeleton / loading */
|
|
.skeleton {
|
|
background: linear-gradient(90deg, #e8e8e8 25%, #f5f5f5 50%, #e8e8e8 75%);
|
|
background-size: 200% 100%;
|
|
animation: shimmer 1.5s infinite;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
@keyframes shimmer { to { background-position: -200% 0; } }
|
|
|
|
/* Dark Mode */
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
--ios-bg: #000000;
|
|
--ios-card: #1C1C1E;
|
|
--ios-label: #FFFFFF;
|
|
--ios-gray6: #2C2C2E;
|
|
--ios-sep: rgba(255,255,255,0.1);
|
|
}
|
|
.skeleton { background: linear-gradient(90deg, #2a2a2a 25%, #383838 50%, #2a2a2a 75%); background-size: 200% 100%; }
|
|
}
|
|
|
|
/* Pulse dot */
|
|
.live-dot {
|
|
display: inline-block;
|
|
width: 8px; height: 8px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--ios-green);
|
|
box-shadow: 0 0 0 0 rgba(52,199,89,0.5);
|
|
animation: pulse 2s infinite;
|
|
margin-right: 6px;
|
|
vertical-align: middle;
|
|
}
|
|
@keyframes pulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(52,199,89,0.5); }
|
|
70% { box-shadow: 0 0 0 8px rgba(52,199,89,0); }
|
|
100%{ box-shadow: 0 0 0 0 rgba(52,199,89,0); }
|
|
}
|
|
|
|
/* Refresh button */
|
|
.refresh-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
background: var(--ios-blue);
|
|
color: white;
|
|
border: none;
|
|
border-radius: var(--radius-lg);
|
|
padding: 16px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
margin-top: 8px;
|
|
font-family: inherit;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
.refresh-btn:active { opacity: 0.8; transform: scale(0.98); }
|
|
@media (max-width: 640px) {
|
|
.hero h1 { font-size: 2rem !important; }
|
|
.hero p { font-size: 1rem !important; }
|
|
.card { padding: 1.5rem !important; }
|
|
.grid { grid-template-columns: 1fr !important; }
|
|
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
|
}
|
|
</style>
|
|
|
|
<style id="wcag-contrast-enforcement">
|
|
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
|
|
/* Regel: Ingen text under 70% opacity. Aldrig. */
|
|
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
/* Fix common low-contrast patterns */
|
|
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
|
|
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
|
|
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
|
|
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
|
|
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
|
|
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
|
|
@media (max-width: 640px) {
|
|
.hero h1 { font-size: 2rem !important; }
|
|
.hero p { font-size: 1rem !important; }
|
|
.card { padding: 1.5rem !important; }
|
|
.grid { grid-template-columns: 1fr !important; }
|
|
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Status Bar -->
|
|
<div class="status-bar">
|
|
<span class="time" id="clock">9:41</span>
|
|
<span class="icons">
|
|
<svg width="17" height="12" viewBox="0 0 17 12" fill="currentColor"><rect x="0" y="3" width="3" height="9" rx="1" opacity="0.35"/><rect x="4" y="2" width="3" height="10" rx="1" opacity="0.55"/><rect x="8" y="1" width="3" height="11" rx="1" opacity="0.75"/><rect x="12" y="0" width="3" height="12" rx="1"/></svg>
|
|
<svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor"><path d="M8 2.5C10.2 2.5 12.2 3.5 13.5 5.1L15 3.4C13.3 1.3 10.8 0 8 0S2.7 1.3 1 3.4L2.5 5.1C3.8 3.5 5.8 2.5 8 2.5zM8 5.5C9.5 5.5 10.8 6.1 11.7 7.1L13.2 5.4C11.9 4 10 3 8 3S4.1 4 2.8 5.4L4.3 7.1C5.2 6.1 6.5 5.5 8 5.5zM8 8.5c.9 0 1.7.4 2.3 1L8 12 5.7 9.5c.6-.6 1.4-1 2.3-1z"/></svg>
|
|
<svg width="25" height="12" viewBox="0 0 25 12" fill="currentColor"><rect x="0" y="1" width="21" height="10" rx="3.5" stroke="currentColor" stroke-width="1" fill="none" opacity="0.35"/><rect x="1.5" y="2.5" width="17" height="7" rx="2" fill="currentColor"/><rect x="22" y="4" width="3" height="4" rx="1.5" opacity="0.4"/></svg>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Nav Bar -->
|
|
<div class="nav-bar">
|
|
<h1>quiX<span>zoom</span></h1>
|
|
<div class="launch-badge" id="go-badge">Laddar...</div>
|
|
</div>
|
|
|
|
<!-- Scroll Area -->
|
|
<div class="scroll">
|
|
|
|
<!-- Hero Card -->
|
|
<div class="hero-card">
|
|
<div class="hero-label">Launch Sverige</div>
|
|
<div class="hero-date">1 augusti 2026</div>
|
|
<div class="hero-sub"><span class="live-dot"></span>Live tracking</div>
|
|
<div class="hero-countdown">
|
|
<div class="countdown-unit">
|
|
<span class="val" id="cd-days">—</span>
|
|
<span class="lbl">Dagar</span>
|
|
</div>
|
|
<div class="countdown-unit">
|
|
<span class="val" id="cd-hours">—</span>
|
|
<span class="lbl">Timmar</span>
|
|
</div>
|
|
<div class="countdown-unit">
|
|
<span class="val" id="cd-mins">—</span>
|
|
<span class="lbl">Minuter</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="section-header">Live-statistik</div>
|
|
<div class="stats-row">
|
|
<div class="stat-card">
|
|
<span class="icon"></span>
|
|
<span class="val" id="stat-missions">—</span>
|
|
<span class="lbl">Uppdrag aktiva</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></span>
|
|
<span class="val" id="stat-zoomers">—</span>
|
|
<span class="lbl">Zoomers aktiva</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="icon"></span>
|
|
<span class="val" id="stat-cities">—</span>
|
|
<span class="lbl">Städer täckta</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="icon"></span>
|
|
<span class="val" id="stat-days">—</span>
|
|
<span class="lbl">Dagar kvar</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Readiness -->
|
|
<div class="readiness-card">
|
|
<div class="readiness-top">
|
|
<div>
|
|
<div class="readiness-pct" id="readiness-pct">—%</div>
|
|
<div class="readiness-label">Launch Readiness</div>
|
|
</div>
|
|
<div style="font-size:13px; color:var(--ios-gray); text-align:right;">
|
|
<span id="done-count">—</span> / <span id="total-count">—</span><br>klara
|
|
</div>
|
|
</div>
|
|
<div class="progress-track">
|
|
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
|
|
</div>
|
|
<div class="readiness-sub" id="readiness-sub">Hämtar status...</div>
|
|
</div>
|
|
|
|
<!-- Cities -->
|
|
<div class="section-header">Täckningsområde</div>
|
|
<div class="cities-row" id="cities-row">
|
|
<div class="city-chip skeleton" style="width:100px; height:36px;"></div>
|
|
<div class="city-chip skeleton" style="width:90px; height:36px;"></div>
|
|
<div class="city-chip skeleton" style="width:80px; height:36px;"></div>
|
|
</div>
|
|
|
|
<!-- Checklist: Done -->
|
|
<div class="section-header">Klart <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
|
<div class="checklist-group" id="done-list">
|
|
<div class="check-item">
|
|
<div class="check-icon done skeleton" style="background:transparent"></div>
|
|
<div class="check-body">
|
|
<div class="skeleton" style="height:14px; width:70%; margin-bottom:6px;"></div>
|
|
<div class="skeleton" style="height:11px; width:50%;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Checklist: Pending -->
|
|
<div class="section-header">Återstår ⏳</div>
|
|
<div class="checklist-group" id="pending-list">
|
|
<div class="check-item">
|
|
<div class="check-icon pending skeleton" style="background:transparent"></div>
|
|
<div class="check-body">
|
|
<div class="skeleton" style="height:14px; width:60%; margin-bottom:6px;"></div>
|
|
<div class="skeleton" style="height:11px; width:45%;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="refresh-btn" onclick="loadData()">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
Uppdatera
|
|
</button>
|
|
|
|
<div class="footer">
|
|
<strong>quiXzoom</strong> — Wavult Group · <span id="last-updated">—</span>
|
|
</div>
|
|
|
|
</div><!-- /scroll -->
|
|
|
|
<script>
|
|
// Clock
|
|
function updateClock() {
|
|
const now = new Date();
|
|
document.getElementById('clock').textContent =
|
|
now.getHours().toString().padStart(2,'0') + ':' +
|
|
now.getMinutes().toString().padStart(2,'0');
|
|
}
|
|
updateClock();
|
|
setInterval(updateClock, 30000);
|
|
|
|
// Countdown
|
|
function updateCountdown() {
|
|
const launch = new Date('2026-06-15T07:00:00+02:00');
|
|
const now = new Date();
|
|
const diff = Math.max(0, launch - now);
|
|
const d = Math.floor(diff / 86400000);
|
|
const h = Math.floor((diff % 86400000) / 3600000);
|
|
const m = Math.floor((diff % 3600000) / 60000);
|
|
document.getElementById('cd-days').textContent = d;
|
|
document.getElementById('cd-hours').textContent = h.toString().padStart(2,'0');
|
|
document.getElementById('cd-mins').textContent = m.toString().padStart(2,'0');
|
|
}
|
|
updateCountdown();
|
|
setInterval(updateCountdown, 60000);
|
|
|
|
// Build checklist item
|
|
const ICONS = { done: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>', pending: '⏳' };
|
|
function buildItem(c) {
|
|
const div = document.createElement('div');
|
|
div.className = 'check-item';
|
|
div.innerHTML = `
|
|
<div class="check-icon ${c.status}">${ICONS[c.status] || '·'}</div>
|
|
<div class="check-body">
|
|
<div class="check-title">${c.label}</div>
|
|
<div class="check-detail">${c.detail || ''}</div>
|
|
</div>
|
|
<span class="check-badge ${c.status}">${c.status === 'done' ? 'Klar' : 'Väntar'}</span>
|
|
`;
|
|
return div;
|
|
}
|
|
|
|
// Load data
|
|
async function loadData() {
|
|
try {
|
|
// Use a dummy JWT for auth (dev mode)
|
|
const demoToken = 'eyJhbGciOiJub25lIn0.' +
|
|
btoa(JSON.stringify({ sub: 'launch-status-ui', email: 'bernt@wavult.com' })) + '.';
|
|
|
|
const [readRes, statsRes] = await Promise.all([
|
|
fetch('https://api.quixzoom.com/api/qz/launch-readiness', {
|
|
headers: { Authorization: 'Bearer ' + demoToken }
|
|
}),
|
|
fetch('https://api.quixzoom.com/api/qz/stats', {
|
|
headers: { Authorization: 'Bearer ' + demoToken }
|
|
})
|
|
]);
|
|
|
|
const data = await readRes.json();
|
|
const stats = statsRes.ok ? await statsRes.json() : null;
|
|
|
|
// Stats cards
|
|
const activeMissions = stats ? (stats.missions.open + stats.missions.active) : data.summary.done;
|
|
const activeZoomers = stats ? (stats.zoomers.available + stats.zoomers.busy) : 3;
|
|
document.getElementById('stat-missions').textContent = activeMissions;
|
|
document.getElementById('stat-zoomers').textContent = activeZoomers;
|
|
document.getElementById('stat-cities').textContent = data.summary.cities;
|
|
document.getElementById('stat-days').textContent = data.days_to_launch;
|
|
|
|
// Readiness
|
|
const pct = data.readiness_pct;
|
|
document.getElementById('readiness-pct').textContent = pct + '%';
|
|
document.getElementById('done-count').textContent = data.summary.done;
|
|
document.getElementById('total-count').textContent = data.summary.total;
|
|
document.getElementById('readiness-sub').textContent =
|
|
data.launch_go
|
|
? '🟢 Launch Go — klar för 1 augusti!'
|
|
: `⏳ ${data.summary.pending} saker återstår innan launch`;
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('progress-fill').style.width = pct + '%';
|
|
}, 100);
|
|
|
|
// Go badge
|
|
const badge = document.getElementById('go-badge');
|
|
if (data.launch_go) {
|
|
badge.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> GO';
|
|
badge.className = 'launch-badge';
|
|
} else {
|
|
badge.textContent = '⏳ ' + data.days_to_launch + 'd kvar';
|
|
badge.className = 'launch-badge warning';
|
|
}
|
|
|
|
// Cities
|
|
const citiesRow = document.getElementById('cities-row');
|
|
citiesRow.innerHTML = '';
|
|
(data.summary.cities_list || []).forEach(city => {
|
|
const chip = document.createElement('div');
|
|
chip.className = 'city-chip';
|
|
chip.innerHTML = `<span class="dot"></span>${city}`;
|
|
citiesRow.appendChild(chip);
|
|
});
|
|
|
|
// Checklist
|
|
const doneList = document.getElementById('done-list');
|
|
const pendingList = document.getElementById('pending-list');
|
|
doneList.innerHTML = '';
|
|
pendingList.innerHTML = '';
|
|
|
|
data.checklist
|
|
.filter(c => c.status === 'done')
|
|
.forEach(c => doneList.appendChild(buildItem(c)));
|
|
|
|
data.checklist
|
|
.filter(c => c.status !== 'done')
|
|
.forEach(c => pendingList.appendChild(buildItem(c)));
|
|
|
|
if (doneList.children.length === 0)
|
|
doneList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-gray);font-size:14px;padding:20px;">Inga klara ännu</div>';
|
|
if (pendingList.children.length === 0)
|
|
pendingList.innerHTML = '<div class="check-item" style="justify-content:center;color:var(--ios-green);font-size:14px;padding:20px;">Allt klart! </div>';
|
|
|
|
// Timestamp
|
|
document.getElementById('last-updated').textContent =
|
|
'Uppdaterat ' + new Date().toLocaleTimeString('sv-SE', { hour:'2-digit', minute:'2-digit' });
|
|
|
|
} catch (err) {
|
|
console.error('Launch readiness load error:', err);
|
|
document.getElementById('readiness-sub').textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Kunde inte ladda data — kontrollera API';
|
|
document.getElementById('go-badge').textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Fel';
|
|
document.getElementById('go-badge').className = 'launch-badge warning';
|
|
}
|
|
}
|
|
|
|
// Load on start and auto-refresh every 60s
|
|
loadData();
|
|
setInterval(loadData, 60000);
|
|
</script>
|
|
</body>
|
|
</html>
|