Files
boc/quixzoom-landing-fixed/api-status.html
T

1046 lines
47 KiB
HTML
Raw Normal View History

<!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>