6989a98d75
- 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
1046 lines
44 KiB
HTML
1046 lines
44 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="sv">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>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 & 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>
|