Files
boc/quixzoom-landing-fixed/api-status.html
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

1046 lines
47 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: 2px 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: 5px;
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: 50%;
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: 999px;
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: 2px;
margin: 0 16px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--tint);
border-radius: 2px;
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: 50%;
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 ? '✓' : '✗';
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 = '✗';
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 ✓`);
}
// ── 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>