2026-07-05 06:41:32 +00:00
<!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 >
2026-07-07 07:11:50 +00:00
/* iOS 26 / Apple Native Design System */
2026-07-05 06:41:32 +00:00
: 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 : 12 px ;
--radius-lg : 18 px ;
--radius-sm : 8 px ;
--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 , 0 px ) ;
--safe-bottom : env ( safe - area - inset - bottom , 0 px ) ;
}
* { 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 : 100 dvh ;
padding-top : calc ( var ( - - safe - top ) + 16 px ) ;
padding-bottom : calc ( var ( - - safe - bottom ) + 32 px ) ;
overscroll-behavior-y : contain ;
}
2026-07-07 07:11:50 +00:00
/* Navigation Bar */
2026-07-05 06:41:32 +00:00
. nav-bar {
position : sticky ;
top : 0 ;
z-index : 100 ;
background : rgba ( 242 , 242 , 247 , 0.82 ) ;
-webkit- backdrop-filter : blur ( 20 px ) saturate ( 180 % ) ;
backdrop-filter : blur ( 20 px ) saturate ( 180 % ) ;
border-bottom : 0.5 px solid var ( - - separator ) ;
padding : 12 px 20 px 10 px ;
display : flex ;
align-items : center ;
justify-content : space-between ;
}
. nav-bar h1 {
font-size : 17 px ;
font-weight : 600 ;
letter-spacing : -0.4 px ;
color : var ( - - label ) ;
}
. nav-bar . nav-badge {
font-size : 13 px ;
font-weight : 500 ;
color : var ( - - label - secondary ) ;
}
2026-07-07 07:11:50 +00:00
/* Large Title */
2026-07-05 06:41:32 +00:00
. page-header {
padding : 24 px 20 px 8 px ;
}
. page-title {
font-size : 34 px ;
font-weight : 700 ;
letter-spacing : -0.5 px ;
line-height : 1.1 ;
color : var ( - - label ) ;
}
. page-subtitle {
font-size : 15 px ;
color : var ( - - label - secondary ) ;
margin-top : 4 px ;
}
2026-07-07 07:11:50 +00:00
/* Summary Cards */
2026-07-05 06:41:32 +00:00
. summary-row {
display : grid ;
grid-template-columns : repeat ( 3 , 1 fr ) ;
gap : 12 px ;
padding : 16 px 20 px 4 px ;
}
. stat-card {
background : var ( - - bg - secondary ) ;
border-radius : var ( - - radius ) ;
padding : 14 px 12 px ;
text-align : center ;
position : relative ;
overflow : hidden ;
}
. stat-card :: before {
content : '' ;
position : absolute ;
top : 0 ; left : 0 ; right : 0 ;
height : 2 px ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - sm ) 2 px 0 0 ;
2026-07-05 06:41:32 +00:00
}
. 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 : 28 px ;
font-weight : 700 ;
font-variant-numeric : tabular-nums ;
letter-spacing : -0.5 px ;
line-height : 1 ;
margin-bottom : 4 px ;
}
. 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 : 11 px ;
font-weight : 500 ;
color : var ( - - label - secondary ) ;
text-transform : uppercase ;
letter-spacing : 0.3 px ;
}
2026-07-07 07:11:50 +00:00
/* Section */
2026-07-05 06:41:32 +00:00
. section {
margin-top : 28 px ;
}
. section-header {
display : flex ;
align-items : center ;
justify-content : space-between ;
padding : 0 20 px 8 px ;
}
. section-title {
font-size : 13 px ;
font-weight : 600 ;
text-transform : uppercase ;
letter-spacing : 0.6 px ;
color : var ( - - label - secondary ) ;
}
. section-action {
font-size : 13 px ;
font-weight : 500 ;
color : var ( - - tint ) ;
background : none ;
border : none ;
cursor : pointer ;
padding : 0 ;
}
. section-action : active { opacity : 0.6 ; }
2026-07-07 07:11:50 +00:00
/* Endpoint List */
2026-07-05 06:41:32 +00:00
. endpoint-list {
background : var ( - - bg - secondary ) ;
border-radius : var ( - - radius - lg ) ;
margin : 0 16 px ;
overflow : hidden ;
}
. endpoint-item {
display : flex ;
align-items : center ;
gap : 12 px ;
padding : 14 px 16 px ;
border-bottom : 0.5 px solid var ( - - separator ) ;
transition : background 0.15 s ;
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 : 10 px ;
font-weight : 700 ;
letter-spacing : 0.4 px ;
padding : 3 px 7 px ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - sm ) ;
2026-07-05 06:41:32 +00:00
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 : 13 px ;
font-weight : 500 ;
color : var ( - - label ) ;
font-family : "SF Mono" , "Menlo" , monospace ;
white-space : nowrap ;
overflow : hidden ;
text-overflow : ellipsis ;
}
. endpoint-desc {
font-size : 12 px ;
color : var ( - - label - secondary ) ;
margin-top : 1 px ;
white-space : nowrap ;
overflow : hidden ;
text-overflow : ellipsis ;
}
. endpoint-meta {
display : flex ;
flex-direction : column ;
align-items : flex-end ;
gap : 3 px ;
flex-shrink : 0 ;
}
. status-dot {
width : 8 px ;
height : 8 px ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
flex-shrink : 0 ;
}
. dot-green { background : var ( - - green ) ; box-shadow : 0 0 6 px rgba ( 48 , 209 , 88 , 0.5 ) ; }
. dot-red { background : var ( - - red ) ; box-shadow : 0 0 6 px rgba ( 255 , 69 , 58 , 0.5 ) ; }
. dot-yellow { background : var ( - - yellow ) ; }
. dot-grey { background : var ( - - label - tertiary ) ; }
. resp-time {
font-size : 11 px ;
font-weight : 500 ;
color : var ( - - label - secondary ) ;
font-variant-numeric : tabular-nums ;
}
. resp-time . fast { color : var ( - - green ) ; }
. resp-time . slow { color : var ( - - orange ) ; }
2026-07-07 07:11:50 +00:00
/* Test Button */
2026-07-05 06:41:32 +00:00
. test-btn {
display : inline-flex ;
align-items : center ;
justify-content : center ;
gap : 6 px ;
background : var ( - - tint ) ;
color : #fff ;
border : none ;
border-radius : var ( - - radius - sm ) ;
font-size : 12 px ;
font-weight : 600 ;
padding : 6 px 12 px ;
cursor : pointer ;
transition : opacity 0.15 s , transform 0.1 s ;
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 ;
}
2026-07-07 07:11:50 +00:00
/* Detail Row (expandable) */
2026-07-05 06:41:32 +00:00
. endpoint-detail {
display : none ;
background : var ( - - bg - tertiary ) ;
border-radius : 0 ;
padding : 12 px 16 px ;
border-top : 0.5 px solid var ( - - separator ) ;
}
. endpoint-detail . open { display : block ; }
. detail-row {
display : flex ;
justify-content : space-between ;
align-items : flex-start ;
padding : 3 px 0 ;
}
. detail-key {
font-size : 12 px ;
color : var ( - - label - tertiary ) ;
font-family : "SF Mono" , monospace ;
}
. detail-val {
font-size : 12 px ;
color : var ( - - label - secondary ) ;
font-family : "SF Mono" , monospace ;
text-align : right ;
max-width : 60 % ;
word-break : break-all ;
}
2026-07-07 07:11:50 +00:00
/* Run All Button */
2026-07-05 06:41:32 +00:00
. run-all-container {
padding : 20 px 16 px 8 px ;
}
. run-all-btn {
width : 100 % ;
display : flex ;
align-items : center ;
justify-content : center ;
gap : 8 px ;
background : var ( - - tint ) ;
color : #fff ;
border : none ;
border-radius : var ( - - radius ) ;
font-size : 17 px ;
font-weight : 600 ;
padding : 16 px ;
cursor : pointer ;
font-family : var ( - - font ) ;
transition : opacity 0.15 s , transform 0.1 s ;
letter-spacing : -0.3 px ;
}
. 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 ;
}
2026-07-07 07:11:50 +00:00
/* Last Run */
2026-07-05 06:41:32 +00:00
. last-run {
text-align : center ;
font-size : 12 px ;
color : var ( - - label - tertiary ) ;
padding : 8 px 20 px 0 ;
}
2026-07-07 07:11:50 +00:00
/* Toast */
2026-07-05 06:41:32 +00:00
. toast {
position : fixed ;
bottom : calc ( var ( - - safe - bottom ) + 20 px ) ;
left : 50 % ;
transform : translateX ( -50 % ) translateY ( 100 px ) ;
background : rgba ( 50 , 50 , 52 , 0.97 ) ;
-webkit- backdrop-filter : blur ( 20 px ) ;
backdrop-filter : blur ( 20 px ) ;
color : #fff ;
font-size : 14 px ;
font-weight : 500 ;
padding : 12 px 20 px ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
z-index : 999 ;
transition : transform 0.3 s cubic-bezier ( 0.34 , 1.56 , 0.64 , 1 ) , opacity 0.3 s ;
opacity : 0 ;
white-space : nowrap ;
border : 0.5 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
}
. toast . show {
transform : translateX ( -50 % ) translateY ( 0 ) ;
opacity : 1 ;
}
2026-07-07 07:11:50 +00:00
/* Progress bar */
2026-07-05 06:41:32 +00:00
. progress-bar {
height : 3 px ;
background : var ( - - bg - tertiary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - sm ) ;
2026-07-05 06:41:32 +00:00
margin : 0 16 px 0 ;
overflow : hidden ;
}
. progress-fill {
height : 100 % ;
background : var ( - - tint ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - sm ) ;
2026-07-05 06:41:32 +00:00
width : 0 % ;
transition : width 0.3 s ease ;
}
2026-07-07 07:11:50 +00:00
/* Spinner */
2026-07-05 06:41:32 +00:00
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. spinner {
width : 14 px ; height : 14 px ;
border : 2 px solid rgba ( 255 , 255 , 255 , 0.3 ) ;
border-top-color : #fff ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
animation : spin 0.6 s linear infinite ;
flex-shrink : 0 ;
}
2026-07-07 07:11:50 +00:00
/* Footer */
2026-07-05 06:41:32 +00:00
. footer {
text-align : center ;
padding : 32 px 20 px 8 px ;
font-size : 12 px ;
color : var ( - - label - tertiary ) ;
}
. footer strong { color : var ( - - label - secondary ) ; }
@ media ( max-width : 640px ) {
. hero h1 { font-size : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !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 : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !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()" >
2026-07-07 07:11:50 +00:00
< span > Testa alla endpoints< / span >
2026-07-05 06:41:32 +00:00
< / button >
< / div >
< div class = "last-run" id = "last-run" > Inte testad ännu< / div >
2026-07-07 07:11:50 +00:00
<!-- Endpoint Sections -->
2026-07-05 06:41:32 +00:00
<!-- 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 >
2026-07-07 07:11:50 +00:00
// Endpoint definitions
2026-07-05 06:41:32 +00:00
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 ;
2026-07-07 07:11:50 +00:00
// Test single endpoint
2026-07-05 06:41:32 +00:00
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' ) ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
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' ) ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
btn . style . background = 'var(--red)' ;
setTimeout ( ( ) => { btn . textContent = 'Test' ; btn . style . background = '' ; } , 2000 ) ;
updateSummary ( ) ;
}
}
2026-07-07 07:11:50 +00:00
// Test a section
2026-07-05 06:41:32 +00:00
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 ) ;
}
}
2026-07-07 07:11:50 +00:00
// Run All
2026-07-05 06:41:32 +00:00
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 ;
2026-07-07 07:11:50 +00:00
btn . innerHTML = '<span> Kör igen</span>' ;
2026-07-05 06:41:32 +00:00
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 ;
2026-07-07 07:11:50 +00:00
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> ` ) ;
2026-07-05 06:41:32 +00:00
}
2026-07-07 07:11:50 +00:00
// Update summary bar
2026-07-05 06:41:32 +00:00
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 ` ;
}
2026-07-07 07:11:50 +00:00
// Toggle detail row
2026-07-05 06:41:32 +00:00
function toggleDetail ( id ) {
const el = document . getElementById ( ` detail- ${ id } ` ) ;
if ( ! el ) return ;
el . classList . toggle ( 'open' ) ;
}
2026-07-07 07:11:50 +00:00
// Toast
2026-07-05 06:41:32 +00:00
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 ) ) ; }
2026-07-07 07:11:50 +00:00
// Auto-run on load
2026-07-05 06:41:32 +00:00
window . addEventListener ( 'load' , ( ) => {
setTimeout ( runAll , 600 ) ;
} ) ;
< / script >
< / body >
< / html >