Files
boc/quixzoom-landing-fixed/api-status.html
T
Bernt 1a12fb870b dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases
- OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics
- Case study: Glasskiosken i Flatenbadet — complete ROI analysis
- Updated /order/ with recurring missions and frequency dropdown
2026-07-14 15:27:33 +00:00

1855 lines
66 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>quiXzoom API Status</title>
<style>
/* iOS 26 / Apple Native Design System */
: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;
--font-rounded: -apple-system, "SF Pro Rounded", 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) + 16px);
padding-bottom: calc(var(--safe-bottom) + 32px);
overscroll-behavior-y: contain;
}
/* Navigation 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: 12px 20px 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-bar h1 {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.4px;
color: var(--label);
}
.nav-bar .nav-badge {
font-size: 13px;
font-weight: 500;
color: var(--label-secondary);
}
/* Large Title */
.page-header {
padding: 24px 20px 8px;
}
.page-title {
font-size: 34px;
font-weight: 700;
letter-spacing: -0.5px;
line-height: 1.1;
color: var(--label);
}
.page-subtitle {
font-size: 15px;
color: var(--label-secondary);
margin-top: 4px;
}
/* Summary Cards */
.summary-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
padding: 16px 20px 4px;
}
.stat-card {
background: var(--bg-secondary);
border-radius: var(--radius);
padding: 14px 12px;
text-align: center;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
border-radius: var(--radius-sm) 2px 0 0;
}
.stat-card.green::before { background: var(--green); }
.stat-card.red::before { background: var(--red); }
.stat-card.yellow::before{ background: var(--yellow); }
.stat-number {
font-size: 28px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: -0.5px;
line-height: 1;
margin-bottom: 4px;
}
.stat-card.green .stat-number { color: var(--green); }
.stat-card.red .stat-number { color: var(--red); }
.stat-card.yellow .stat-number{ color: var(--yellow); }
.stat-label {
font-size: 11px;
font-weight: 500;
color: var(--label-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
}
/* Section */
.section {
margin-top: 28px;
}
.section-header {
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-action {
font-size: 13px;
font-weight: 500;
color: var(--tint);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.section-action:active { opacity: 0.6; }
/* Endpoint List */
.endpoint-list {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
margin: 0 16px;
overflow: hidden;
}
.endpoint-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-bottom: 0.5px solid var(--separator);
transition: background 0.15s;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
}
.endpoint-item:last-child { border-bottom: none; }
.endpoint-item:active { background: var(--fill); }
.method-badge {
flex-shrink: 0;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.4px;
padding: 3px 7px;
border-radius: var(--radius-sm);
font-family: "SF Mono", "Menlo", monospace;
}
.method-GET { background: rgba(48,209,88,0.2); color: var(--green); }
.method-POST { background: rgba(0,122,255,0.2); color: var(--tint); }
.method-PATCH { background: rgba(255,159,10,0.2); color: var(--orange); }
.method-DELETE { background: rgba(255,69,58,0.2); color: var(--red); }
.endpoint-info {
flex: 1;
min-width: 0;
}
.endpoint-path {
font-size: 13px;
font-weight: 500;
color: var(--label);
font-family: "SF Mono", "Menlo", monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.endpoint-desc {
font-size: 12px;
color: var(--label-secondary);
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.endpoint-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 3px;
flex-shrink: 0;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.dot-green { background: var(--green); box-shadow: 0 0 6px rgba(48,209,88,0.5); }
.dot-red { background: var(--red); box-shadow: 0 0 6px rgba(255,69,58,0.5); }
.dot-yellow { background: var(--yellow);}
.dot-grey { background: var(--label-tertiary); }
.resp-time {
font-size: 11px;
font-weight: 500;
color: var(--label-secondary);
font-variant-numeric: tabular-nums;
}
.resp-time.fast { color: var(--green); }
.resp-time.slow { color: var(--orange); }
/* Test Button */
.test-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--tint);
color: #fff;
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 600;
padding: 6px 12px;
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
font-family: var(--font);
-webkit-user-select: none;
user-select: none;
}
.test-btn:active {
opacity: 0.7;
transform: scale(0.97);
}
.test-btn.testing {
background: var(--bg-tertiary);
color: var(--label-secondary);
pointer-events: none;
}
/* Detail Row (expandable) */
.endpoint-detail {
display: none;
background: var(--bg-tertiary);
border-radius: 0;
padding: 12px 16px;
border-top: 0.5px solid var(--separator);
}
.endpoint-detail.open { display: block; }
.detail-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 3px 0;
}
.detail-key {
font-size: 12px;
color: var(--label-tertiary);
font-family: "SF Mono", monospace;
}
.detail-val {
font-size: 12px;
color: var(--label-secondary);
font-family: "SF Mono", monospace;
text-align: right;
max-width: 60%;
word-break: break-all;
}
/* Run All Button */
.run-all-container {
padding: 20px 16px 8px;
}
.run-all-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--tint);
color: #fff;
border: none;
border-radius: var(--radius);
font-size: 17px;
font-weight: 600;
padding: 16px;
cursor: pointer;
font-family: var(--font);
transition: opacity 0.15s, transform 0.1s;
letter-spacing: -0.3px;
}
.run-all-btn:active {
opacity: 0.8;
transform: scale(0.98);
}
.run-all-btn:disabled {
background: var(--bg-tertiary);
color: var(--label-secondary);
pointer-events: none;
}
/* Last Run */
.last-run {
text-align: center;
font-size: 12px;
color: var(--label-tertiary);
padding: 8px 20px 0;
}
/* Toast */
.toast {
position: fixed;
bottom: calc(var(--safe-bottom) + 20px);
left: 50%;
transform: translateX(-50%) translateY(100px);
background: rgba(50,50,52,0.97);
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
color: #fff;
font-size: 14px;
font-weight: 500;
padding: 12px 20px;
border-radius: var(--radius-full);
z-index: 999;
transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), opacity 0.3s;
opacity: 0;
white-space: nowrap;
border: 0.5px solid rgba(255,255,255,0.1);
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Progress bar */
.progress-bar {
height: 3px;
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
margin: 0 16px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--tint);
border-radius: var(--radius-sm);
width: 0%;
transition: width 0.3s ease;
}
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: var(--radius-full);
animation: spin 0.6s linear infinite;
flex-shrink: 0;
}
/* Footer */
.footer {
text-align: center;
padding: 32px 20px 8px;
font-size: 12px;
color: var(--label-tertiary);
}
.footer strong { color: var(--label-secondary); }
@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; }
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</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; }
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</style>
<script type="module" src="https://auth.quixzoom.com/site-auth-snippet.js"></script>
</head>
<body>
<!-- Nav Bar -->
<div class="nav-bar">
<h1>quiXzoom API</h1>
<span class="nav-badge" id="nav-status">Kontrollerar…</span>
</div>
<!-- Page Header -->
<div class="page-header">
<div class="page-title">iOS Backend Status</div>
<div class="page-subtitle">Realtids-kontroll av alla quiXzoom-endpoints</div>
</div>
<!-- Summary -->
<div class="summary-row">
<div class="stat-card green">
<div class="stat-number" id="count-ok"></div>
<div class="stat-label">OK</div>
</div>
<div class="stat-card red">
<div class="stat-number" id="count-err"></div>
<div class="stat-label">Fel</div>
</div>
<div class="stat-card yellow">
<div class="stat-number" id="avg-ms"></div>
<div class="stat-label">Median ms</div>
</div>
</div>
<!-- Progress -->
<div class="progress-bar" style="margin-top:16px;">
<div class="progress-fill" id="progress-fill"></div>
</div>
<!-- Run All -->
<div class="run-all-container">
<button class="run-all-btn" id="run-all-btn" onclick="runAll()">
<span> Testa alla endpoints</span>
</button>
</div>
<div class="last-run" id="last-run">Inte testad ännu</div>
<!-- Endpoint Sections -->
<!-- Zoomers / Profiles -->
<div class="section">
<div class="section-header">
<span class="section-title">Zoomer Profiler</span>
<button class="section-action" onclick="testSection('profiles')">Testa alla</button>
</div>
<div class="endpoint-list" id="list-profiles">
<div class="endpoint-item" onclick="toggleDetail('ep-z-list')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/api/quixzoom/zoomers</div>
<div class="endpoint-desc">Lista alla zoomers</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-z-list"></div>
<div class="resp-time" id="ms-ep-z-list"></div>
</div>
<button class="test-btn" id="btn-ep-z-list" onclick="event.stopPropagation(); testEndpoint('ep-z-list')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-z-list">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-list"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-z-profile')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/zoomers/:id/profile</div>
<div class="endpoint-desc">Fullständig zoomerprofil</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-z-profile"></div>
<div class="resp-time" id="ms-ep-z-profile"></div>
</div>
<button class="test-btn" id="btn-ep-z-profile" onclick="event.stopPropagation(); testEndpoint('ep-z-profile')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-z-profile">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/profile</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-profile"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-z-patch')">
<span class="method-badge method-PATCH">PATCH</span>
<div class="endpoint-info">
<div class="endpoint-path">/zoomers/:id/profile</div>
<div class="endpoint-desc">Uppdatera bio/utrustning/plats</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-z-patch"></div>
<div class="resp-time" id="ms-ep-z-patch"></div>
</div>
<button class="test-btn" id="btn-ep-z-patch" onclick="event.stopPropagation(); testEndpoint('ep-z-patch')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-z-patch">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/profile</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"bio":"Test"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-patch"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-z-portfolio')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/zoomers/:id/portfolio</div>
<div class="endpoint-desc">Portfolio foton/videos</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-z-portfolio"></div>
<div class="resp-time" id="ms-ep-z-portfolio"></div>
</div>
<button class="test-btn" id="btn-ep-z-portfolio" onclick="event.stopPropagation(); testEndpoint('ep-z-portfolio')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-z-portfolio">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/portfolio</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-portfolio"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-z-portfolio-post')">
<span class="method-badge method-POST">POST</span>
<div class="endpoint-info">
<div class="endpoint-path">/zoomers/:id/portfolio</div>
<div class="endpoint-desc">Lägg till portfolioobjekt</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-z-portfolio-post"></div>
<div class="resp-time" id="ms-ep-z-portfolio-post"></div>
</div>
<button class="test-btn" id="btn-ep-z-portfolio-post" onclick="event.stopPropagation(); testEndpoint('ep-z-portfolio-post')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-z-portfolio-post">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-002/portfolio</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"url":"…","type":"photo"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-portfolio-post"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-z-earnings')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/zoomers/:id/earnings</div>
<div class="endpoint-desc">Inkomstöversikt per period</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-z-earnings"></div>
<div class="resp-time" id="ms-ep-z-earnings"></div>
</div>
<button class="test-btn" id="btn-ep-z-earnings" onclick="event.stopPropagation(); testEndpoint('ep-z-earnings')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-z-earnings">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/zoomers/zoomer-001/earnings?period=month</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-z-earnings"></span></div>
</div>
</div>
</div>
<!-- Missions -->
<div class="section">
<div class="section-header">
<span class="section-title">Uppdrag / Missions</span>
<button class="section-action" onclick="testSection('missions')">Testa alla</button>
</div>
<div class="endpoint-list" id="list-missions">
<div class="endpoint-item" onclick="toggleDetail('ep-m-list')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/api/quixzoom/missions</div>
<div class="endpoint-desc">Lista alla uppdrag</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-list"></div>
<div class="resp-time" id="ms-ep-m-list"></div>
</div>
<button class="test-btn" id="btn-ep-m-list" onclick="event.stopPropagation(); testEndpoint('ep-m-list')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-list">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-list"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-stats')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/stats</div>
<div class="endpoint-desc">Aggregerad statistik</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-stats"></div>
<div class="resp-time" id="ms-ep-m-stats"></div>
</div>
<button class="test-btn" id="btn-ep-m-stats" onclick="event.stopPropagation(); testEndpoint('ep-m-stats')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-stats">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/stats</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-stats"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-post')">
<span class="method-badge method-POST">POST</span>
<div class="endpoint-info">
<div class="endpoint-path">/api/quixzoom/missions</div>
<div class="endpoint-desc">Skapa nytt uppdrag</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-post"></div>
<div class="resp-time" id="ms-ep-m-post"></div>
</div>
<button class="test-btn" id="btn-ep-m-post" onclick="event.stopPropagation(); testEndpoint('ep-m-post')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-post">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"location":"Phuket","description":"API test uppdrag","start_time":"…"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-post"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-get')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id</div>
<div class="endpoint-desc">Uppdragsdetaljer</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-get"></div>
<div class="resp-time" id="ms-ep-m-get"></div>
</div>
<button class="test-btn" id="btn-ep-m-get" onclick="event.stopPropagation(); testEndpoint('ep-m-get')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-get">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-001</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-get"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-zoomers')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id/zoomers</div>
<div class="endpoint-desc">Tilldelade zoomers</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-zoomers"></div>
<div class="resp-time" id="ms-ep-m-zoomers"></div>
</div>
<button class="test-btn" id="btn-ep-m-zoomers" onclick="event.stopPropagation(); testEndpoint('ep-m-zoomers')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-zoomers">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-001/zoomers</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-zoomers"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-assign')">
<span class="method-badge method-POST">POST</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id/assign</div>
<div class="endpoint-desc">Tilldela zoomer</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-assign"></div>
<div class="resp-time" id="ms-ep-m-assign"></div>
</div>
<button class="test-btn" id="btn-ep-m-assign" onclick="event.stopPropagation(); testEndpoint('ep-m-assign')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-assign">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-006/assign</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"zoomer_id":"zoomer-003"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-assign"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-status')">
<span class="method-badge method-PATCH">PATCH</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id/status</div>
<div class="endpoint-desc">Ändra uppdragsstatus</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-status"></div>
<div class="resp-time" id="ms-ep-m-status"></div>
</div>
<button class="test-btn" id="btn-ep-m-status" onclick="event.stopPropagation(); testEndpoint('ep-m-status')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-status">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-007/status</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"status":"active"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-status"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-checkin')">
<span class="method-badge method-POST">POST</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id/checkin</div>
<div class="endpoint-desc">Zoomer incheckning</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-checkin"></div>
<div class="resp-time" id="ms-ep-m-checkin"></div>
</div>
<button class="test-btn" id="btn-ep-m-checkin" onclick="event.stopPropagation(); testEndpoint('ep-m-checkin')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-checkin">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-005/checkin</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"zoomer_id":"zoomer-005","lat":7.99,"lng":98.26}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-checkin"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-checkout')">
<span class="method-badge method-POST">POST</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id/checkout</div>
<div class="endpoint-desc">Leverans + utcheckning</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-checkout"></div>
<div class="resp-time" id="ms-ep-m-checkout"></div>
</div>
<button class="test-btn" id="btn-ep-m-checkout" onclick="event.stopPropagation(); testEndpoint('ep-m-checkout')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-checkout">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-004/checkout</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"zoomer_id":"zoomer-004","delivery_url":"https://cdn.test/file.zip"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-checkout"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-m-deliverables')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/missions/:id/deliverables</div>
<div class="endpoint-desc">Levererade filer</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-m-deliverables"></div>
<div class="resp-time" id="ms-ep-m-deliverables"></div>
</div>
<button class="test-btn" id="btn-ep-m-deliverables" onclick="event.stopPropagation(); testEndpoint('ep-m-deliverables')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-m-deliverables">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/missions/mission-001/deliverables</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-m-deliverables"></span></div>
</div>
</div>
</div>
<!-- Payouts -->
<div class="section">
<div class="section-header">
<span class="section-title">Utbetalningar</span>
<button class="section-action" onclick="testSection('payouts')">Testa alla</button>
</div>
<div class="endpoint-list" id="list-payouts">
<div class="endpoint-item" onclick="toggleDetail('ep-p-zoomer')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/payouts/zoomer/:id</div>
<div class="endpoint-desc">Utbetalningshistorik</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-p-zoomer"></div>
<div class="resp-time" id="ms-ep-p-zoomer"></div>
</div>
<button class="test-btn" id="btn-ep-p-zoomer" onclick="event.stopPropagation(); testEndpoint('ep-p-zoomer')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-p-zoomer">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/payouts/zoomer/zoomer-001</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-p-zoomer"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-p-pending')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/payouts/pending</div>
<div class="endpoint-desc">Väntande utbetalningar</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-p-pending"></div>
<div class="resp-time" id="ms-ep-p-pending"></div>
</div>
<button class="test-btn" id="btn-ep-p-pending" onclick="event.stopPropagation(); testEndpoint('ep-p-pending')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-p-pending">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/payouts/pending</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-p-pending"></span></div>
</div>
</div>
</div>
<!-- Ratings -->
<div class="section">
<div class="section-header">
<span class="section-title">Betyg / Ratings</span>
<button class="section-action" onclick="testSection('ratings')">Testa alla</button>
</div>
<div class="endpoint-list" id="list-ratings">
<div class="endpoint-item" onclick="toggleDetail('ep-r-zoomer')">
<span class="method-badge method-GET">GET</span>
<div class="endpoint-info">
<div class="endpoint-path">/ratings/zoomer/:id</div>
<div class="endpoint-desc">Betyg för zoomer</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-r-zoomer"></div>
<div class="resp-time" id="ms-ep-r-zoomer"></div>
</div>
<button class="test-btn" id="btn-ep-r-zoomer" onclick="event.stopPropagation(); testEndpoint('ep-r-zoomer')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-r-zoomer">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/ratings/zoomer/z001</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-r-zoomer"></span></div>
</div>
<div class="endpoint-item" onclick="toggleDetail('ep-r-post')">
<span class="method-badge method-POST">POST</span>
<div class="endpoint-info">
<div class="endpoint-path">/ratings</div>
<div class="endpoint-desc">Skicka in betyg</div>
</div>
<div class="endpoint-meta">
<div class="status-dot dot-grey" id="dot-ep-r-post"></div>
<div class="resp-time" id="ms-ep-r-post"></div>
</div>
<button class="test-btn" id="btn-ep-r-post" onclick="event.stopPropagation(); testEndpoint('ep-r-post')">Test</button>
</div>
<div class="endpoint-detail" id="detail-ep-r-post">
<div class="detail-row"><span class="detail-key">URL</span><span class="detail-val">/api/quixzoom/ratings</span></div>
<div class="detail-row"><span class="detail-key">Body</span><span class="detail-val">{"missionId":"m_test","zoomerId":"z001","rating":5,"comment":"Perfekt!","customerId":"c_test"}</span></div>
<div class="detail-row"><span class="detail-key">Svar</span><span class="detail-val" id="resp-ep-r-post"></span></div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
quiXzoom Backend · iOS deadline <strong>5 augusti 2026</strong><br>
Byggs av Wavult &amp; AAMOS · Phuket Workcamp 2026
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
// Endpoint definitions
const BASE = '';
const ENDPOINTS = {
// Profiles
'ep-z-list': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers`, section: 'profiles' },
'ep-z-profile': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/profile`, section: 'profiles' },
'ep-z-patch': { method: 'PATCH', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/profile`, body: { bio: 'API status test' }, section: 'profiles' },
'ep-z-portfolio': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/portfolio`, section: 'profiles' },
'ep-z-portfolio-post': { method: 'POST', url: `${BASE}/api/quixzoom/zoomers/zoomer-002/portfolio`, body: { url: 'https://example.com/test.jpg', type: 'photo', caption: 'API test' }, section: 'profiles' },
'ep-z-earnings': { method: 'GET', url: `${BASE}/api/quixzoom/zoomers/zoomer-001/earnings?period=month`, section: 'profiles' },
// Missions
'ep-m-list': { method: 'GET', url: `${BASE}/api/quixzoom/missions`, section: 'missions' },
'ep-m-stats': { method: 'GET', url: `${BASE}/api/quixzoom/missions/stats`, section: 'missions' },
'ep-m-post': { method: 'POST', url: `${BASE}/api/quixzoom/missions`, body: { location: 'Phuket API Test', description: 'API health check test mission', start_time: new Date(Date.now() + 86400000).toISOString(), duration_hours: 2, budget_sek: 1000, camera_count: 1 }, section: 'missions' },
'ep-m-get': { method: 'GET', url: `${BASE}/api/quixzoom/missions/mission-001`, section: 'missions' },
'ep-m-zoomers': { method: 'GET', url: `${BASE}/api/quixzoom/missions/mission-001/zoomers`, section: 'missions' },
'ep-m-assign': { method: 'POST', url: `${BASE}/api/quixzoom/missions/mission-009/assign`, body: { zoomer_id: 'zoomer-004' }, section: 'missions' },
'ep-m-status': { method: 'PATCH', url: `${BASE}/api/quixzoom/missions/mission-010/status`, body: { status: 'active' }, section: 'missions' },
'ep-m-checkin': { method: 'POST', url: `${BASE}/api/quixzoom/missions/mission-005/checkin`, body: { zoomer_id: 'zoomer-005', lat: 7.9901, lng: 98.2631 }, section: 'missions' },
'ep-m-checkout': { method: 'POST', url: `${BASE}/api/quixzoom/missions/mission-004/checkout`, body: { zoomer_id: 'zoomer-004', delivery_url: 'https://storage.quixzoom.se/test/delivery.zip' }, section: 'missions' },
'ep-m-deliverables': { method: 'GET', url: `${BASE}/api/quixzoom/missions/mission-001/deliverables`, section: 'missions' },
// Payouts
'ep-p-zoomer': { method: 'GET', url: `${BASE}/api/quixzoom/payouts/zoomer/zoomer-001`, section: 'payouts' },
'ep-p-pending': { method: 'GET', url: `${BASE}/api/quixzoom/payouts/pending`, section: 'payouts' },
// Ratings
'ep-r-zoomer': { method: 'GET', url: `${BASE}/api/quixzoom/ratings/zoomer/z001`, section: 'ratings' },
'ep-r-post': { method: 'POST', url: `${BASE}/api/quixzoom/ratings`, body: { missionId: 'm_api_test', zoomerId: 'z001', rating: 5, comment: 'API status test', customerId: 'c_test' }, section: 'ratings' },
};
const results = {};
let testingAll = false;
// Test single endpoint
async function testEndpoint(id) {
const ep = ENDPOINTS[id];
if (!ep) return;
const btn = document.getElementById(`btn-${id}`);
const dot = document.getElementById(`dot-${id}`);
const ms = document.getElementById(`ms-${id}`);
const resp = document.getElementById(`resp-${id}`);
btn.classList.add('testing');
btn.innerHTML = '<div class="spinner"></div>';
dot.className = 'status-dot dot-yellow';
const t0 = performance.now();
try {
const opts = {
method: ep.method,
headers: { 'Content-Type': 'application/json' },
};
if (ep.body) opts.body = JSON.stringify(ep.body);
const r = await fetch(ep.url, opts);
const elapsed = Math.round(performance.now() - t0);
let data;
try { data = await r.json(); } catch { data = {}; }
const ok = r.status >= 200 && r.status < 300;
results[id] = { ok, status: r.status, ms: elapsed };
dot.className = `status-dot ${ok ? 'dot-green' : 'dot-red'}`;
ms.textContent = `${elapsed}ms`;
ms.className = `resp-time ${elapsed < 200 ? 'fast' : elapsed < 800 ? '' : 'slow'}`;
if (resp) resp.textContent = JSON.stringify(data).slice(0, 120) + (JSON.stringify(data).length > 120 ? '…' : '');
btn.classList.remove('testing');
btn.textContent = 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>' : '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg>';
btn.style.background = ok ? 'var(--green)' : 'var(--red)';
setTimeout(() => {
btn.textContent = 'Test';
btn.style.background = '';
}, 2000);
updateSummary();
} catch (e) {
const elapsed = Math.round(performance.now() - t0);
results[id] = { ok: false, status: 0, ms: elapsed };
dot.className = 'status-dot dot-red';
ms.textContent = `${elapsed}ms`;
if (resp) resp.textContent = String(e).slice(0, 120);
btn.classList.remove('testing');
btn.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg>';
btn.style.background = 'var(--red)';
setTimeout(() => { btn.textContent = 'Test'; btn.style.background = ''; }, 2000);
updateSummary();
}
}
// Test a section
async function testSection(section) {
const ids = Object.keys(ENDPOINTS).filter(id => ENDPOINTS[id].section === section);
for (const id of ids) {
await testEndpoint(id);
await delay(120);
}
}
// Run All
async function runAll() {
if (testingAll) return;
testingAll = true;
const btn = document.getElementById('run-all-btn');
const prog = document.getElementById('progress-fill');
btn.disabled = true;
btn.innerHTML = '<div class="spinner" style="border-color:rgba(255,255,255,.3);border-top-color:#fff;"></div><span>Testar…</span>';
const ids = Object.keys(ENDPOINTS);
for (let i = 0; i < ids.length; i++) {
await testEndpoint(ids[i]);
prog.style.width = `${Math.round(((i + 1) / ids.length) * 100)}%`;
await delay(80);
}
prog.style.width = '100%';
testingAll = false;
btn.disabled = false;
btn.innerHTML = '<span> Kör igen</span>';
const now = new Date().toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
document.getElementById('last-run').textContent = `Senast testad: ${now}`;
const okCount = Object.values(results).filter(r => r.ok).length;
showToast(`${okCount}/${ids.length} endpoints <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>`);
}
// Update summary bar
function updateSummary() {
const vals = Object.values(results);
const ok = vals.filter(r => r.ok).length;
const err = vals.filter(r => !r.ok).length;
const times = vals.map(r => r.ms).sort((a, b) => a - b);
const median = times.length ? times[Math.floor(times.length / 2)] : 0;
document.getElementById('count-ok').textContent = ok;
document.getElementById('count-err').textContent = err;
document.getElementById('avg-ms').textContent = median ? `${median}` : '';
const total = Object.keys(ENDPOINTS).length;
const tested = vals.length;
document.getElementById('nav-status').textContent = `${tested}/${total} testad`;
}
// Toggle detail row
function toggleDetail(id) {
const el = document.getElementById(`detail-${id}`);
if (!el) return;
el.classList.toggle('open');
}
// Toast
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
// Auto-run on load
window.addEventListener('load', () => {
setTimeout(runAll, 600);
});
</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;
}
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</style>
<!--
quiXzoom Universal Auth Snippet v2.0
Add this to ALL quiXzoom sites for seamless cross-domain login
Features:
- Silent login (auto-refresh tokens)
- Cross-domain cookie sync
- Real-time auth state updates
- Mobile app compatible
-->
<!-- Auth bar styles -->
<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-menu {
position: relative;
}
#qz-auth-bar .qz-dropdown {
position: absolute;
top: 40px;
right: 0;
background: #1C1C1E;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 12px;
padding: 8px;
min-width: 220px;
display: none;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
#qz-auth-bar .qz-dropdown.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-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #007AFF;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: white;
}
#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;
}
/* QUIXZOOM STANDARD — Dark Theme */
:root {
--qz-bg: #0a0a0a;
--qz-surface: #111111;
--qz-surface-2: #1a1a1a;
--qz-border: rgba(255,255,255,0.08);
--qz-text: #e8e8e8;
--qz-text-dim: #a0a0a0;
--qz-text-muted: #6b6b6b;
--qz-blue: #007AFF;
--qz-blue-dark: #0056CC;
--qz-radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
background: var(--qz-bg);
color: var(--qz-text);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
/* Navigation */
.qz-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px; height: 56px;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--qz-border);
}
.qz-nav-logo { font-size: 18px; font-weight: 700; color: #fff; text-decoration: none; }
.qz-nav-links { display: flex; gap: 24px; list-style: none; }
.qz-nav-links a { font-size: 14px; color: var(--qz-text-dim); text-decoration: none; transition: color 0.2s; }
.qz-nav-links a:hover { color: #fff; }
/* Buttons */
.qz-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px; border-radius: var(--qz-radius);
font-size: 15px; font-weight: 600; text-decoration: none;
transition: all 0.2s; cursor: pointer; border: none;
}
.qz-btn-primary {
background: var(--qz-blue); color: #fff;
}
.qz-btn-primary:hover {
background: var(--qz-blue-dark); transform: translateY(-1px);
}
.qz-btn-ghost {
background: transparent; color: var(--qz-text);
border: 1px solid var(--qz-border);
}
.qz-btn-ghost:hover {
border-color: var(--qz-blue); color: var(--qz-blue);
}
/* Cards */
.qz-card {
background: var(--qz-surface);
border: 1px solid var(--qz-border);
border-radius: var(--qz-radius);
padding: 24px;
transition: transform 0.2s, border-color 0.2s;
}
.qz-card:hover {
transform: translateY(-2px);
border-color: rgba(0,122,255,0.3);
}
/* Footer */
.qz-footer {
border-top: 1px solid var(--qz-border);
padding: 40px 24px;
text-align: center;
color: var(--qz-text-dim);
font-size: 14px;
}
/* Banner */
.qz-banner {
background: #001E50;
color: rgba(255,255,255,0.55);
text-align: center;
padding: 10px;
font-size: 12px;
}
.qz-banner a { color: rgba(255,255,255,0.8); text-decoration: none; }
@media (max-width: 768px) {
.qz-nav-links { display: none; }
}
</style>
<!-- Auth bar HTML -->
<div id="qz-auth-bar">
<a href="https://quixzoom.com" class="qz-logo">qui<span>X</span>zoom</a>
<div class="qz-nav">
<!-- Guest view -->
<div id="qz-guest-view">
<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>
<!-- Authenticated view -->
<div id="qz-user-view" style="display:none;">
<span class="qz-balance" id="qz-balance">$0.00</span>
<div class="qz-user-menu">
<button class="qz-btn qz-btn-ghost" onclick="qzToggleMenu()" style="display:flex;align-items:center;gap:8px;">
<div class="qz-avatar" id="qz-avatar">?</div>
<span id="qz-user-name" style="color:white;">User</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" id="qz-dropdown">
<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>
<!-- SSO Client Script -->
<script>
(function() {
const SSO_BASE = 'https://auth.quixzoom.com';
// Check auth status on load
async function qzCheckAuth() {
try {
const res = await fetch(SSO_BASE + '/auth/silent', {
method: 'GET',
credentials: 'include',
});
if (res.ok) {
const data = await res.json();
if (data.authenticated) {
qzShowUser(data.user);
return;
}
}
// Try check endpoint
const checkRes = await fetch(SSO_BASE + '/auth/check', {
credentials: 'include',
});
if (checkRes.ok) {
const checkData = await checkRes.json();
if (checkData.authenticated && checkData.user) {
qzShowUser(checkData.user);
return;
}
}
qzShowGuest();
} catch (e) {
console.log('[QZAuth] Check failed, showing guest');
qzShowGuest();
}
}
function qzShowUser(user) {
document.getElementById('qz-guest-view').style.display = 'none';
document.getElementById('qz-user-view').style.display = 'flex';
document.getElementById('qz-user-name').textContent = user.name || user.email;
document.getElementById('qz-avatar').textContent = (user.name || user.email)[0].toUpperCase();
// Dispatch event for other scripts
window.dispatchEvent(new CustomEvent('qz-auth-change', {
detail: { authenticated: true, user }
}));
}
function qzShowGuest() {
document.getElementById('qz-guest-view').style.display = 'flex';
document.getElementById('qz-user-view').style.display = 'none';
window.dispatchEvent(new CustomEvent('qz-auth-change', {
detail: { authenticated: false }
}));
}
window.qzToggleMenu = function() {
document.getElementById('qz-dropdown').classList.toggle('active');
};
window.qzLogout = async function() {
try {
await fetch(SSO_BASE + '/auth/logout', {
method: 'POST',
credentials: 'include',
});
} catch (e) {}
// Clear all cookies
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=/;';
document.cookie = 'qz_auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;';
qzShowGuest();
window.location.reload();
};
// Close dropdown on outside click
document.addEventListener('click', function(e) {
if (!e.target.closest('.qz-user-menu')) {
document.getElementById('qz-dropdown').classList.remove('active');
}
});
// Check auth on page load
qzCheckAuth();
// Re-check auth periodically (every 5 minutes)
setInterval(qzCheckAuth, 5 * 60 * 1000);
// Listen for storage events (login from other tabs)
window.addEventListener('storage', function(e) {
if (e.key === 'qz_auth_event') {
const event = JSON.parse(e.newValue || '{}');
if (event.type === 'login') {
qzCheckAuth();
} else if (event.type === 'logout') {
qzShowGuest();
}
}
});
})();
</script>
</body>
</html>