Files
boc/quixzoom-landing-fixed/api-status.html
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

1046 lines
44 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; }
}
</style>
<style id="wcag-contrast-enforcement">
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* Fix common low-contrast patterns */
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(255,255,255,0.4)"] { color: rgba(255,255,255,0.72) !important; }
[style*="rgba(255,255,255,0.3)"] { color: rgba(255,255,255,0.72) !important; }
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
</head>
<body>
<!-- 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>
</body>
</html>