48ea61cdcc
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
1025 lines
38 KiB
HTML
1025 lines
38 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>iOS Readiness — quiXzoom</title>
|
|
<style>
|
|
/* iOS 26 / Apple Native — Dark Mode */
|
|
:root {
|
|
--bg: #000000;
|
|
--bg-secondary: #1c1c1e;
|
|
--bg-tertiary: #2c2c2e;
|
|
--bg-grouped: #1c1c1e;
|
|
--separator: rgba(84,84,88,0.65);
|
|
--label: #ffffff;
|
|
--label-secondary: rgba(235,235,245,0.6);
|
|
--label-tertiary: rgba(235,235,245,0.3);
|
|
--tint: #007AFF;
|
|
--green: #30D158;
|
|
--red: #FF453A;
|
|
--yellow: #FFD60A;
|
|
--orange: #FF9F0A;
|
|
--purple: #BF5AF2;
|
|
--fill: rgba(118,118,128,0.24);
|
|
--fill-secondary: rgba(118,118,128,0.16);
|
|
--radius: 12px;
|
|
--radius-lg: 18px;
|
|
--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) + 40px);
|
|
overscroll-behavior-y: contain;
|
|
}
|
|
|
|
/* Nav Bar */
|
|
.nav-bar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
background: rgba(242,242,247,0.82);
|
|
-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-right { display: flex; align-items: center; gap: 10px; }
|
|
.nav-time { font-size: 13px; color: var(--label-secondary); font-variant-numeric: tabular-nums; }
|
|
.refresh-btn {
|
|
background: var(--fill);
|
|
border: none;
|
|
color: var(--tint);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
font-family: var(--font);
|
|
padding: 6px 12px;
|
|
border-radius: var(--radius-xl);
|
|
cursor: pointer;
|
|
transition: opacity 0.15s;
|
|
}
|
|
.refresh-btn:active { opacity: 0.6; }
|
|
|
|
/* Hero Verdict */
|
|
.verdict-hero {
|
|
margin: 20px 16px 0;
|
|
border-radius: var(--radius-lg);
|
|
padding: 24px 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: background 0.4s;
|
|
}
|
|
.verdict-hero.ready { background: rgba(48,209,88,0.15); border: 1px solid rgba(48,209,88,0.3); }
|
|
.verdict-hero.partial { background: rgba(255,214,10,0.12); border: 1px solid rgba(255,214,10,0.3); }
|
|
.verdict-hero.not-ready { background: rgba(255,69,58,0.12); border: 1px solid rgba(255,69,58,0.3); }
|
|
.verdict-hero.loading { background: var(--bg-secondary); border: 1px solid var(--separator); }
|
|
|
|
.verdict-icon { font-size: 52px; line-height: 1; flex-shrink: 0; }
|
|
.verdict-text {}
|
|
.verdict-question {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--label-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.verdict-answer {
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.5px;
|
|
line-height: 1.1;
|
|
}
|
|
.verdict-hero.ready .verdict-answer { color: var(--green); }
|
|
.verdict-hero.partial .verdict-answer { color: var(--yellow); }
|
|
.verdict-hero.not-ready .verdict-answer { color: var(--red); }
|
|
.verdict-hero.loading .verdict-answer { color: var(--label-secondary); }
|
|
.verdict-sub {
|
|
font-size: 14px;
|
|
color: var(--label-secondary);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Stat Grid */
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 10px;
|
|
padding: 16px 16px 4px;
|
|
}
|
|
.stat-card {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius);
|
|
padding: 14px 10px;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.stat-card::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0;
|
|
height: 2px;
|
|
}
|
|
.stat-card.green::after { background: var(--green); }
|
|
.stat-card.red::after { background: var(--red); }
|
|
.stat-card.yellow::after { background: var(--yellow); }
|
|
.stat-card.blue::after { background: var(--tint); }
|
|
.stat-num {
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: -0.5px;
|
|
line-height: 1;
|
|
margin-bottom: 4px;
|
|
}
|
|
.stat-card.green .stat-num { color: var(--green); }
|
|
.stat-card.red .stat-num { color: var(--red); }
|
|
.stat-card.yellow .stat-num { color: var(--yellow); }
|
|
.stat-card.blue .stat-num { color: var(--tint); }
|
|
.stat-lbl {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
color: var(--label-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
/* Section */
|
|
.section { margin-top: 28px; }
|
|
.section-hdr {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 20px 8px;
|
|
}
|
|
.section-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.6px;
|
|
color: var(--label-secondary);
|
|
}
|
|
.section-badge {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--label-tertiary);
|
|
}
|
|
|
|
/* List */
|
|
.list {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius-lg);
|
|
margin: 0 16px;
|
|
overflow: hidden;
|
|
}
|
|
.list-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 13px 16px;
|
|
border-bottom: 0.5px solid var(--separator);
|
|
}
|
|
.list-row:last-child { border-bottom: none; }
|
|
|
|
.row-icon {
|
|
width: 32px; height: 32px;
|
|
border-radius: var(--radius-md);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
.row-icon.green { background: rgba(48,209,88,0.18); }
|
|
.row-icon.red { background: rgba(255,69,58,0.18); }
|
|
.row-icon.yellow { background: rgba(255,214,10,0.18); }
|
|
.row-icon.blue { background: rgba(0,122,255,0.18); }
|
|
.row-icon.grey { background: var(--fill); }
|
|
|
|
.row-body { flex: 1; min-width: 0; }
|
|
.row-title {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: var(--label);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.row-sub {
|
|
font-size: 13px;
|
|
color: var(--label-secondary);
|
|
margin-top: 1px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.row-right { flex-shrink: 0; text-align: right; }
|
|
.status-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius-xl);
|
|
}
|
|
.pill-green { background: rgba(48,209,88,0.18); color: var(--green); }
|
|
.pill-red { background: rgba(255,69,58,0.18); color: var(--red); }
|
|
.pill-yellow { background: rgba(255,214,10,0.18); color: var(--yellow); }
|
|
.pill-blue { background: rgba(0,122,255,0.18); color: var(--tint); }
|
|
.pill-grey { background: var(--fill); color: var(--label-secondary); }
|
|
|
|
.dot { width: 7px; height: 7px; border-radius: var(--radius-full); }
|
|
.dot-green { background: var(--green); }
|
|
.dot-red { background: var(--red); }
|
|
.dot-yellow { background: var(--yellow); }
|
|
.dot-grey { background: var(--label-tertiary); }
|
|
|
|
/* Progress Bar */
|
|
.progress-wrap { padding: 0 16px; }
|
|
.progress-bar-bg {
|
|
background: var(--fill);
|
|
border-radius: var(--radius-sm);
|
|
height: 8px;
|
|
overflow: hidden;
|
|
margin-top: 8px;
|
|
}
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
border-radius: var(--radius-sm);
|
|
transition: width 0.6s ease;
|
|
background: var(--green);
|
|
}
|
|
.progress-label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 13px;
|
|
color: var(--label-secondary);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
/* Blocker */
|
|
.blocker-card {
|
|
background: rgba(255,69,58,0.08);
|
|
border: 1px solid rgba(255,69,58,0.25);
|
|
border-radius: var(--radius);
|
|
margin: 0 16px;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
}
|
|
.blocker-card + .blocker-card { margin-top: 10px; }
|
|
.blocker-icon { font-size: 20px; flex-shrink: 0; margin-top: 1px; }
|
|
.blocker-body {}
|
|
.blocker-title { font-size: 15px; font-weight: 600; color: var(--red); }
|
|
.blocker-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
|
|
.blocker-owner { font-size: 12px; color: var(--label-tertiary); margin-top: 4px; }
|
|
|
|
/* Warning */
|
|
.warning-card {
|
|
background: rgba(255,214,10,0.08);
|
|
border: 1px solid rgba(255,214,10,0.25);
|
|
border-radius: var(--radius);
|
|
margin: 0 16px;
|
|
padding: 14px 16px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-start;
|
|
}
|
|
.warning-card + .warning-card { margin-top: 10px; }
|
|
.warning-icon { font-size: 20px; flex-shrink: 0; }
|
|
.warning-body {}
|
|
.warning-title { font-size: 15px; font-weight: 600; color: var(--yellow); }
|
|
.warning-desc { font-size: 13px; color: var(--label-secondary); margin-top: 3px; }
|
|
|
|
/* Deploy checklist */
|
|
.check-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-bottom: 0.5px solid var(--separator);
|
|
}
|
|
.check-item:last-child { border-bottom: none; }
|
|
.check-box {
|
|
width: 22px; height: 22px;
|
|
border-radius: var(--radius-full);
|
|
border: 2px solid var(--separator);
|
|
flex-shrink: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 13px;
|
|
margin-top: 1px;
|
|
}
|
|
.check-box.done { background: var(--green); border-color: var(--green); color: #000; }
|
|
.check-box.warn { background: var(--yellow); border-color: var(--yellow); color: #000; }
|
|
.check-box.fail { background: var(--red); border-color: var(--red); color: #fff; }
|
|
.check-box.pending { border-color: var(--label-tertiary); }
|
|
.check-body { flex: 1; }
|
|
.check-title { font-size: 15px; font-weight: 500; color: var(--label); }
|
|
.check-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
|
|
|
/* Footer */
|
|
.footer {
|
|
text-align: center;
|
|
padding: 32px 20px 12px;
|
|
font-size: 12px;
|
|
color: var(--label-tertiary);
|
|
}
|
|
|
|
/* Spinner */
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.spinner {
|
|
width: 16px; height: 16px;
|
|
border: 2px solid var(--fill);
|
|
border-top-color: var(--tint);
|
|
border-radius: var(--radius-full);
|
|
animation: spin 0.8s linear infinite;
|
|
display: inline-block;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
.pulsing { animation: pulse 1.5s ease-in-out infinite; }
|
|
@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>
|
|
<script type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
|
|
</head>
|
|
<body>
|
|
<h1 style="position:absolute;left:-9999px;">iOS Readiness Dashboard</h1>
|
|
|
|
<!-- Nav Bar -->
|
|
<div class="nav-bar">
|
|
<div class="nav-title"> iOS Readiness</div>
|
|
<div class="nav-right">
|
|
<span class="nav-time" id="navTime">—</span>
|
|
<button class="refresh-btn" onclick="runCheck()">↻ Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hero Verdict -->
|
|
<div class="verdict-hero loading" id="verdictHero">
|
|
<div class="verdict-icon pulsing" id="verdictIcon">⏳</div>
|
|
<div class="verdict-text">
|
|
<div class="verdict-question">Är backend redo för iOS-release?</div>
|
|
<div class="verdict-answer" id="verdictAnswer">Kontrollerar…</div>
|
|
<div class="verdict-sub" id="verdictSub">Hämtar status från alla tjänster</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stat Grid -->
|
|
<div class="stat-grid">
|
|
<div class="stat-card green">
|
|
<div class="stat-num" id="statOk">—</div>
|
|
<div class="stat-lbl">API OK</div>
|
|
</div>
|
|
<div class="stat-card red">
|
|
<div class="stat-num" id="statFail">—</div>
|
|
<div class="stat-lbl">API Fel</div>
|
|
</div>
|
|
<div class="stat-card yellow">
|
|
<div class="stat-num" id="statBlockers">—</div>
|
|
<div class="stat-lbl">Blockers</div>
|
|
</div>
|
|
<div class="stat-card blue">
|
|
<div class="stat-num" id="statDeploy">—</div>
|
|
<div class="stat-lbl">Deploy %</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Health Section -->
|
|
<div class="section">
|
|
<div class="section-hdr">
|
|
<span class="section-title">API Health</span>
|
|
<span class="section-badge" id="apiSummary">Kontrollerar…</span>
|
|
</div>
|
|
<div class="list" id="apiList">
|
|
<div class="list-row">
|
|
<div class="row-icon grey"><span class="spinner"></span></div>
|
|
<div class="row-body"><div class="row-title">Hämtar endpoints…</div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backend Features -->
|
|
<div class="section">
|
|
<div class="section-hdr">
|
|
<span class="section-title">Backend Features</span>
|
|
<span class="section-badge">iOS critical</span>
|
|
</div>
|
|
<div class="list" id="featureList">
|
|
<!-- filled by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Blockers -->
|
|
<div class="section">
|
|
<div class="section-hdr">
|
|
<span class="section-title"> Kända blockers</span>
|
|
<span class="section-badge" id="blockerCount">—</span>
|
|
</div>
|
|
<div id="blockerList"></div>
|
|
</div>
|
|
|
|
<!-- Warnings -->
|
|
<div class="section">
|
|
<div class="section-hdr">
|
|
<span class="section-title"><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> Varningar</span>
|
|
<span class="section-badge" id="warningCount">—</span>
|
|
</div>
|
|
<div id="warningList"></div>
|
|
</div>
|
|
|
|
<!-- Deployment Checklist -->
|
|
<div class="section">
|
|
<div class="section-hdr">
|
|
<span class="section-title">Deployment Checklist</span>
|
|
<span class="section-badge" id="deployProgress">—</span>
|
|
</div>
|
|
<div class="progress-wrap">
|
|
<div class="progress-bar-bg">
|
|
<div class="progress-bar-fill" id="deployBar" style="width:0%"></div>
|
|
</div>
|
|
<div class="progress-label">
|
|
<span>Framsteg mot June 5</span>
|
|
<span id="deployPct">0%</span>
|
|
</div>
|
|
</div>
|
|
<div style="height:12px"></div>
|
|
<div class="list" id="deployList"></div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
quiXzoom iOS Readiness Dashboard · <span id="footerTime">—</span>
|
|
</div>
|
|
|
|
<script>
|
|
// Data: Backend Features
|
|
const FEATURES = [
|
|
{ name: 'BankID Authentication', icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg>', status: 'done', note: 'Staging verifierat' },
|
|
{ name: 'Zoomer Registration', icon: '', status: 'done', note: 'POST /api/quixzoom/zoomers' },
|
|
{ name: 'Mission CRUD', icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', status: 'done', note: 'Full create/read/update' },
|
|
{ name: 'Geolocation Matching', icon: '', status: 'done', note: 'Radius-sökning aktiv' },
|
|
{ name: 'Real-time Push (APNs)', icon: '', status: 'warn', note: 'APNs cert upphör aug 2026' },
|
|
{ name: 'Earnings & Payouts', icon: '', status: 'done', note: 'Beräkningar OK, utbetalning pending' },
|
|
{ name: 'KYC Document Upload', icon: '', status: 'done', note: 'S3 + validering OK' },
|
|
{ name: 'Rating & Reviews', icon: '', status: 'done', note: 'Bi-directional ratings' },
|
|
{ name: 'Portfolio / Media', icon: '', status: 'done', note: 'Upload + CDN aktiv' },
|
|
{ name: 'Stripe Payouts', icon: '', status: 'warn', note: 'Testläge — ej prod-nyckel' },
|
|
{ name: 'DAC7 Reporting', icon: '', status: 'done', note: 'Automatisk generering OK' },
|
|
{ name: 'GDPR / Data Deletion', icon: '', status: 'pending', note: 'Legal review pågår' },
|
|
];
|
|
|
|
// Data: Blockers
|
|
const BLOCKERS = [
|
|
{
|
|
icon: '',
|
|
title: 'Stripe Production Keys saknas',
|
|
desc: 'Utbetalningar kan ej köras i prod-miljö utan live Stripe-nycklar.',
|
|
owner: 'Ägare: Erik / Ops · Deadline: June 1'
|
|
},
|
|
{
|
|
icon: '',
|
|
title: 'APNs Production Certificate',
|
|
desc: 'Push-notiser till iOS kräver prod APNs-certifikat i staging-testet.',
|
|
owner: 'Ägare: iOS team · Deadline: May 30'
|
|
},
|
|
{
|
|
icon: '<span class="flag-se">SE</span>',
|
|
title: 'BankID Prod-avtal ej signerat',
|
|
desc: 'Testmiljö körs mot Sandbox. Prod-avtal med Finansiell ID-Teknik AB krävs.',
|
|
owner: 'Ägare: Erik · Deadline: May 28'
|
|
},
|
|
];
|
|
|
|
// Data: Warnings
|
|
const WARNINGS = [
|
|
{
|
|
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg>',
|
|
title: 'Rate limits ej konfigurerade för iOS',
|
|
desc: 'API-rate-limiting behöver tunas för mobila request-mönster.'
|
|
},
|
|
{
|
|
icon: '',
|
|
title: 'App Store metadata ej uppladdad',
|
|
desc: 'Screenshots, beskrivning och metadata saknas i App Store Connect.'
|
|
},
|
|
{
|
|
icon: '',
|
|
title: 'SSL-cert auto-renewal ej verifierat',
|
|
desc: 'Kontrollera att certbot renew fungerar för amos.wavult.com.'
|
|
},
|
|
];
|
|
|
|
// Data: Deploy Checklist
|
|
const DEPLOY_ITEMS = [
|
|
{ title: 'API endpoints testade mot iOS simulator', sub: 'Backend', status: 'done' },
|
|
{ title: 'BankID Sandbox-integration verifierad', sub: 'Backend', status: 'done' },
|
|
{ title: 'Zoomer onboarding flow live', sub: 'Backend', status: 'done' },
|
|
{ title: 'Mission create/accept flow live', sub: 'Backend', status: 'done' },
|
|
{ title: 'Earnings beräkning verifierad', sub: 'Backend', status: 'done' },
|
|
{ title: 'KYC upload + validering live', sub: 'Backend', status: 'done' },
|
|
{ title: 'BankID Production-avtal signerat', sub: 'Legal', status: 'fail' },
|
|
{ title: 'Stripe Production Keys konfigurerade', sub: 'Ops', status: 'fail' },
|
|
{ title: 'APNs Production Certificate installerat', sub: 'iOS', status: 'fail' },
|
|
{ title: 'Load test körd (1000 concurrent users)', sub: 'QA', status: 'pending' },
|
|
{ title: 'App Store metadata uppladdad', sub: 'Mktg', status: 'pending' },
|
|
{ title: 'TestFlight externa testers (25 st)', sub: 'iOS', status: 'pending' },
|
|
{ title: 'Privacy Policy publicerad', sub: 'Legal', status: 'done' },
|
|
{ title: 'Terms of Service publicerad', sub: 'Legal', status: 'done' },
|
|
{ title: 'GDPR Data Deletion verifierad', sub: 'Legal', status: 'pending' },
|
|
];
|
|
|
|
// Endpoints att kontrollera
|
|
const ENDPOINTS = [
|
|
{ label: 'Zoomer List', method: 'GET', url: 'https://api.quixzoom.com/api/qz/zoomers', critical: true },
|
|
{ label: 'Zoomer Profile', method: 'GET', url: 'https://api.quixzoom.com/api/qz/zoomers/zoomer-001/profile', critical: true },
|
|
{ label: 'Mission List', method: 'GET', url: 'https://api.quixzoom.com/api/qz/missions', critical: true },
|
|
{ label: 'Mission Stats', method: 'GET', url: 'https://api.quixzoom.com/api/qz/missions/stats', critical: false },
|
|
{ label: 'Zoomer Earnings', method: 'GET', url: 'https://api.quixzoom.com/api/qz/zoomers/zoomer-001/earnings?period=month', critical: true },
|
|
{ label: 'Zoomer Portfolio', method: 'GET', url: 'https://api.quixzoom.com/api/qz/zoomers/zoomer-001/portfolio', critical: false },
|
|
{ label: 'Health Check', method: 'GET', url: '/api/health', critical: true },
|
|
];
|
|
|
|
// State
|
|
let results = {};
|
|
|
|
// Render helpers
|
|
function statusClass(s) {
|
|
return s === 'done' ? 'green' : s === 'warn' ? 'yellow' : s === 'fail' ? 'red' : 'grey';
|
|
}
|
|
function statusIcon(s) {
|
|
return s === 'done' ? '<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>' : s === 'warn' ? '!' : s === 'fail' ? '' : '';
|
|
}
|
|
function statusPill(s, label) {
|
|
const cls = s === 'done' ? 'pill-green' : s === 'warn' ? 'pill-yellow' : s === 'fail' ? 'pill-red' : 'pill-grey';
|
|
const dot = `<span class="dot dot-${statusClass(s)}"></span>`;
|
|
return `<span class="status-pill ${cls}">${dot} ${label}</span>`;
|
|
}
|
|
|
|
// Render static sections
|
|
function renderFeatures() {
|
|
const el = document.getElementById('featureList');
|
|
el.innerHTML = FEATURES.map(f => `
|
|
<div class="list-row">
|
|
<div class="row-icon ${statusClass(f.status)}">${f.icon}</div>
|
|
<div class="row-body">
|
|
<div class="row-title">${f.name}</div>
|
|
<div class="row-sub">${f.note}</div>
|
|
</div>
|
|
<div class="row-right">
|
|
${statusPill(f.status, f.status === 'done' ? 'Klar' : f.status === 'warn' ? 'Varning' : 'Pending')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderBlockers() {
|
|
const el = document.getElementById('blockerList');
|
|
document.getElementById('blockerCount').textContent = BLOCKERS.length + ' aktiva';
|
|
el.innerHTML = BLOCKERS.map(b => `
|
|
<div class="blocker-card">
|
|
<div class="blocker-icon">${b.icon}</div>
|
|
<div class="blocker-body">
|
|
<div class="blocker-title">${b.title}</div>
|
|
<div class="blocker-desc">${b.desc}</div>
|
|
<div class="blocker-owner">${b.owner}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderWarnings() {
|
|
const el = document.getElementById('warningList');
|
|
document.getElementById('warningCount').textContent = WARNINGS.length + ' st';
|
|
el.innerHTML = WARNINGS.map(w => `
|
|
<div class="warning-card">
|
|
<div class="warning-icon">${w.icon}</div>
|
|
<div class="warning-body">
|
|
<div class="warning-title">${w.title}</div>
|
|
<div class="warning-desc">${w.desc}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderDeploy() {
|
|
const done = DEPLOY_ITEMS.filter(i => i.status === 'done').length;
|
|
const total = DEPLOY_ITEMS.length;
|
|
const pct = Math.round(done / total * 100);
|
|
|
|
document.getElementById('deployProgress').textContent = `${done}/${total} klara`;
|
|
document.getElementById('deployPct').textContent = `${pct}%`;
|
|
document.getElementById('deployBar').style.width = pct + '%';
|
|
document.getElementById('statDeploy').textContent = pct + '%';
|
|
|
|
const el = document.getElementById('deployList');
|
|
el.innerHTML = DEPLOY_ITEMS.map(item => `
|
|
<div class="check-item">
|
|
<div class="check-box ${item.status === 'done' ? 'done' : item.status === 'fail' ? 'fail' : item.status === 'warn' ? 'warn' : 'pending'}">
|
|
${item.status === 'done' ? '<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>' : item.status === 'fail' ? '' : item.status === 'warn' ? '!' : ''}
|
|
</div>
|
|
<div class="check-body">
|
|
<div class="check-title">${item.title}</div>
|
|
<div class="check-sub">${item.sub}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Check an endpoint
|
|
async function checkEndpoint(ep) {
|
|
const t0 = Date.now();
|
|
try {
|
|
const r = await fetch(ep.url, { method: ep.method, signal: AbortSignal.timeout(5000) });
|
|
return { ok: r.ok, status: r.status, ms: Date.now() - t0 };
|
|
} catch (e) {
|
|
return { ok: false, status: 0, ms: Date.now() - t0, error: e.message };
|
|
}
|
|
}
|
|
|
|
// Main check
|
|
async function runCheck() {
|
|
// Reset
|
|
document.getElementById('verdictHero').className = 'verdict-hero loading';
|
|
document.getElementById('verdictIcon').textContent = '⏳';
|
|
document.getElementById('verdictIcon').classList.add('pulsing');
|
|
document.getElementById('verdictAnswer').textContent = 'Kontrollerar…';
|
|
document.getElementById('verdictSub').textContent = 'Hämtar status från alla tjänster';
|
|
document.getElementById('statOk').textContent = '—';
|
|
document.getElementById('statFail').textContent = '—';
|
|
document.getElementById('statBlockers').textContent = '—';
|
|
document.getElementById('apiSummary').textContent = 'Kontrollerar…';
|
|
|
|
// Render API list with spinners
|
|
const apiList = document.getElementById('apiList');
|
|
apiList.innerHTML = ENDPOINTS.map((ep, i) => `
|
|
<div class="list-row" id="ep-row-${i}">
|
|
<div class="row-icon grey" id="ep-icon-${i}"><span class="spinner"></span></div>
|
|
<div class="row-body">
|
|
<div class="row-title">${ep.label} <span style="font-size:11px;color:var(--label-tertiary);font-family:monospace">${ep.method} ${ep.url}</span></div>
|
|
<div class="row-sub" id="ep-sub-${i}">Kontrollerar…</div>
|
|
</div>
|
|
<div class="row-right" id="ep-right-${i}"><span class="status-pill pill-grey"><span class="dot dot-grey"></span> —</span></div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Check all
|
|
const checks = await Promise.all(ENDPOINTS.map(checkEndpoint));
|
|
let okCount = 0, failCount = 0;
|
|
|
|
checks.forEach((res, i) => {
|
|
const ep = ENDPOINTS[i];
|
|
const iconEl = document.getElementById(`ep-icon-${i}`);
|
|
const subEl = document.getElementById(`ep-sub-${i}`);
|
|
const rightEl = document.getElementById(`ep-right-${i}`);
|
|
|
|
if (res.ok) {
|
|
okCount++;
|
|
const color = ep.critical ? 'green' : 'blue';
|
|
iconEl.className = `row-icon ${color}`;
|
|
iconEl.textContent = '<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>';
|
|
subEl.textContent = `HTTP ${res.status} · ${res.ms}ms`;
|
|
rightEl.innerHTML = `<span class="status-pill pill-green"><span class="dot dot-green"></span> OK</span>`;
|
|
} else {
|
|
failCount++;
|
|
iconEl.className = 'row-icon red';
|
|
iconEl.textContent = '';
|
|
subEl.textContent = res.error ? `Error: ${res.error}` : `HTTP ${res.status} · ${res.ms}ms`;
|
|
rightEl.innerHTML = `<span class="status-pill pill-red"><span class="dot dot-red"></span> FEL</span>`;
|
|
}
|
|
});
|
|
|
|
document.getElementById('statOk').textContent = okCount;
|
|
document.getElementById('statFail').textContent = failCount;
|
|
document.getElementById('statBlockers').textContent = BLOCKERS.length;
|
|
document.getElementById('apiSummary').textContent = `${okCount}/${ENDPOINTS.length} OK`;
|
|
|
|
// Verdict
|
|
const hero = document.getElementById('verdictHero');
|
|
const icon = document.getElementById('verdictIcon');
|
|
const ans = document.getElementById('verdictAnswer');
|
|
const sub = document.getElementById('verdictSub');
|
|
icon.classList.remove('pulsing');
|
|
|
|
const criticalFailing = ENDPOINTS.filter((ep, i) => ep.critical && !checks[i].ok).length;
|
|
const hardBlockers = BLOCKERS.length;
|
|
|
|
if (failCount === 0 && hardBlockers === 0) {
|
|
hero.className = 'verdict-hero ready';
|
|
icon.textContent = '<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>';
|
|
ans.textContent = 'JA — Backend redo!';
|
|
sub.textContent = `Alla ${ENDPOINTS.length} API:er OK · Inga blockers · Klar för iOS`;
|
|
} else if (criticalFailing > 0 || hardBlockers >= 2) {
|
|
hero.className = 'verdict-hero not-ready';
|
|
icon.textContent = '';
|
|
ans.textContent = 'NEJ — Ej release-redo';
|
|
sub.textContent = `${criticalFailing} kritiska API-fel · ${hardBlockers} blockers kvar`;
|
|
} else {
|
|
hero.className = 'verdict-hero partial';
|
|
icon.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>';
|
|
ans.textContent = 'DELVIS — Åtgärder krävs';
|
|
sub.textContent = `API:er OK · ${hardBlockers} blockers kvar att lösa`;
|
|
}
|
|
|
|
updateTime();
|
|
}
|
|
|
|
function updateTime() {
|
|
const now = new Date();
|
|
const str = now.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
document.getElementById('navTime').textContent = str;
|
|
document.getElementById('footerTime').textContent = now.toLocaleString('sv-SE');
|
|
}
|
|
|
|
// Init
|
|
renderFeatures();
|
|
renderBlockers();
|
|
renderWarnings();
|
|
renderDeploy();
|
|
runCheck();
|
|
|
|
// Auto-refresh every 60s
|
|
setInterval(runCheck, 60_000);
|
|
// Clock tick
|
|
setInterval(() => {
|
|
const str = new Date().toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
document.getElementById('navTime').textContent = str;
|
|
}, 1000);
|
|
</script>
|
|
<!-- quiXzoom Mina Sidor - Universal Header Component -->
|
|
<!-- Injicera detta i <head> på alla quiXzoom-sajter -->
|
|
|
|
<style>
|
|
.qz-auth-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 48px;
|
|
background: #0A0A0A;
|
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 20px;
|
|
z-index: 9999;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
.qz-auth-bar .qz-logo {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
text-decoration: none;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
.qz-auth-bar .qz-logo span {
|
|
color: #007AFF;
|
|
}
|
|
.qz-auth-bar .qz-nav {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
.qz-auth-bar .qz-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
text-decoration: none;
|
|
}
|
|
.qz-auth-bar .qz-btn-primary {
|
|
background: #007AFF;
|
|
color: white;
|
|
}
|
|
.qz-auth-bar .qz-btn-primary:hover {
|
|
background: #0056CC;
|
|
}
|
|
.qz-auth-bar .qz-btn-ghost {
|
|
background: transparent;
|
|
color: rgba(255,255,255,0.8);
|
|
}
|
|
.qz-auth-bar .qz-btn-ghost:hover {
|
|
background: rgba(255,255,255,0.1);
|
|
color: white;
|
|
}
|
|
.qz-auth-bar .qz-user {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: white;
|
|
font-size: 14px;
|
|
}
|
|
.qz-auth-bar .qz-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #007AFF;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
.qz-auth-bar .qz-dropdown {
|
|
position: relative;
|
|
}
|
|
.qz-auth-bar .qz-dropdown-menu {
|
|
position: absolute;
|
|
top: 40px;
|
|
right: 0;
|
|
background: #1C1C1E;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
border-radius: 12px;
|
|
padding: 8px;
|
|
min-width: 200px;
|
|
display: none;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
|
}
|
|
.qz-auth-bar .qz-dropdown-menu.active {
|
|
display: block;
|
|
}
|
|
.qz-auth-bar .qz-dropdown-item {
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
color: white;
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 14px;
|
|
transition: background 0.2s;
|
|
}
|
|
.qz-auth-bar .qz-dropdown-item:hover {
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
.qz-auth-bar .qz-dropdown-divider {
|
|
height: 1px;
|
|
background: rgba(255,255,255,0.1);
|
|
margin: 8px 0;
|
|
}
|
|
.qz-auth-bar .qz-balance {
|
|
background: rgba(0,122,255,0.15);
|
|
color: #007AFF;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
@media (max-width: 640px) {
|
|
.qz-auth-bar .qz-nav .qz-btn-ghost:not(.qz-user) {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div id="qz-auth-bar" class="qz-auth-bar">
|
|
<a href="https://quixzoom.com" class="qz-logo">qui<span>X</span>zoom</a>
|
|
<div class="qz-nav">
|
|
<div id="qz-auth-guest">
|
|
<a href="https://app.quixzoom.com/login" class="qz-btn qz-btn-ghost">Logga in</a>
|
|
<a href="https://app.quixzoom.com/register" class="qz-btn qz-btn-primary">Bli Zoomer</a>
|
|
</div>
|
|
<div id="qz-auth-user" style="display:none;">
|
|
<span class="qz-balance" id="qz-balance">$0.00</span>
|
|
<div class="qz-dropdown">
|
|
<button class="qz-btn qz-btn-ghost qz-user" onclick="qzToggleDropdown()">
|
|
<div class="qz-avatar" id="qz-avatar">E</div>
|
|
<span id="qz-user-name">Erik</span>
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
<div class="qz-dropdown-menu" id="qz-dropdown-menu">
|
|
<a href="https://app.quixzoom.com/dashboard" class="qz-dropdown-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
|
|
Dashboard
|
|
</a>
|
|
<a href="https://app.quixzoom.com/wallet" class="qz-dropdown-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/></svg>
|
|
Plånbok
|
|
</a>
|
|
<a href="https://app.quixzoom.com/missions" class="qz-dropdown-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
Mina uppdrag
|
|
</a>
|
|
<div class="qz-dropdown-divider"></div>
|
|
<a href="https://app.quixzoom.com/settings" class="qz-dropdown-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.67 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.67 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.67a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
|
Inställningar
|
|
</a>
|
|
<a href="#" onclick="qzLogout(); return false;" class="qz-dropdown-item">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
|
Logga ut
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const SSO_BASE = 'https://auth.quixzoom.com';
|
|
const APP_BASE = 'https://app.quixzoom.com';
|
|
|
|
// Check auth status
|
|
async function qzCheckAuth() {
|
|
try {
|
|
const res = await fetch(SSO_BASE + '/auth/check', {
|
|
credentials: 'include',
|
|
});
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
if (data.authenticated) {
|
|
document.getElementById('qz-auth-guest').style.display = 'none';
|
|
document.getElementById('qz-auth-user').style.display = 'flex';
|
|
document.getElementById('qz-user-name').textContent = data.user.name || data.user.email;
|
|
document.getElementById('qz-avatar').textContent = (data.user.name || data.user.email)[0].toUpperCase();
|
|
if (data.user.balance !== undefined) {
|
|
document.getElementById('qz-balance').textContent = '$' + data.user.balance.toFixed(2);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Silent fail - show guest state
|
|
}
|
|
}
|
|
|
|
// Toggle dropdown
|
|
window.qzToggleDropdown = function() {
|
|
document.getElementById('qz-dropdown-menu').classList.toggle('active');
|
|
};
|
|
|
|
// Close dropdown on outside click
|
|
document.addEventListener('click', function(e) {
|
|
if (!e.target.closest('.qz-dropdown')) {
|
|
document.getElementById('qz-dropdown-menu').classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Logout
|
|
window.qzLogout = async function() {
|
|
try {
|
|
await fetch(SSO_BASE + '/auth/logout', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
} catch (e) {}
|
|
document.cookie = 'qz_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
|
|
document.cookie = 'qz_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
|
|
window.location.reload();
|
|
};
|
|
|
|
// Init
|
|
qzCheckAuth();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|