6989a98d75
- Arkitektur: docs/auth/passwordless-architecture.md - Backend: iom/quixzoom-auth-service/ (FastAPI + Redis) - Webb: quixzoom-market-pages/se/login/ (QR-kod + polling) - App: iom/quixzoom-app/src/features/auth/ (push + deep links) Flöde: QR-kod → app-godkännande → webb-inloggad
838 lines
35 KiB
HTML
838 lines
35 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">
|
||
<title>Launch Readiness — quiXzoom 1 augusti 2026</title>
|
||
<style>
|
||
/* iOS 26 / Apple Native — Light Mode */
|
||
:root {
|
||
--bg: #f2f2f7;
|
||
--bg-card: #ffffff;
|
||
--bg-secondary: #f2f2f7;
|
||
--separator: rgba(60,60,67,0.18);
|
||
--label: #000000;
|
||
--label-secondary: rgba(60,60,67,0.6);
|
||
--label-tertiary: rgba(60,60,67,0.3);
|
||
--tint: #007AFF;
|
||
--green: #34C759;
|
||
--red: #FF3B30;
|
||
--yellow: #FF9500;
|
||
--orange: #FF9500;
|
||
--purple: #AF52DE;
|
||
--fill: rgba(120,120,128,0.12);
|
||
--fill-secondary: rgba(120,120,128,0.08);
|
||
--radius: 12px;
|
||
--radius-lg: 16px;
|
||
--radius-sm: 8px;
|
||
--font: -apple-system, "SF Pro Display", "Helvetica Neue", sans-serif;
|
||
--safe-top: env(safe-area-inset-top, 0px);
|
||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||
|
||
body {
|
||
font-family: var(--font);
|
||
background: var(--bg);
|
||
color: var(--label);
|
||
min-height: 100dvh;
|
||
padding-top: calc(var(--safe-top) + 0px);
|
||
padding-bottom: calc(var(--safe-bottom) + 60px);
|
||
}
|
||
|
||
/* Nav Bar */
|
||
.nav-bar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
background: rgba(242,242,247,0.88);
|
||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||
backdrop-filter: blur(20px) saturate(180%);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
padding: 14px 20px 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.nav-title { font-size: 17px; font-weight: 600; letter-spacing: -0.4px; }
|
||
.nav-badge {
|
||
background: var(--tint);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
padding: 4px 12px;
|
||
border-radius: var(--radius-xl);
|
||
transition: background 0.3s;
|
||
}
|
||
.nav-badge.ready { background: var(--green); }
|
||
.nav-badge.warn { background: var(--orange); }
|
||
|
||
/* Page Header */
|
||
.page-header { padding: 24px 20px 8px; }
|
||
.page-title { font-size: 34px; font-weight: 700; letter-spacing: -0.5px; line-height: 1.1; }
|
||
.page-sub { font-size: 15px; color: var(--label-secondary); margin-top: 4px; }
|
||
|
||
/* Countdown Hero */
|
||
.countdown-hero {
|
||
margin: 16px 16px 0;
|
||
background: linear-gradient(135deg, #001E50 0%, #0040A8 100%);
|
||
border-radius: var(--radius-lg);
|
||
padding: 22px 20px 20px;
|
||
box-shadow: 0 4px 20px rgba(0,30,80,0.3);
|
||
color: #fff;
|
||
}
|
||
.countdown-label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
color: rgba(255,255,255,0.75);
|
||
margin-bottom: 12px;
|
||
}
|
||
.countdown-blocks {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: flex-start;
|
||
}
|
||
.cd-block {
|
||
flex: 1;
|
||
background: rgba(255,255,255,0.12);
|
||
border-radius: var(--radius-md);
|
||
padding: 12px 6px 10px;
|
||
text-align: center;
|
||
}
|
||
.cd-num {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: -1px;
|
||
line-height: 1;
|
||
}
|
||
.cd-unit {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: rgba(255,255,255,0.75);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-top: 4px;
|
||
}
|
||
.cd-sep {
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
color: rgba(255,255,255,0.75);
|
||
margin-top: 10px;
|
||
}
|
||
.countdown-launch {
|
||
margin-top: 14px;
|
||
font-size: 13px;
|
||
color: rgba(255,255,255,0.8);
|
||
text-align: center;
|
||
}
|
||
.countdown-launch strong { color: #fff; font-weight: 700; }
|
||
|
||
/* Progress Hero */
|
||
.progress-hero {
|
||
margin: 12px 16px 0;
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-lg);
|
||
padding: 18px 20px 16px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
}
|
||
.progress-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
margin-bottom: 12px;
|
||
}
|
||
.progress-pct {
|
||
font-size: 42px;
|
||
font-weight: 700;
|
||
letter-spacing: -1px;
|
||
line-height: 1;
|
||
color: var(--tint);
|
||
}
|
||
.progress-label { font-size: 14px; color: var(--label-secondary); margin-top: 2px; }
|
||
.progress-right { text-align: right; }
|
||
.deadline-label { font-size: 12px; color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.deadline-date { font-size: 20px; font-weight: 700; letter-spacing: -0.4px; }
|
||
.days-left { font-size: 13px; color: var(--orange); font-weight: 600; margin-top: 2px; }
|
||
.days-left.green { color: var(--green); }
|
||
.progress-bar-bg {
|
||
background: var(--fill);
|
||
border-radius: var(--radius-sm);
|
||
height: 10px;
|
||
overflow: hidden;
|
||
}
|
||
.progress-bar-fill {
|
||
height: 100%;
|
||
border-radius: var(--radius-sm);
|
||
background: linear-gradient(90deg, var(--tint), #5AC8FA);
|
||
transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
/* Stat Row */
|
||
.stat-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 10px;
|
||
padding: 12px 16px 0;
|
||
}
|
||
.stat-mini {
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius);
|
||
padding: 12px 10px;
|
||
text-align: center;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||
}
|
||
.stat-mini-num {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: -0.4px;
|
||
}
|
||
.stat-mini-num.green { color: var(--green); }
|
||
.stat-mini-num.red { color: var(--red); }
|
||
.stat-mini-num.yellow { color: var(--yellow); }
|
||
.stat-mini-num.blue { color: var(--tint); }
|
||
.stat-mini-lbl {
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
color: var(--label-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.3px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* Final Verification Banner */
|
||
.verify-banner {
|
||
margin: 22px 16px 0;
|
||
background: rgba(52,199,89,0.10);
|
||
border: 1.5px solid rgba(52,199,89,0.35);
|
||
border-radius: var(--radius-lg);
|
||
padding: 14px 16px;
|
||
}
|
||
.verify-banner-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.6px;
|
||
color: var(--green);
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.verify-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 7px 0;
|
||
border-bottom: 0.5px solid rgba(52,199,89,0.2);
|
||
}
|
||
.verify-row:last-child { border-bottom: none; padding-bottom: 0; }
|
||
.verify-dot {
|
||
width: 20px; height: 20px;
|
||
border-radius: var(--radius-full);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
.vdot-ok { background: var(--green); color: #fff; }
|
||
.vdot-warn { background: var(--orange); color: #fff; }
|
||
.vdot-err { background: var(--red); color: #fff; }
|
||
.verify-text { font-size: 14px; font-weight: 500; flex: 1; }
|
||
.verify-endpoint {
|
||
font-size: 11px;
|
||
font-family: "SF Mono", "Menlo", monospace;
|
||
color: var(--label-secondary);
|
||
background: var(--fill);
|
||
padding: 2px 6px;
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
/* Category Section */
|
||
.cat-section { margin-top: 28px; }
|
||
.cat-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 0 20px 10px;
|
||
}
|
||
.cat-icon {
|
||
width: 28px; height: 28px;
|
||
border-radius: var(--radius-sm);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 14px;
|
||
flex-shrink: 0;
|
||
}
|
||
.cat-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.6px;
|
||
color: var(--label-secondary);
|
||
flex: 1;
|
||
}
|
||
.cat-progress {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--label-tertiary);
|
||
}
|
||
.cat-progress-bar {
|
||
height: 3px;
|
||
border-radius: var(--radius-sm);
|
||
background: var(--fill);
|
||
margin: 0 20px 8px;
|
||
overflow: hidden;
|
||
}
|
||
.cat-progress-fill {
|
||
height: 100%;
|
||
border-radius: var(--radius-sm);
|
||
transition: width 0.6s ease;
|
||
}
|
||
|
||
/* Checklist */
|
||
.checklist {
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-lg);
|
||
margin: 0 16px;
|
||
overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||
}
|
||
.check-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
padding: 14px 16px;
|
||
border-bottom: 0.5px solid var(--separator);
|
||
transition: background 0.12s;
|
||
-webkit-user-select: none;
|
||
user-select: none;
|
||
}
|
||
.check-item:last-child { border-bottom: none; }
|
||
.check-item:active { background: var(--fill); }
|
||
|
||
.check-circle {
|
||
width: 24px; height: 24px;
|
||
border-radius: var(--radius-full);
|
||
flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-top: 1px;
|
||
transition: all 0.2s;
|
||
}
|
||
.check-circle.done { background: var(--green); color: #fff; }
|
||
.check-circle.in-prog { background: rgba(0,122,255,0.15); border: 2px solid var(--tint); color: var(--tint); }
|
||
.check-circle.blocked { background: rgba(255,59,48,0.12); border: 2px solid var(--red); color: var(--red); }
|
||
.check-circle.pending { border: 2px solid var(--separator); color: var(--label-tertiary); background: transparent; }
|
||
|
||
.check-content { flex: 1; min-width: 0; }
|
||
.check-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--label);
|
||
line-height: 1.3;
|
||
}
|
||
.check-title.line-through { text-decoration: line-through; color: var(--label-secondary); }
|
||
.check-note {
|
||
font-size: 12px;
|
||
color: var(--label-tertiary);
|
||
margin-top: 3px;
|
||
font-style: italic;
|
||
}
|
||
.check-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-top: 5px;
|
||
align-items: center;
|
||
}
|
||
.meta-chip {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: 2px 8px;
|
||
border-radius: var(--radius-sm);
|
||
background: var(--fill);
|
||
color: var(--label-secondary);
|
||
}
|
||
.meta-deadline {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--label-tertiary);
|
||
}
|
||
.meta-deadline.urgent { color: var(--red); font-weight: 600; }
|
||
.meta-deadline.soon { color: var(--orange); font-weight: 600; }
|
||
|
||
.check-right {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4px;
|
||
}
|
||
.owner-badge {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 3px 8px;
|
||
border-radius: var(--radius-sm);
|
||
background: var(--fill);
|
||
color: var(--label-secondary);
|
||
}
|
||
.status-tag {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 3px 8px;
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
.tag-done { background: rgba(52,199,89,0.15); color: var(--green); }
|
||
.tag-inprog { background: rgba(0,122,255,0.12); color: var(--tint); }
|
||
.tag-blocked { background: rgba(255,59,48,0.12); color: var(--red); }
|
||
.tag-pending { background: var(--fill); color: var(--label-tertiary); }
|
||
|
||
/* Footer */
|
||
.footer {
|
||
text-align: center;
|
||
padding: 32px 20px 12px;
|
||
font-size: 12px;
|
||
color: var(--label-tertiary);
|
||
line-height: 1.6;
|
||
}
|
||
@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>
|
||
<h1 style="position:absolute;left:-9999px;">Launch Checklist for quiXzoom</h1>
|
||
|
||
<!-- Nav Bar -->
|
||
<div class="nav-bar">
|
||
<div class="nav-title"><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> Launch Readiness</div>
|
||
<div class="nav-badge" id="navBadge">Laddar…</div>
|
||
</div>
|
||
|
||
<!-- Page Header -->
|
||
<div class="page-header">
|
||
<div class="page-title">quiXzoom Sverige</div>
|
||
<div class="page-sub">Final Launch Check · <span id="totalItems">—</span> items · 1 augusti 2026</div>
|
||
</div>
|
||
|
||
<!-- Countdown Hero -->
|
||
<div class="countdown-hero">
|
||
<div class="countdown-label">Tid till launch</div>
|
||
<div class="countdown-blocks">
|
||
<div class="cd-block">
|
||
<div class="cd-num" id="cdDays">--</div>
|
||
<div class="cd-unit">Dagar</div>
|
||
</div>
|
||
<div class="cd-sep">:</div>
|
||
<div class="cd-block">
|
||
<div class="cd-num" id="cdHours">--</div>
|
||
<div class="cd-unit">Timmar</div>
|
||
</div>
|
||
<div class="cd-sep">:</div>
|
||
<div class="cd-block">
|
||
<div class="cd-num" id="cdMins">--</div>
|
||
<div class="cd-unit">Minuter</div>
|
||
</div>
|
||
<div class="cd-sep">:</div>
|
||
<div class="cd-block">
|
||
<div class="cd-num" id="cdSecs">--</div>
|
||
<div class="cd-unit">Sekunder</div>
|
||
</div>
|
||
</div>
|
||
<div class="countdown-launch">
|
||
Launch: <strong>1 augusti 2026 kl. 09:00 CEST</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress Hero -->
|
||
<div class="progress-hero">
|
||
<div class="progress-row">
|
||
<div class="progress-left">
|
||
<div class="progress-pct" id="heroPct">0%</div>
|
||
<div class="progress-label" id="heroLabel">Laddar…</div>
|
||
</div>
|
||
<div class="progress-right">
|
||
<div class="deadline-label">Go / No-Go</div>
|
||
<div class="deadline-date">1 augusti</div>
|
||
<div class="days-left" id="daysLeft">—</div>
|
||
</div>
|
||
</div>
|
||
<div class="progress-bar-bg">
|
||
<div class="progress-bar-fill" id="heroBar" style="width:0%"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stat Row -->
|
||
<div class="stat-row">
|
||
<div class="stat-mini">
|
||
<div class="stat-mini-num green" id="cntDone">—</div>
|
||
<div class="stat-mini-lbl">Klara</div>
|
||
</div>
|
||
<div class="stat-mini">
|
||
<div class="stat-mini-num blue" id="cntInprog">—</div>
|
||
<div class="stat-mini-lbl">Pågår</div>
|
||
</div>
|
||
<div class="stat-mini">
|
||
<div class="stat-mini-num red" id="cntBlocked">—</div>
|
||
<div class="stat-mini-lbl">Blockade</div>
|
||
</div>
|
||
<div class="stat-mini">
|
||
<div class="stat-mini-num yellow" id="cntPending">—</div>
|
||
<div class="stat-mini-lbl">Pending</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Final Verification Section -->
|
||
<div style="margin:22px 16px 0">
|
||
<div style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:0.6px;color:var(--label-secondary);padding:0 4px 8px;display:flex;align-items:center;gap:6px;">
|
||
<span style="font-size:16px"><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></span> Final Verification — W11-QUIXZOOM-LAUNCH
|
||
</div>
|
||
</div>
|
||
<div class="verify-banner">
|
||
<div class="verify-banner-title"><span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg></span> 5 kritiska kontrollpunkter</div>
|
||
|
||
<div class="verify-row">
|
||
<div class="verify-dot vdot-ok"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||
<div class="verify-text">Alla quiXzoom API-endpoints svarar</div>
|
||
<div class="verify-endpoint">14 moduler · /api/quixzoom/*</div>
|
||
</div>
|
||
|
||
<div class="verify-row">
|
||
<div class="verify-dot vdot-ok"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||
<div class="verify-text">Zoomer-registrering end-to-end</div>
|
||
<div class="verify-endpoint">POST /zoomers/register → KYC → godkänd</div>
|
||
</div>
|
||
|
||
<div class="verify-row">
|
||
<div class="verify-dot vdot-ok"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||
<div class="verify-text">Mission create / accept / complete-flöde</div>
|
||
<div class="verify-endpoint">draft → open → active → completed</div>
|
||
</div>
|
||
|
||
<div class="verify-row">
|
||
<div class="verify-dot vdot-ok"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||
<div class="verify-text">Payout-automation aktiv (Stripe Connect)</div>
|
||
<div class="verify-endpoint">POST /payouts/process · 85% Zoomer / 15% plattform</div>
|
||
</div>
|
||
|
||
<div class="verify-row">
|
||
<div class="verify-dot vdot-ok"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||
<div class="verify-text">DAC7 XML-export fungerar</div>
|
||
<div class="verify-endpoint">GET /compliance/dac7/report?format=xml · Skatteverket-schema</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Checklist (rendered by JS) -->
|
||
<div id="checklistContainer"></div>
|
||
|
||
<div class="footer">
|
||
quiXzoom Launch Readiness Final Check<br>
|
||
Uppdaterat <span id="footerTs">—</span> · W11-QUIXZOOM-LAUNCH
|
||
</div>
|
||
|
||
<script>
|
||
const LAUNCH = new Date('2026-06-15T09:00:00+02:00');
|
||
|
||
// Countdown
|
||
function pad(n) { return String(n).padStart(2, '0'); }
|
||
|
||
function tickCountdown() {
|
||
const diff = LAUNCH - new Date();
|
||
if (diff <= 0) {
|
||
document.getElementById('cdDays').textContent = '00';
|
||
document.getElementById('cdHours').textContent = '00';
|
||
document.getElementById('cdMins').textContent = '00';
|
||
document.getElementById('cdSecs').textContent = '00';
|
||
return;
|
||
}
|
||
const totalSecs = Math.floor(diff / 1000);
|
||
const days = Math.floor(totalSecs / 86400);
|
||
const hours = Math.floor((totalSecs % 86400) / 3600);
|
||
const mins = Math.floor((totalSecs % 3600) / 60);
|
||
const secs = totalSecs % 60;
|
||
document.getElementById('cdDays').textContent = pad(days);
|
||
document.getElementById('cdHours').textContent = pad(hours);
|
||
document.getElementById('cdMins').textContent = pad(mins);
|
||
document.getElementById('cdSecs').textContent = pad(secs);
|
||
}
|
||
tickCountdown();
|
||
setInterval(tickCountdown, 1000);
|
||
|
||
const CATEGORIES = [
|
||
{
|
||
id: 'verification',
|
||
name: 'API Verification',
|
||
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>',
|
||
color: '#007AFF',
|
||
items: [
|
||
{ title: 'missions.mjs — create/accept/complete lifecycle', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions + PATCH /:id/status · statusmaskin verifierad' },
|
||
{ title: 'registry.mjs — Zoomer-registrering E2E', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /zoomers/register → KYC-steg 1-6 → verified' },
|
||
{ title: 'payouts.mjs — Stripe Connect batch-utbetalning', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /payouts/batch · 15% platform fee · min 100 SEK trösk' },
|
||
{ title: 'dac7.mjs — XML-export Skatteverket-schema', owner: 'Legal', status: 'done', deadline: 'Juni 10', note: 'GET /compliance/dac7/report?format=xml · DAC7 Art. 8ac DAC' },
|
||
{ title: 'matching.mjs — Haversine-matchning live', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions/:id/find-zoomers · avstånd+rating+utrustning' },
|
||
{ title: 'ratings.mjs — exponential decay weighting', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: '30-dagars halveringstid · minimum 3 ratings för publik score' },
|
||
{ title: 'kyc-metrics.mjs — /kyc/metrics endpoint', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'Var blockad 2026-05-20 → fixad och verifierad' },
|
||
{ title: 'push-notifications.mjs — APNs Production Certificate', owner: 'iOS', status: 'in-prog', deadline: 'Juni 13', urgent: true, note: 'Installerat på ny Mac efter SSH-access löst' },
|
||
{ title: 'analytics.mjs — 7 KPI-endpoints svarar', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: '/overview /funnel /geographic /trend /earnings /revenue /health' },
|
||
{ title: 'mission-ws.mjs — WebSocket real-time broadcast', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'ws://quixzoom-api.wavult.com/ws/missions · 3s interval' },
|
||
]
|
||
},
|
||
{
|
||
id: 'zoomer',
|
||
name: 'Zoomer-flöde',
|
||
icon: '',
|
||
color: '#34C759',
|
||
items: [
|
||
{ title: 'Zoomer-registrering komplett (6-stegs KYC)', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'Personuppgifter → ID → Adress → Bankkonto → Skatteform → Villkor' },
|
||
{ title: 'BankID Sandbox verifierad', owner: 'Backend', status: 'done', deadline: 'Juni 1' },
|
||
{ title: 'BankID Production-avtal signerat', owner: 'Erik', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Zoomer-profil & portfolio-hantering live', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'GET/PATCH /zoomers/:id/profile + portfolio CRUD' },
|
||
{ title: 'Lönerapport & earnings-historik synlig i app', owner: 'iOS', status: 'in-prog', deadline: 'Juni 13' },
|
||
{ title: 'Skill-nivå-validering (professional/drone/basic)', owner: 'Backend', status: 'done', deadline: 'Juni 5' },
|
||
]
|
||
},
|
||
{
|
||
id: 'mission',
|
||
name: 'Mission-flöde',
|
||
icon: '',
|
||
color: '#FF9500',
|
||
items: [
|
||
{ title: 'Customer bokar uppdrag (request-mission-flöde)', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /customers/:id/book → mission draft' },
|
||
{ title: 'Matchningsalgoritm föreslår topp-3 Zoomers', owner: 'Backend', status: 'done', deadline: 'Juni 10' },
|
||
{ title: 'Zoomer accepterar → mission active', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions/:id/accept · push-notis skickas' },
|
||
{ title: 'Check-in/check-out GPS-verifiering', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions/:id/checkin + /checkout' },
|
||
{ title: 'Mission-komplettering + leverabler uppladdade', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions/:id/complete + /deliverables' },
|
||
{ title: 'Rating-formulär skickas automatiskt efter complete', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /ratings · viktad genomsnitt beräknas' },
|
||
{ title: '12 seed-uppdrag i SE-städer (Sthlm/Gbg/Malmö/Uppsala)', owner: 'Backend', status: 'done', deadline: 'Juni 5', note: 'Via deep-build.mjs' },
|
||
]
|
||
},
|
||
{
|
||
id: 'payout',
|
||
name: 'Utbetalningar & DAC7',
|
||
icon: '',
|
||
color: '#AF52DE',
|
||
items: [
|
||
{ title: 'Stripe Production Keys konfigurerade', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Stripe Connect Zoomer-onboarding testad', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'stripe_connect + bank_transfer + mock-metoder' },
|
||
{ title: 'Automatisk payout dag 7 efter mission complete', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /payouts/process · mintröskel 100 SEK ackumuleras' },
|
||
{ title: 'Batch-utbetalning månadsvis konfigurerad', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /payouts/batch · alla pending zoomers' },
|
||
{ title: 'DAC7 TIN-verifiering per Zoomer', owner: 'Legal', status: 'done', deadline: 'Juni 10', note: 'POST /compliance/dac7/sellers/:sellerId/verify' },
|
||
{ title: 'DAC7 XML rapport genereras (Skatteverket-format)', owner: 'Legal', status: 'done', deadline: 'Juni 10', note: 'urn:oecd:ties:dpi:v1 · messageRefId QZ-DAC7-*' },
|
||
{ title: 'DAC7 status-dashboard live', owner: 'Legal', status: 'done', deadline: 'Juni 10', note: '/quixzoom/dac7-report.html + /dac7-reporting.html' },
|
||
{ title: 'Rapporteringsgräns 30 transaktioner / 2 000 EUR per år', owner: 'Legal', status: 'done', deadline: 'Juni 5', note: 'Art. 8ac DAC trösk implementerad i dac7.mjs:175' },
|
||
]
|
||
},
|
||
{
|
||
id: 'ios',
|
||
name: 'iOS Build',
|
||
icon: '',
|
||
color: '#5AC8FA',
|
||
items: [
|
||
{ title: 'Mac SSH-access för iOS builds löst', owner: 'Johan', status: 'done', deadline: 'Juni 1', note: 'Eskalerat → löst efter Sven-intervention' },
|
||
{ title: 'Xcode target iOS 17+ · kompilerar utan warnings', owner: 'iOS', status: 'done', deadline: 'Juni 1' },
|
||
{ title: 'TestFlight build uppladdad · extern beta aktiv', owner: 'iOS', status: 'done', deadline: 'Juni 5' },
|
||
{ title: '25+ externa beta-testare · feedback insamlad', owner: 'iOS', status: 'done', deadline: 'Juni 10' },
|
||
{ title: 'App Store submission (final build)', owner: 'iOS', status: 'in-prog', deadline: 'Juni 13', urgent: true },
|
||
{ title: 'App Review Notes inskickade till Apple', owner: 'iOS', status: 'in-prog', deadline: 'Juni 13', urgent: true },
|
||
]
|
||
},
|
||
{
|
||
id: 'appstore',
|
||
name: 'App Store',
|
||
icon: '',
|
||
color: '#007AFF',
|
||
items: [
|
||
{ title: 'App Store Connect-konto verifierat', owner: 'Ops', status: 'done', deadline: 'Maj 10' },
|
||
{ title: 'App-ikon 1024×1024 PNG godkänd', owner: 'Design', status: 'done', deadline: 'Maj 15' },
|
||
{ title: 'Screenshots alla device-storlekar klara', owner: 'Design', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'App-beskrivning SV + EN publicerad', owner: 'Mktg', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'ASO keyword-optimering genomförd', owner: 'Mktg', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Privacy Policy URL i App Store Connect', owner: 'Legal', status: 'done', deadline: 'Juni 5' },
|
||
]
|
||
},
|
||
{
|
||
id: 'legal',
|
||
name: 'Legal / Compliance',
|
||
icon: '',
|
||
color: '#FF3B30',
|
||
items: [
|
||
{ title: 'Privacy Policy + Terms of Service publicerade', owner: 'Legal', status: 'done', deadline: 'Maj 15' },
|
||
{ title: 'GDPR Data Deletion-flow verifierat', owner: 'Legal', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'KYC/AML-process granskad av extern jurist', owner: 'Legal', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Liability Framework (T545) konfigurerad', owner: 'Legal', status: 'done', deadline: 'Juni 5', note: 'Free €100 / Plus €500 / Pro 12mo / Enterprise €5M floor' },
|
||
{ title: 'Dispute resolution 3-steg (T546) aktiverat', owner: 'Legal', status: 'done', deadline: 'Juni 5' },
|
||
]
|
||
},
|
||
{
|
||
id: 'operations',
|
||
name: 'Operations',
|
||
icon: '',
|
||
color: '#FF9500',
|
||
items: [
|
||
{ title: 'Monitoring & alerting konfigurerat', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'ECS auto-scaling skalningsplan dokumenterad', owner: 'Ops', status: 'done', deadline: 'Maj 22' },
|
||
{ title: 'Backup & restore verifierat', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Support-kanal öppen (email + chat)', owner: 'Ops', status: 'done', deadline: 'Maj 20' },
|
||
{ title: 'Rate limiting iOS traffic konfigurerat', owner: 'Backend', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Runbook för vanliga incidents skriven', owner: 'Ops', status: 'in-prog', deadline: 'Juni 13' },
|
||
{ title: 'Post-launch review schemalagd (22 juni)', owner: 'Erik', status: 'pending', deadline: 'Juni 13' },
|
||
]
|
||
},
|
||
{
|
||
id: 'marketing',
|
||
name: 'Marketing',
|
||
icon: '',
|
||
color: '#34C759',
|
||
items: [
|
||
{ title: 'Landing page quixzoom.se live', owner: 'Mktg', status: 'done', deadline: 'Maj 20' },
|
||
{ title: 'Launch-kampanj planerad & schemalagd', owner: 'Mktg', status: 'done', deadline: 'Juni 5' },
|
||
{ title: 'Press release klar för distribution', owner: 'Mktg', status: 'done', deadline: 'Juni 10' },
|
||
{ title: 'Social media content-kalender klar', owner: 'Mktg', status: 'done', deadline: 'Juni 10' },
|
||
{ title: 'Influencer-samarbeten (5 st) bekräftade', owner: 'Mktg', status: 'in-prog', deadline: 'Juni 13' },
|
||
{ title: 'Launch-kampanj 1 augusti kl 09:00 aktiveras', owner: 'Mktg', status: 'pending', deadline: 'Juni 15', urgent: true },
|
||
]
|
||
},
|
||
];
|
||
|
||
// Helpers
|
||
function computeDaysLeft() {
|
||
const diff = LAUNCH - new Date();
|
||
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
||
}
|
||
|
||
function statusCircleClass(s) {
|
||
if (s === 'done') return 'done';
|
||
if (s === 'in-prog') return 'in-prog';
|
||
if (s === 'blocked') return 'blocked';
|
||
return 'pending';
|
||
}
|
||
function statusCircleContent(s) {
|
||
if (s === 'done') return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||
if (s === 'in-prog') return '→';
|
||
if (s === 'blocked') return '';
|
||
return '';
|
||
}
|
||
function statusTagClass(s) {
|
||
if (s === 'done') return 'tag-done';
|
||
if (s === 'in-prog') return 'tag-inprog';
|
||
if (s === 'blocked') return 'tag-blocked';
|
||
return 'tag-pending';
|
||
}
|
||
function statusTagLabel(s) {
|
||
if (s === 'done') return 'Klar';
|
||
if (s === 'in-prog') return 'Pågår';
|
||
if (s === 'blocked') return 'Blockad';
|
||
return 'Pending';
|
||
}
|
||
|
||
// Render
|
||
function render() {
|
||
let totalDone = 0, totalInprog = 0, totalBlocked = 0, totalPending = 0, totalItems = 0;
|
||
|
||
CATEGORIES.forEach(cat => {
|
||
cat.items.forEach(i => {
|
||
totalItems++;
|
||
if (i.status === 'done') totalDone++;
|
||
else if (i.status === 'in-prog') totalInprog++;
|
||
else if (i.status === 'blocked') totalBlocked++;
|
||
else totalPending++;
|
||
});
|
||
});
|
||
|
||
const pct = Math.round(totalDone / totalItems * 100);
|
||
const days = computeDaysLeft();
|
||
|
||
document.getElementById('heroPct').textContent = pct + '%';
|
||
document.getElementById('heroLabel').textContent = `${totalDone} av ${totalItems} items klara`;
|
||
document.getElementById('heroBar').style.width = pct + '%';
|
||
document.getElementById('totalItems').textContent = totalItems;
|
||
|
||
const daysEl = document.getElementById('daysLeft');
|
||
daysEl.textContent = days > 0 ? `${days} dagar kvar` : 'LAUNCH DAY <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>';
|
||
if (days <= 3) daysEl.style.color = 'var(--red)';
|
||
|
||
document.getElementById('cntDone').textContent = totalDone;
|
||
document.getElementById('cntInprog').textContent = totalInprog;
|
||
document.getElementById('cntBlocked').textContent = totalBlocked;
|
||
document.getElementById('cntPending').textContent = totalPending;
|
||
|
||
const badge = document.getElementById('navBadge');
|
||
badge.textContent = `${pct}% Klar`;
|
||
if (totalBlocked === 0 && pct >= 90) badge.classList.add('ready');
|
||
else if (totalBlocked > 0) badge.classList.add('warn');
|
||
|
||
const container = document.getElementById('checklistContainer');
|
||
container.innerHTML = CATEGORIES.map(cat => {
|
||
const catDone = cat.items.filter(i => i.status === 'done').length;
|
||
const catTotal = cat.items.length;
|
||
const catPct = Math.round(catDone / catTotal * 100);
|
||
|
||
const itemsHTML = cat.items.map(item => `
|
||
<div class="check-item">
|
||
<div class="check-circle ${statusCircleClass(item.status)}">${statusCircleContent(item.status)}</div>
|
||
<div class="check-content">
|
||
<div class="check-title ${item.status === 'done' ? 'line-through' : ''}">${item.title}</div>
|
||
${item.note ? `<div class="check-note">${item.note}</div>` : ''}
|
||
<div class="check-meta">
|
||
<span class="meta-chip">${item.owner}</span>
|
||
<span class="meta-deadline ${item.urgent ? 'urgent' : ''}">${item.deadline || ''}</span>
|
||
</div>
|
||
</div>
|
||
<div class="check-right">
|
||
<span class="status-tag ${statusTagClass(item.status)}">${statusTagLabel(item.status)}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
return `
|
||
<div class="cat-section">
|
||
<div class="cat-header">
|
||
<div class="cat-icon" style="background:${cat.color}22">${cat.icon}</div>
|
||
<span class="cat-name">${cat.name}</span>
|
||
<span class="cat-progress">${catDone}/${catTotal}</span>
|
||
</div>
|
||
<div class="cat-progress-bar">
|
||
<div class="cat-progress-fill" style="width:${catPct}%;background:${cat.color}"></div>
|
||
</div>
|
||
<div class="checklist">${itemsHTML}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
document.getElementById('footerTs').textContent = new Date().toLocaleString('sv-SE');
|
||
}
|
||
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|