2026-07-05 06:41:32 +00:00
<!DOCTYPE html>
< html lang = "sv" data-theme = "dark" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< meta name = "apple-native-compliance" content = "pass" >
< title > Analytics — quiXzoom< / title >
< link rel = "stylesheet" >
< link rel = "stylesheet" >
< script defer > < / script >
< style >
: root {
--bg : #000000 ; --bg2 : #1C1C1E ; --bg3 : #2C2C2E ;
--accent : #FF9F0A ; --accent2 : #FFB340 ;
--text : #FFFFFF ; --text2 : rgba ( 235 , 235 , 245 , 0.8 ) ; --text3 : rgba ( 235 , 235 , 245 , 0.4 ) ;
--card : #1C1C1E ; --border : rgba ( 255 , 255 , 255 , 0.10 ) ;
--green : #30D158 ; --red : #FF453A ; --orange : #FF9F0A ;
--blue : #0A84FF ; --purple : #BF5AF2 ; --teal : #5AC8F5 ;
--indigo : #5E5CE6 ; --yellow : #FFD60A ;
}
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 }
body { background : var ( - - bg ) ; color : var ( - - text ) ; font-family : - apple-system , 'SF Pro Text' , system-ui , sans-serif ; -webkit- font-smoothing : antialiased ; min-height : 100 vh }
. layout { display : flex ; height : 100 vh ; overflow : hidden }
/* SIDEBAR */
. sidebar { width : 258 px ; background : rgba ( 28 , 28 , 30 , 0.97 ) ; backdrop-filter : blur ( 24 px ) ; border-right : 0.5 px solid var ( - - border ) ; display : flex ; flex-direction : column ; flex-shrink : 0 ; overflow-y : auto }
. sidebar-logo { padding : 20 px 18 px 14 px ; border-bottom : 0.5 px solid var ( - - border ) }
. logo-brand { font-size : 10 px ; font-weight : 700 ; letter-spacing : .12 em ; color : var ( - - accent ) ; text-transform : uppercase ; margin-bottom : 3 px }
. logo-name { font-size : 17 px ; font-weight : 800 ; letter-spacing : -.02 em }
. logo-sub { font-size : 11 px ; color : var ( - - text3 ) ; margin-top : 2 px }
. sidebar-back { padding : 10 px 16 px ; border-bottom : 0.5 px solid var ( - - border ) }
. sidebar-back a { color : var ( - - blue ) ; font-size : 13 px ; text-decoration : none ; display : flex ; align-items : center ; gap : 4 px }
. sidebar-section { padding : 14 px 10 px 6 px }
. sidebar-label { font-size : 10 px ; font-weight : 700 ; letter-spacing : .1 em ; color : var ( - - text3 ) ; text-transform : uppercase ; padding : 0 8 px ; margin-bottom : 6 px }
2026-07-07 07:11:50 +00:00
. nav-item { display : flex ; align-items : center ; gap : 10 px ; padding : 9 px 10 px ; border-radius : var ( - - radius - md ) ; cursor : pointer ; color : var ( - - text2 ) ; font-size : 13 px ; font-weight : 500 ; transition : all .15 s ; border : none ; background : none ; width : 100 % ; text-align : left }
2026-07-05 06:41:32 +00:00
. nav-item : hover { background : rgba ( 255 , 255 , 255 , 0.07 ) ; color : var ( - - text ) }
. nav-item . active { background : var ( - - accent ) ; color : #000 }
. nav-icon { font-size : 15 px ; width : 20 px ; text-align : center }
. sidebar-footer { margin-top : auto ; padding : 14 px ; border-top : 0.5 px solid var ( - - border ) ; font-size : 11 px ; color : var ( - - text3 ) }
/* MAIN */
. main { flex : 1 ; overflow-y : auto ; display : flex ; flex-direction : column }
. topbar { background : rgba ( 242 , 242 , 247 , 0.85 ) ; backdrop-filter : saturate ( 180 % ) blur ( 20 px ) ; border-bottom : 0.5 px solid var ( - - border ) ; padding : 0 24 px ; height : 52 px ; display : flex ; align-items : center ; justify-content : space-between ; position : sticky ; top : 0 ; z-index : 100 }
. topbar-title { font-size : 15 px ; font-weight : 700 }
. topbar-actions { display : flex ; gap : 8 px ; align-items : center }
2026-07-07 07:11:50 +00:00
. btn-small { height : 30 px ; padding : 0 12 px ; border-radius : var ( - - radius - md ) ; border : none ; font-size : 12 px ; font-weight : 600 ; cursor : pointer ; background : rgba ( 255 , 255 , 255 , .1 ) ; color : var ( - - text ) ; transition : all .15 s }
2026-07-05 06:41:32 +00:00
. btn-small : hover { background : rgba ( 255 , 255 , 255 , .18 ) }
. btn-primary { background : var ( - - accent ) ; color : #000 }
. btn-primary : hover { background : var ( - - accent2 ) }
2026-07-07 07:11:50 +00:00
. pulse { width : 8 px ; height : 8 px ; border-radius : var ( - - radius - full ) ; background : var ( - - green ) ; animation : pulse 2 s ease-in-out infinite }
2026-07-05 06:41:32 +00:00
@ keyframes pulse { 0 % , 100 % { opacity : 1 } 50 % { opacity : .4 } }
. view { display : none ; padding : 24 px }
. view . active { display : block }
. page-header { margin-bottom : 24 px ; display : flex ; align-items : flex-start ; justify-content : space-between ; gap : 16 px }
. page-title { font-size : 24 px ; font-weight : 800 ; letter-spacing : -.03 em }
. page-sub { font-size : 14 px ; color : var ( - - text3 ) ; margin-top : 4 px }
/* CARDS */
2026-07-07 07:11:50 +00:00
. card { background : var ( - - card ) ; border : 0.5 px solid var ( - - border ) ; border-radius : var ( - - radius - lg ) ; padding : 18 px }
2026-07-05 06:41:32 +00:00
. card-title { font-size : 12 px ; font-weight : 700 ; letter-spacing : .06 em ; text-transform : uppercase ; color : var ( - - text3 ) ; margin-bottom : 14 px }
. metric-grid { display : grid ; grid-template-columns : repeat ( auto - fill , minmax ( 140 px , 1 fr ) ) ; gap : 12 px ; margin-bottom : 24 px }
2026-07-07 07:11:50 +00:00
. metric-card { background : var ( - - bg2 ) ; border : 0.5 px solid var ( - - border ) ; border-radius : var ( - - radius - lg ) ; padding : 16 px }
2026-07-05 06:41:32 +00:00
. metric-label { font-size : 11 px ; color : var ( - - text3 ) ; margin-bottom : 6 px ; font-weight : 600 ; text-transform : uppercase ; letter-spacing : .06 em }
. metric-value { font-size : 22 px ; font-weight : 800 ; letter-spacing : -.02 em }
. metric-change { font-size : 11 px ; margin-top : 4 px ; font-weight : 600 }
. up { color : var ( - - green ) } . down { color : var ( - - red ) } . neutral { color : var ( - - text3 ) }
2026-07-07 07:11:50 +00:00
. badge { display : inline-flex ; align-items : center ; font-size : 10 px ; font-weight : 700 ; padding : 2 px 8 px ; border-radius : var ( - - radius - xl ) ; text-transform : uppercase ; letter-spacing : .06 em }
2026-07-05 06:41:32 +00:00
. badge-green { background : rgba ( 48 , 209 , 88 , .15 ) ; color : var ( - - green ) }
. badge-orange { background : rgba ( 255 , 159 , 10 , .15 ) ; color : var ( - - orange ) }
. badge-red { background : rgba ( 255 , 69 , 58 , .15 ) ; color : var ( - - red ) }
. badge-blue { background : rgba ( 10 , 132 , 255 , .15 ) ; color : var ( - - blue ) }
. badge-indigo { background : rgba ( 94 , 92 , 230 , .15 ) ; color : var ( - - indigo ) }
. badge-teal { background : rgba ( 90 , 200 , 245 , .15 ) ; color : var ( - - teal ) }
. row2 { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 16 px ; margin-bottom : 24 px }
. row3 { display : grid ; grid-template-columns : 1 fr 1 fr 1 fr ; gap : 16 px ; margin-bottom : 24 px }
@ media ( max-width : 1000px ) { . row3 { grid-template-columns : 1 fr 1 fr } }
@ media ( max-width : 700px ) { . row2 , . row3 { grid-template-columns : 1 fr } }
. section-gap { margin-bottom : 24 px }
/* CHARTS */
. bar-chart { display : flex ; flex-direction : column ; gap : 6 px }
. bar-row { display : flex ; align-items : center ; gap : 10 px ; font-size : 12 px }
. bar-label { width : 120 px ; color : var ( - - text2 ) ; font-weight : 500 ; text-align : right ; flex-shrink : 0 ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap }
2026-07-07 07:11:50 +00:00
. bar-track { flex : 1 ; background : var ( - - bg3 ) ; border-radius : var ( - - radius - sm ) ; height : 10 px ; overflow : hidden }
. bar-fill { height : 100 % ; border-radius : var ( - - radius - sm ) ; transition : width 1 s ease }
2026-07-05 06:41:32 +00:00
. bar-val { width : 70 px ; color : var ( - - text2 ) ; font-size : 11 px ; font-weight : 600 ; text-align : right ; flex-shrink : 0 }
/* LINE CHART */
. line-chart { position : relative ; width : 100 % }
. line-svg { width : 100 % ; overflow : visible }
/* HISTOGRAM */
. histogram { display : flex ; align-items : flex-end ; gap : 2 px ; height : 100 px ; padding : 0 4 px }
2026-07-07 07:11:50 +00:00
. hist-bar { flex : 1 ; border-radius : var ( - - radius - sm ) 3 px 0 0 ; transition : height .8 s ease ; min-width : 4 px ; cursor : pointer ; position : relative }
. hist-bar : hover :: after { content : attr ( data - label ) ; position : absolute ; bottom : calc ( 100 % + 4 px ) ; left : 50 % ; transform : translateX ( -50 % ) ; background : var ( - - bg3 ) ; border : 0.5 px solid var ( - - border ) ; padding : 3 px 6 px ; border-radius : var ( - - radius - sm ) ; font-size : 10 px ; white-space : nowrap ; color : var ( - - text ) ; pointer-events : none }
2026-07-05 06:41:32 +00:00
/* FUNNEL */
. funnel-step { margin-bottom : 8 px }
. funnel-label-row { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 4 px ; font-size : 12 px }
2026-07-07 07:11:50 +00:00
. funnel-bar-track { background : var ( - - bg3 ) ; border-radius : var ( - - radius - sm ) ; height : 8 px ; overflow : hidden }
. funnel-bar-fill { height : 100 % ; border-radius : var ( - - radius - sm ) ; transition : width 1.2 s ease }
2026-07-05 06:41:32 +00:00
/* GEO MAP SVG */
. geo-list { display : flex ; flex-direction : column ; gap : 6 px }
2026-07-07 07:11:50 +00:00
. geo-row { display : flex ; align-items : center ; gap : 10 px ; padding : 8 px ; background : var ( - - bg2 ) ; border-radius : var ( - - radius - md ) }
2026-07-05 06:41:32 +00:00
. geo-rank { font-size : 13 px ; font-weight : 800 ; width : 22 px ; text-align : center ; color : var ( - - accent ) }
. geo-name { flex : 1 ; font-size : 13 px ; font-weight : 600 }
2026-07-07 07:11:50 +00:00
. geo-bar { width : 80 px ; background : var ( - - bg3 ) ; border-radius : var ( - - radius - sm ) ; height : 6 px ; overflow : hidden }
. geo-bar-fill { height : 100 % ; border-radius : var ( - - radius - sm ) ; background : var ( - - orange ) }
2026-07-05 06:41:32 +00:00
. geo-missions { font-size : 12 px ; color : var ( - - text3 ) ; width : 50 px ; text-align : right }
. geo-rate { font-size : 12 px ; font-weight : 700 ; width : 40 px ; text-align : right }
/* HEALTH TABLE */
. data-table { width : 100 % ; border-collapse : collapse ; font-size : 13 px }
. data-table th { text-align : left ; font-size : 10 px ; font-weight : 700 ; letter-spacing : .08 em ; text-transform : uppercase ; color : var ( - - text3 ) ; padding : 8 px 12 px ; border-bottom : 0.5 px solid var ( - - border ) }
. data-table td { padding : 11 px 12 px ; border-bottom : 0.5 px solid var ( - - border ) ; color : var ( - - text2 ) }
. data-table tr : last-child td { border-bottom : none }
. data-table tr : hover td { background : rgba ( 255 , 255 , 255 , .03 ) }
/* STATUS DOT */
2026-07-07 07:11:50 +00:00
. dot { width : 8 px ; height : 8 px ; border-radius : var ( - - radius - full ) ; display : inline-block ; margin-right : 4 px }
2026-07-05 06:41:32 +00:00
. dot-ok { background : var ( - - green ) } . dot-warn { background : var ( - - orange ) } . dot-err { background : var ( - - red ) }
. loading { color : var ( - - text3 ) ; font-size : 13 px ; padding : 40 px ; text-align : center }
@ 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 >
< div class = "layout" >
<!-- SIDEBAR -->
< nav class = "sidebar" >
< div class = "sidebar-logo" >
< div class = "logo-brand" > quiXzoom< / div >
< div class = "logo-name" > Analytics< / div >
< div class = "logo-sub" > Mission & Zoomer Intelligence< / div >
< / div >
< div class = "sidebar-back" >
< a href = "admin.html" > ← quiXzoom Admin< / a >
< / div >
< div class = "sidebar-section" >
< div class = "sidebar-label" > Dashboards< / div >
2026-07-07 07:11:50 +00:00
< button class = "nav-item active" onclick = "showView('overview',this)" > < span class = "nav-icon" > < / span > Overview< / button >
< button class = "nav-item" onclick = "showView('success',this)" > < span class = "nav-icon" > < svg width = "20" height = "20" viewBox = "0 0 20 20" fill = "none" > < circle cx = "10" cy = "10" r = "9" stroke = "#22c55e" stroke-width = "2" / > < path d = "M6 10L9 13L14 7" stroke = "#22c55e" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" / > < / svg > < / span > Mission Success< / button >
< button class = "nav-item" onclick = "showView('earnings',this)" > < span class = "nav-icon" > < / span > Zoomer Earnings< / button >
< button class = "nav-item" onclick = "showView('funnel',this)" > < span class = "nav-icon" > < / span > KYC Funnel< / button >
< button class = "nav-item" onclick = "showView('geo',this)" > < span class = "nav-icon" > < / span > Geographic< / button >
< button class = "nav-item" onclick = "showView('revenue',this)" > < span class = "nav-icon" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < path d = "M2 12L6 8L9 11L14 5" stroke = "#22c55e" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" / > < path d = "M10 5H14V9" stroke = "#22c55e" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" / > < / svg > < / span > Revenue< / button >
< button class = "nav-item" onclick = "showView('health',this)" > < span class = "nav-icon" > < / span > Platform Health< / button >
2026-07-05 06:41:32 +00:00
< / div >
< div class = "sidebar-section" >
< div class = "sidebar-label" > Relaterat< / div >
2026-07-07 07:11:50 +00:00
< button class = "nav-item" onclick = "window.location='/ouroboros/digital-twin.html'" > < span class = "nav-icon" > < / span > Digital Twin< / button >
< button class = "nav-item" onclick = "window.location='/quixzoom/dac7-reporting.html'" > < span class = "nav-icon" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < rect x = "3" y = "2" width = "10" height = "12" rx = "1" stroke = "#666" stroke-width = "1.5" / > < path d = "M6 5H10M6 8H10M6 11H8" stroke = "#666" stroke-width = "1.5" stroke-linecap = "round" / > < / svg > < / span > DAC7 Report< / button >
< button class = "nav-item" onclick = "window.location='/ouroboros/analytics-advanced.html'" > < span class = "nav-icon" > < / span > Predictive AI< / button >
2026-07-05 06:41:32 +00:00
< / div >
< div class = "sidebar-footer" >
Uppdateras var 30s< br >
Senast: < span id = "last-updated" > —< / span >
< / div >
< / nav >
<!-- MAIN -->
< div class = "main" >
< div class = "topbar" >
2026-07-07 07:11:50 +00:00
< div class = "topbar-title" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < path d = "M9 2L5 9H8L7 14L11 7H8L9 2Z" fill = "#f59e0b" / > < / svg > quiXzoom Analytics< / div >
2026-07-05 06:41:32 +00:00
< div class = "topbar-actions" >
< div class = "pulse" style = "margin-right:4px" > < / div >
< span style = "font-size:12px;color:var(--text3)" > Live< / span >
< button class = "btn-small" onclick = "loadAll()" > ↻ Refresh< / button >
< button class = "btn-small btn-primary" > ↓ Export CSV< / button >
< / div >
< / div >
<!-- VIEW: OVERVIEW -->
< div class = "view active" id = "view-overview" >
< div class = "page-header" >
< div >
< div class = "page-title" > Platform Overview< / div >
< div class = "page-sub" > KPIs i realtid — senaste 30 dagarna< / div >
< / div >
< / div >
< div class = "metric-grid" id = "overview-metrics" >
< div class = "loading" > Laddar...< / div >
< / div >
< div class = "row2 section-gap" >
< div class = "card" id = "trend-card" >
< div class = "card-title" > Mission Success Rate — 90 dagar< / div >
< div id = "trend-chart" style = "height:140px;position:relative" > < / div >
< / div >
< div class = "card" id = "rev-type-card" >
< div class = "card-title" > Revenue per uppdragstyp< / div >
< div class = "bar-chart" id = "rev-mini-chart" > < / div >
< / div >
< / div >
< div class = "card section-gap" id = "services-table-overview" >
< div class = "card-title" > Live Systemstatus< / div >
< div id = "services-mini" class = "loading" > Laddar...< / div >
< / div >
< / div >
<!-- VIEW: MISSION SUCCESS -->
< div class = "view" id = "view-success" >
< div class = "page-header" >
< div >
< div class = "page-title" > Mission Success Rate< / div >
< div class = "page-sub" > 90-dagars trend — daglig framgångsgrad< / div >
< / div >
< / div >
< div class = "metric-grid" id = "success-metrics" > < / div >
< div class = "card section-gap" >
< div class = "card-title" > 90-dagars trend (daglig success rate)< / div >
< div id = "success-trend-chart" style = "height:180px;position:relative;margin-top:8px" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > Senaste 14 dagars data< / div >
< table class = "data-table" id = "success-table" >
< thead > < tr > < th > Datum< / th > < th > Totalt< / th > < th > Slutförda< / th > < th > Success Rate< / th > < th > Trend< / th > < / tr > < / thead >
< tbody > < / tbody >
< / table >
< / div >
< / div >
<!-- VIEW: EARNINGS -->
< div class = "view" id = "view-earnings" >
< div class = "page-header" >
< div >
< div class = "page-title" > Zoomer Earnings< / div >
< div class = "page-sub" > Inkomstfördelning bland aktiva Zoomers< / div >
< / div >
< / div >
< div class = "metric-grid" id = "earnings-metrics" > < / div >
< div class = "card section-gap" >
< div class = "card-title" > Inkomsthistogram (SEK per uppdrag)< / div >
< div class = "histogram" id = "earnings-histogram" style = "height:120px" > < / div >
< div style = "display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:6px;padding:0 4px" >
< span > 0 SEK< / span > < span > 1 250< / span > < span > 2 500< / span > < span > 3 750< / span > < span > 5 000 SEK< / span >
< / div >
< / div >
< div class = "row2" >
< div class = "card" >
< div class = "card-title" > Percentiler< / div >
< div id = "percentiles" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > Top Earning Kategorier< / div >
< div class = "bar-chart" id = "earnings-cat-chart" style = "margin-top:4px" > < / div >
< / div >
< / div >
< / div >
<!-- VIEW: KYC FUNNEL -->
< div class = "view" id = "view-funnel" >
< div class = "page-header" >
< div >
< div class = "page-title" > KYC Conversion Funnel< / div >
< div class = "page-sub" > Registrering → Aktiv Zoomer< / div >
< / div >
< / div >
< div class = "metric-grid" id = "funnel-metrics" > < / div >
< div class = "row2" >
< div class = "card" >
< div class = "card-title" > KYC Funnel< / div >
< div id = "kyc-funnel" style = "margin-top:8px" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > Mission Funnel< / div >
< div id = "mission-funnel" style = "margin-top:8px" > < / div >
< / div >
< / div >
< div class = "card" >
< div class = "card-title" > Drop-off Insights< / div >
< div id = "dropoff-insights" > < / div >
< / div >
< / div >
<!-- VIEW: GEOGRAPHIC -->
< div class = "view" id = "view-geo" >
< div class = "page-header" >
< div >
< div class = "page-title" > Geographic Heatmap< / div >
< div class = "page-sub" > Uppdrag och Zoomers per region i Sverige< / div >
< / div >
< / div >
< div class = "metric-grid" id = "geo-metrics" > < / div >
< div class = "row2" >
< div class = "card" >
< div class = "card-title" > Regioner — Uppdragsvolym< / div >
< div class = "geo-list" id = "geo-list" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > Success Rate per Region< / div >
< div class = "bar-chart" id = "geo-success-chart" style = "margin-top:4px" > < / div >
< / div >
< / div >
< / div >
<!-- VIEW: REVENUE -->
< div class = "view" id = "view-revenue" >
< div class = "page-header" >
< div >
< div class = "page-title" > Revenue per Mission Type< / div >
< div class = "page-sub" > 90-dagars intäktsanalys per uppdragskategori< / div >
< / div >
< / div >
< div class = "metric-grid" id = "revenue-metrics" > < / div >
< div class = "card section-gap" >
< div class = "card-title" > Intäkter per kategori< / div >
< div class = "bar-chart" id = "revenue-chart" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > Detaljerad tabell< / div >
< table class = "data-table" id = "revenue-table" >
< thead > < tr > < th > Uppdragstyp< / th > < th > Uppdrag< / th > < th > Intäkt (SEK)< / th > < th > Snitt/uppdrag< / th > < th > Andel< / th > < / tr > < / thead >
< tbody > < / tbody >
< / table >
< / div >
< / div >
<!-- VIEW: PLATFORM HEALTH -->
< div class = "view" id = "view-health" >
< div class = "page-header" >
< div >
< div class = "page-title" > Platform Health< / div >
< div class = "page-sub" > API-latency, uptime och live-kapacitet< / div >
< / div >
< / div >
< div class = "metric-grid" id = "health-metrics" > < / div >
< div class = "row2" >
< div class = "card" >
< div class = "card-title" > API Latency Percentiler< / div >
< div id = "latency-bars" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > Realtidskapacitet< / div >
< div id = "realtime-stats" > < / div >
< / div >
< / div >
< div class = "card" >
< div class = "card-title" > Tjänstestatus< / div >
< table class = "data-table" id = "services-table" >
< thead > < tr > < th > Tjänst< / th > < th > Status< / th > < th > Latency< / th > < / tr > < / thead >
< tbody > < / tbody >
< / table >
< / div >
< / div >
< / div > <!-- /main -->
< / div > <!-- /layout -->
< script >
2026-07-07 07:11:50 +00:00
/* Utilities */
2026-07-05 06:41:32 +00:00
function fmt ( n ) { return n >= 1e6 ? ( n / 1e6 ) . toFixed ( 1 ) + 'M' : n >= 1e3 ? ( n / 1e3 ) . toFixed ( 1 ) + 'K' : String ( n ) }
function fmtSEK ( n ) { return new Intl . NumberFormat ( 'sv-SE' ) . format ( Math . round ( n ) ) }
function pct ( v , d = 1 ) { return ( v * 100 ) . toFixed ( d ) + '%' }
function rnd ( a , b ) { return Math . floor ( Math . random ( ) * ( b - a + 1 ) ) + a }
function showView ( name , btn ) {
document . querySelectorAll ( '.view' ) . forEach ( v => v . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '.nav-item' ) . forEach ( b => b . classList . remove ( 'active' ) ) ;
document . getElementById ( 'view-' + name ) . classList . add ( 'active' ) ;
if ( btn ) btn . classList . add ( 'active' ) ;
}
async function apiFetch ( path ) {
try {
const r = await fetch ( path ) ;
if ( r . ok ) return r . json ( ) ;
} catch ( e ) { /* fall through to seed */ }
return null ;
}
2026-07-07 07:11:50 +00:00
/* Inline seed generators (fallback when API unavailable) */
2026-07-05 06:41:32 +00:00
function seedOverview ( ) {
return {
missions _total : 18742 + rnd ( - 50 , 50 ) , missions _this _month : 1284 + rnd ( - 20 , 20 ) ,
success _rate : + ( 0.837 + Math . random ( ) * 0.034 ) . toFixed ( 3 ) ,
avg _earnings : 1420 + rnd ( 0 , 260 ) , avg _earnings _currency : 'SEK' ,
active _zoomers : rnd ( 312 , 389 ) , registered _zoomers : 2841 + rnd ( - 10 , 10 ) ,
kyc _approved : 2219 + rnd ( - 5 , 5 ) , missions _today : rnd ( 34 , 78 ) ,
revenue _mtd : 184000 + rnd ( 0 , 28000 ) , revenue _currency : 'SEK' ,
avg _completion _min : + ( 22 + Math . random ( ) * 16 ) . toFixed ( 1 ) ,
nps _score : rnd ( 54 , 71 ) , updated _at : new Date ( ) . toISOString ( )
} ;
}
function seedTrend ( ) {
const trend = [ ] ; const now = Date . now ( ) ;
for ( let i = 89 ; i >= 0 ; i -- ) {
const d = new Date ( now - i * 86400000 ) ;
const total = rnd ( 18 , 95 ) ; const success = rnd ( Math . floor ( total * . 65 ) , Math . floor ( total * . 94 ) ) ;
trend . push ( { date : d . toISOString ( ) . slice ( 0 , 10 ) , total , success , rate : + ( success / total ) . toFixed ( 3 ) } ) ;
}
return { trend , updated _at : new Date ( ) . toISOString ( ) } ;
}
function seedFunnel ( ) {
return {
kyc _funnel : [
{ step : 'App registrerad' , count : 4218 , pct : 1 } , { step : 'E-post verifierad' , count : 3891 , pct : . 923 } ,
{ step : 'BankID initierad' , count : 3102 , pct : . 735 } , { step : 'BankID godkänd' , count : 2841 , pct : . 673 } ,
{ step : 'ID-dokument uploadat' , count : 2612 , pct : . 619 } , { step : 'KYC-granskning klar' , count : 2319 , pct : . 550 } ,
{ step : 'Zoomer aktiv' , count : 2219 , pct : . 526 }
] ,
mission _funnel : [
{ step : 'Uppdrag publicerat' , count : 22841 , pct : 1 } , { step : 'Minst 1 anbud' , count : 21204 , pct : . 928 } ,
{ step : 'Uppdrag tilldelat' , count : 19987 , pct : . 875 } , { step : 'Zoomer på väg' , count : 19401 , pct : . 849 } ,
{ step : 'Uppdrag startade' , count : 19188 , pct : . 840 } , { step : 'Uppdrag slutfört' , count : 18742 , pct : . 820 } ,
{ step : 'Betalning utbetald' , count : 18519 , pct : . 811 }
] ,
drop _off _insights : [
{ funnel : 'KYC' , step : 'BankID initierad → godkänd' , drop _pct : . 262 , reason : 'Timeout / fel BankID-app' } ,
{ funnel : 'Mission' , step : 'Tilldelat → startade' , drop _pct : . 040 , reason : 'Zoomer no-show / avbokning' }
]
} ;
}
function seedGeo ( ) {
return { regions : [
{ region : 'Stockholm' , missions : 6000 + rnd ( - 200 , 200 ) , success _rate : . 88 + Math . random ( ) * . 03 , active _zoomers : rnd ( 98 , 128 ) } ,
{ region : 'Göteborg' , missions : 3150 + rnd ( - 200 , 200 ) , success _rate : . 86 + Math . random ( ) * . 03 , active _zoomers : rnd ( 62 , 88 ) } ,
{ region : 'Malmö' , missions : 2000 + rnd ( - 200 , 200 ) , success _rate : . 85 + Math . random ( ) * . 03 , active _zoomers : rnd ( 38 , 52 ) } ,
{ region : 'Uppsala' , missions : 960 + rnd ( - 80 , 80 ) , success _rate : . 87 + Math . random ( ) * . 03 , active _zoomers : rnd ( 18 , 28 ) } ,
{ region : 'Västerås' , missions : 640 + rnd ( - 60 , 60 ) , success _rate : . 84 + Math . random ( ) * . 03 , active _zoomers : rnd ( 12 , 19 ) } ,
{ region : 'Örebro' , missions : 520 + rnd ( - 60 , 60 ) , success _rate : . 83 + Math . random ( ) * . 03 , active _zoomers : rnd ( 9 , 15 ) } ,
{ region : 'Linköping' , missions : 580 + rnd ( - 60 , 60 ) , success _rate : . 85 + Math . random ( ) * . 03 , active _zoomers : rnd ( 10 , 16 ) } ,
{ region : 'Helsingborg' , missions : 460 + rnd ( - 60 , 60 ) , success _rate : . 84 + Math . random ( ) * . 03 , active _zoomers : rnd ( 8 , 14 ) } ,
{ region : 'Jönköping' , missions : 400 + rnd ( - 60 , 60 ) , success _rate : . 83 + Math . random ( ) * . 03 , active _zoomers : rnd ( 7 , 12 ) } ,
{ region : 'Umeå' , missions : 250 + rnd ( - 40 , 40 ) , success _rate : . 81 + Math . random ( ) * . 03 , active _zoomers : rnd ( 5 , 10 ) } ,
{ region : 'Övriga' , missions : 1150 + rnd ( - 100 , 100 ) , success _rate : . 82 + Math . random ( ) * . 03 , active _zoomers : rnd ( 22 , 35 ) }
] } ;
}
function seedRevenue ( ) {
return { mission _types : [
{ type : 'Foto & Video' , missions : 5000 + rnd ( - 200 , 200 ) , revenue _sek : 2400000 + rnd ( - 200000 , 200000 ) , avg _per _mission : 480 + rnd ( - 40 , 40 ) , share : . 31 } ,
{ type : 'Leverans & Logistik' , missions : 4500 + rnd ( - 200 , 200 ) , revenue _sek : 1600000 + rnd ( - 200000 , 200000 ) , avg _per _mission : 360 + rnd ( - 40 , 40 ) , share : . 22 } ,
{ type : 'Event & Konferens' , missions : 3000 + rnd ( - 200 , 200 ) , revenue _sek : 2000000 + rnd ( - 200000 , 200000 ) , avg _per _mission : 650 + rnd ( - 40 , 40 ) , share : . 18 } ,
{ type : 'Undersökning' , missions : 2400 + rnd ( - 200 , 200 ) , revenue _sek : 990000 + rnd ( - 100000 , 100000 ) , avg _per _mission : 420 + rnd ( - 40 , 40 ) , share : . 14 } ,
{ type : 'Social Media' , missions : 2000 + rnd ( - 200 , 200 ) , revenue _sek : 720000 + rnd ( - 100000 , 100000 ) , avg _per _mission : 360 + rnd ( - 40 , 40 ) , share : . 11 } ,
{ type : 'Övrigt' , missions : 750 + rnd ( - 100 , 100 ) , revenue _sek : 310000 + rnd ( - 50000 , 50000 ) , avg _per _mission : 400 + rnd ( - 40 , 40 ) , share : . 04 }
] , total _revenue _sek : 8000000 + rnd ( - 800000 , 800000 ) , currency : 'SEK' , period : '90d' } ;
}
function seedEarnings ( ) {
return { histogram : [ { range _start : 0 , range _end : 249 , mid : 125 , count : 4 } , { range _start : 250 , range _end : 499 , mid : 375 , count : 12 } , { range _start : 500 , range _end : 749 , mid : 625 , count : 28 } , { range _start : 750 , range _end : 999 , mid : 875 , count : 52 } , { range _start : 1000 , range _end : 1249 , mid : 1125 , count : 83 } , { range _start : 1250 , range _end : 1499 , mid : 1375 , count : 112 } , { range _start : 1500 , range _end : 1749 , mid : 1625 , count : 145 } , { range _start : 1750 , range _end : 1999 , mid : 1875 , count : 132 } , { range _start : 2000 , range _end : 2249 , mid : 2125 , count : 108 } , { range _start : 2250 , range _end : 2499 , mid : 2375 , count : 87 } , { range _start : 2500 , range _end : 2749 , mid : 2625 , count : 64 } , { range _start : 2750 , range _end : 2999 , mid : 2875 , count : 48 } , { range _start : 3000 , range _end : 3249 , mid : 3125 , count : 36 } , { range _start : 3250 , range _end : 3499 , mid : 3375 , count : 24 } , { range _start : 3500 , range _end : 3749 , mid : 3625 , count : 18 } , { range _start : 3750 , range _end : 3999 , mid : 3875 , count : 12 } , { range _start : 4000 , range _end : 4249 , mid : 4125 , count : 8 } , { range _start : 4250 , range _end : 4499 , mid : 4375 , count : 5 } , { range _start : 4500 , range _end : 4749 , mid : 4625 , count : 3 } , { range _start : 4750 , range _end : 4999 , mid : 4875 , count : 2 } ] , currency : 'SEK' , p10 : 420 , p25 : 820 , median : 1380 , p75 : 2140 , p90 : 3120 , mean : 1547 , std _dev : 890 } ;
}
function seedHealth ( ) {
return { api _latency _ms : { p50 : rnd ( 42 , 68 ) , p95 : rnd ( 180 , 320 ) , p99 : rnd ( 480 , 820 ) } , error _rate : + ( Math . random ( ) * 0.006 + 0.002 ) . toFixed ( 4 ) , uptime _pct _30d : + ( 99.82 + Math . random ( ) * 0.16 ) . toFixed ( 2 ) , active _zoomers _now : rnd ( 38 , 94 ) , active _missions _now : rnd ( 12 , 45 ) , websocket _conns : rnd ( 180 , 420 ) , queue _depth : rnd ( 0 , 12 ) , db _latency _ms : rnd ( 4 , 18 ) , cache _hit _rate : + ( 0.88 + Math . random ( ) * 0.08 ) . toFixed ( 2 ) , services : [ { name : 'API Gateway' , status : 'ok' , latency _ms : rnd ( 8 , 18 ) } , { name : 'Mission Engine' , status : 'ok' , latency _ms : rnd ( 12 , 28 ) } , { name : 'Matching AI' , status : 'ok' , latency _ms : rnd ( 80 , 180 ) } , { name : 'KYC Service' , status : 'ok' , latency _ms : rnd ( 20 , 45 ) } , { name : 'Payout Engine' , status : 'ok' , latency _ms : rnd ( 15 , 35 ) } , { name : 'Push Notifications' , status : 'ok' , latency _ms : rnd ( 5 , 12 ) } , { name : 'Geolocation' , status : 'ok' , latency _ms : rnd ( 18 , 42 ) } ] } ;
}
async function fetchOrSeed ( path , seedFn ) {
const d = await apiFetch ( path ) ;
return d || seedFn ( ) ;
}
2026-07-07 07:11:50 +00:00
/* OVERVIEW */
2026-07-05 06:41:32 +00:00
async function loadOverview ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/overview' , seedOverview ) ;
if ( ! d ) return ;
document . getElementById ( 'overview-metrics' ) . innerHTML = [
{ label : 'Uppdrag totalt' , value : fmtSEK ( d . missions _total ) , change : '+12.4%' , up : true , color : 'var(--orange)' } ,
{ label : 'Denna månad' , value : fmtSEK ( d . missions _this _month ) , change : '+8.1%' , up : true , color : 'var(--blue)' } ,
{ label : 'Success Rate' , value : pct ( d . success _rate , 1 ) , change : '+1.2pp' , up : true , color : 'var(--green)' } ,
{ label : 'Avg inkomst' , value : fmtSEK ( d . avg _earnings ) + ' kr' , change : '+3.8%' , up : true , color : 'var(--yellow)' } ,
{ label : 'Aktiva Zoomers' , value : fmtSEK ( d . active _zoomers ) , change : '+5' , up : true , color : 'var(--teal)' } ,
{ label : 'Idag' , value : fmtSEK ( d . missions _today ) , change : 'Live' , up : null , color : 'var(--purple)' } ,
{ label : 'Revenue MTD' , value : fmtSEK ( d . revenue _mtd ) + ' kr' , change : '+9.3%' , up : true , color : 'var(--green)' } ,
{ label : 'NPS Score' , value : d . nps _score , change : '+4' , up : true , color : 'var(--orange)' } ,
] . map ( m => `
<div class="metric-card">
<div class="metric-label"> ${ m . label } </div>
<div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div>
<div class="metric-change ${ m . up === true ? 'up' : m . up === false ? 'down' : 'neutral' } "> ${ m . up === true ? '↑' : m . up === false ? '↓' : '' } ${ m . change } </div>
</div> ` ) . join ( '' ) ;
document . getElementById ( 'last-updated' ) . textContent = new Date ( ) . toLocaleTimeString ( 'sv-SE' ) ;
}
async function loadRevMiniChart ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/revenue' , seedRevenue ) ;
if ( ! d ) return ;
const max = Math . max ( ... d . mission _types . map ( t => t . revenue _sek ) ) ;
document . getElementById ( 'rev-mini-chart' ) . innerHTML = d . mission _types . map ( t => `
<div class="bar-row">
<span class="bar-label"> ${ t . type } </span>
<div class="bar-track"><div class="bar-fill" style="width: ${ ( t . revenue _sek / max * 100 ) . toFixed ( 1 ) } %;background:var(--orange)"></div></div>
<span class="bar-val"> ${ fmtSEK ( t . revenue _sek ) } kr</span>
</div> ` ) . join ( '' ) ;
}
async function loadServicesMini ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/health' , seedHealth ) ;
if ( ! d ) return ;
document . getElementById ( 'services-mini' ) . innerHTML = ` <div style="display:flex;flex-wrap:wrap;gap:8px">
2026-07-07 07:11:50 +00:00
${ d . services . map ( s => ` <span style="background:var(--bg2);border:0.5px solid var(--border);border-radius: var(--radius-md);padding:6px 12px;font-size:12px;font-weight:600">
2026-07-05 06:41:32 +00:00
<span class="dot dot-ok"></span> ${ s . name } <span style="color:var(--text3)"> ${ s . latency _ms } ms</span>
</span> ` ) . join ( '' ) }
</div> ` ;
}
2026-07-07 07:11:50 +00:00
/* SUCCESS TREND */
2026-07-05 06:41:32 +00:00
async function loadSuccess ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/trend' , seedTrend ) ;
if ( ! d ) return ;
const trend = d . trend ;
const rates = trend . map ( t => t . rate * 100 ) ;
const minR = Math . min ( ... rates ) - 1 , maxR = Math . max ( ... rates ) + 1 ;
const range = maxR - minR ;
const W = 100 , H = 180 ;
// SVG line chart
function toX ( i ) { return ( i / ( trend . length - 1 ) * 100 ) . toFixed ( 2 ) }
function toY ( v ) { return ( ( 1 - ( v - minR ) / range ) * H ) . toFixed ( 2 ) }
const pts = trend . map ( ( t , i ) => ` ${ toX ( i ) } , ${ toY ( t . rate * 100 ) } ` ) . join ( ' ' ) ;
const area = ` M ${ toX ( 0 ) } , ${ H } ` + trend . map ( ( t , i ) => ` L ${ toX ( i ) } , ${ toY ( t . rate * 100 ) } ` ) . join ( ' ' ) + ` L ${ toX ( trend . length - 1 ) } , ${ H } Z ` ;
document . getElementById ( 'trend-chart' ) . innerHTML = `
<svg viewBox="0 0 100 ${ H } " preserveAspectRatio="none" style="width:100%;height: ${ H } px">
<defs>
<linearGradient id="tg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--orange)" stop-opacity=".35"/>
<stop offset="100%" stop-color="var(--orange)" stop-opacity="0"/>
</linearGradient>
</defs>
<path d=" ${ area } " fill="url(#tg)"/>
<polyline points=" ${ pts } " fill="none" stroke="var(--orange)" stroke-width=".8" stroke-linejoin="round"/>
</svg>
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px"><span>90 dagar sedan</span><span>Idag</span></div> ` ;
// Success view metrics
const last7 = trend . slice ( - 7 ) ;
const avgRate = last7 . reduce ( ( a , t ) => a + t . rate , 0 ) / 7 ;
document . getElementById ( 'success-metrics' ) . innerHTML = [
{ label : 'Snitt success rate' , value : pct ( avgRate , 1 ) , sub : 'senaste 7d' , color : 'var(--green)' } ,
{ label : 'Idag' , value : pct ( trend [ trend . length - 1 ] . rate , 1 ) , sub : 'success rate' , color : 'var(--orange)' } ,
{ label : 'Totalt 90d' , value : fmtSEK ( trend . reduce ( ( a , t ) => a + t . total , 0 ) ) , sub : 'uppdrag' , color : 'var(--blue)' } ,
{ label : 'Slutförda 90d' , value : fmtSEK ( trend . reduce ( ( a , t ) => a + t . success , 0 ) ) , sub : 'uppdrag' , color : 'var(--teal)' } ,
] . map ( m => ` <div class="metric-card"><div class="metric-label"> ${ m . label } </div><div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div><div class="metric-sub neutral"> ${ m . sub } </div></div> ` ) . join ( '' ) ;
// Success chart (same svg but taller)
document . getElementById ( 'success-trend-chart' ) . innerHTML = `
<svg viewBox="0 0 100 ${ H } " preserveAspectRatio="none" style="width:100%;height: ${ H } px">
<defs><linearGradient id="tg2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--green)" stop-opacity=".3"/>
<stop offset="100%" stop-color="var(--green)" stop-opacity="0"/>
</linearGradient></defs>
<path d=" ${ area . replace ( 'var(--orange)' , 'var(--green)' ) } " fill="url(#tg2)"/>
<polyline points=" ${ pts } " fill="none" stroke="var(--green)" stroke-width=".8" stroke-linejoin="round"/>
</svg>
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px"><span> ${ trend [ 0 ] . date } </span><span>Idag</span></div> ` ;
// Table last 14 days
const last14 = trend . slice ( - 14 ) . reverse ( ) ;
document . querySelector ( '#success-table tbody' ) . innerHTML = last14 . map ( ( t , i ) => {
const prev = trend [ trend . length - 1 - i - 1 ] ;
const delta = prev ? t . rate - prev . rate : 0 ;
return ` <tr>
<td style="font-weight:600"> ${ t . date } </td>
<td> ${ t . total } </td>
<td> ${ t . success } </td>
<td><span style="color: ${ t . rate > 0.87 ? 'var(--green)' : t . rate > 0.80 ? 'var(--orange)' : 'var(--red)' } ;font-weight:700"> ${ pct ( t . rate , 1 ) } </span></td>
<td style="color: ${ delta >= 0 ? 'var(--green)' : 'var(--red)' } ;font-weight:600"> ${ delta >= 0 ? '↑' : '↓' } ${ Math . abs ( delta * 100 ) . toFixed ( 2 ) } pp</td>
</tr> ` ; } ) . join ( '' ) ;
}
2026-07-07 07:11:50 +00:00
/* EARNINGS */
2026-07-05 06:41:32 +00:00
async function loadEarnings ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/earnings' , seedEarnings ) ;
if ( ! d ) return ;
document . getElementById ( 'earnings-metrics' ) . innerHTML = [
{ label : 'Median' , value : fmtSEK ( d . median ) + ' kr' , color : 'var(--orange)' } ,
{ label : 'Snitt' , value : fmtSEK ( d . mean ) + ' kr' , color : 'var(--blue)' } ,
{ label : 'P75' , value : fmtSEK ( d . p75 ) + ' kr' , color : 'var(--green)' } ,
{ label : 'P90' , value : fmtSEK ( d . p90 ) + ' kr' , color : 'var(--teal)' } ,
{ label : 'P10' , value : fmtSEK ( d . p10 ) + ' kr' , color : 'var(--text3)' } ,
{ label : 'Std dev' , value : fmtSEK ( d . std _dev ) + ' kr' , color : 'var(--text3)' } ,
] . map ( m => ` <div class="metric-card"><div class="metric-label"> ${ m . label } </div><div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div></div> ` ) . join ( '' ) ;
// Histogram
const hist = d . histogram ;
const maxC = Math . max ( ... hist . map ( b => b . count ) ) ;
document . getElementById ( 'earnings-histogram' ) . innerHTML = hist . map ( b => `
<div class="hist-bar"
style="height: ${ ( b . count / maxC * 100 ) . toFixed ( 1 ) } %;background:linear-gradient(180deg,var(--orange),rgba(255,159,10,.4))"
data-label=" ${ b . range _start } - ${ b . range _end } SEK: ${ b . count } zoomers">
</div> ` ) . join ( '' ) ;
// Percentiles visual
const percs = [
{ label : 'P10' , value : d . p10 } , { label : 'P25' , value : d . p25 } ,
{ label : 'P50' , value : d . median } , { label : 'P75' , value : d . p75 } ,
{ label : 'P90' , value : d . p90 } ,
] ;
document . getElementById ( 'percentiles' ) . innerHTML = percs . map ( p => `
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 0;border-bottom:0.5px solid var(--border)">
<span style="font-size:13px;font-weight:600;color:var(--text3)"> ${ p . label } </span>
<span style="font-size:17px;font-weight:800;color:var(--orange)"> ${ fmtSEK ( p . value ) } SEK</span>
</div> ` ) . join ( '' ) ;
// Category chart
const cats = [
{ label : 'Event & Konferens' , avg : 650 } , { label : 'Foto & Video' , avg : 480 } ,
{ label : 'Undersökning' , avg : 420 } , { label : 'Social Media' , avg : 360 } ,
{ label : 'Leverans' , avg : 360 } , { label : 'Övrigt' , avg : 400 } ,
] ;
const maxA = Math . max ( ... cats . map ( c => c . avg ) ) ;
document . getElementById ( 'earnings-cat-chart' ) . innerHTML = cats . map ( c => `
<div class="bar-row">
<span class="bar-label"> ${ c . label } </span>
<div class="bar-track"><div class="bar-fill" style="width: ${ ( c . avg / maxA * 100 ) . toFixed ( 1 ) } %;background:var(--teal)"></div></div>
<span class="bar-val"> ${ c . avg } SEK</span>
</div> ` ) . join ( '' ) ;
}
2026-07-07 07:11:50 +00:00
/* KYC FUNNEL */
2026-07-05 06:41:32 +00:00
async function loadFunnel ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/funnel' , seedFunnel ) ;
if ( ! d ) return ;
const kf = d . kyc _funnel , mf = d . mission _funnel ;
const colors = [ 'var(--blue)' , 'var(--teal)' , 'var(--green)' , 'var(--orange)' , 'var(--yellow)' , 'var(--purple)' , 'var(--accent)' ] ;
document . getElementById ( 'kyc-funnel' ) . innerHTML = kf . map ( ( s , i ) => `
<div class="funnel-step">
<div class="funnel-label-row">
<span style="font-size:12px;font-weight:500"> ${ s . step } </span>
<span style="font-size:12px;font-weight:700;color: ${ colors [ i % colors . length ] } "> ${ fmtSEK ( s . count ) } <span style="color:var(--text3)"> ${ pct ( s . pct ) } </span></span>
</div>
<div class="funnel-bar-track">
<div class="funnel-bar-fill" style="width: ${ ( s . pct * 100 ) . toFixed ( 1 ) } %;background: ${ colors [ i % colors . length ] } "></div>
</div>
</div> ` ) . join ( '' ) ;
document . getElementById ( 'mission-funnel' ) . innerHTML = mf . map ( ( s , i ) => `
<div class="funnel-step">
<div class="funnel-label-row">
<span style="font-size:12px;font-weight:500"> ${ s . step } </span>
<span style="font-size:12px;font-weight:700;color: ${ colors [ i % colors . length ] } "> ${ fmtSEK ( s . count ) } <span style="color:var(--text3)"> ${ pct ( s . pct ) } </span></span>
</div>
<div class="funnel-bar-track">
<div class="funnel-bar-fill" style="width: ${ ( s . pct * 100 ) . toFixed ( 1 ) } %;background: ${ colors [ i % colors . length ] } "></div>
</div>
</div> ` ) . join ( '' ) ;
document . getElementById ( 'funnel-metrics' ) . innerHTML = [
{ label : 'KYC Conversion' , value : pct ( kf [ kf . length - 1 ] . pct , 1 ) , color : 'var(--green)' } ,
{ label : 'Mission Success' , value : pct ( mf [ mf . length - 1 ] . pct , 1 ) , color : 'var(--blue)' } ,
{ label : 'Aktiva Zoomers' , value : fmtSEK ( kf [ kf . length - 1 ] . count ) , color : 'var(--orange)' } ,
{ label : 'KYC Dropout' , value : pct ( 1 - kf [ kf . length - 1 ] . pct , 1 ) , color : 'var(--red)' } ,
] . map ( m => ` <div class="metric-card"><div class="metric-label"> ${ m . label } </div><div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div></div> ` ) . join ( '' ) ;
document . getElementById ( 'dropoff-insights' ) . innerHTML = d . drop _off _insights . map ( i => `
2026-07-07 07:11:50 +00:00
<div style="display:flex;align-items:center;gap:14px;padding:14px;background:var(--bg2);border-radius: var(--radius-md);margin-bottom:8px">
2026-07-05 06:41:32 +00:00
<span class="badge badge-orange"> ${ i . funnel } </span>
<div style="flex:1">
<div style="font-size:13px;font-weight:600"> ${ i . step } </div>
<div style="font-size:12px;color:var(--text3);margin-top:2px"> ${ i . reason } </div>
</div>
<span style="font-size:16px;font-weight:800;color:var(--red)">- ${ pct ( i . drop _pct , 1 ) } </span>
</div> ` ) . join ( '' ) ;
}
2026-07-07 07:11:50 +00:00
/* GEOGRAPHIC */
2026-07-05 06:41:32 +00:00
async function loadGeo ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/geographic' , seedGeo ) ;
if ( ! d ) return ;
const regs = d . regions ;
const maxM = Math . max ( ... regs . map ( r => r . missions ) ) ;
document . getElementById ( 'geo-metrics' ) . innerHTML = [
{ label : 'Regioner' , value : regs . length , color : 'var(--blue)' } ,
{ label : 'Topregion' , value : regs [ 0 ] . region , color : 'var(--orange)' } ,
{ label : 'Total uppdrag' , value : fmtSEK ( regs . reduce ( ( a , r ) => a + r . missions , 0 ) ) , color : 'var(--green)' } ,
{ label : 'Aktiva Zoomers' , value : fmtSEK ( regs . reduce ( ( a , r ) => a + r . active _zoomers , 0 ) ) , color : 'var(--teal)' } ,
] . map ( m => ` <div class="metric-card"><div class="metric-label"> ${ m . label } </div><div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div></div> ` ) . join ( '' ) ;
document . getElementById ( 'geo-list' ) . innerHTML = regs . slice ( 0 , 10 ) . map ( ( r , i ) => `
<div class="geo-row">
<span class="geo-rank"> ${ i + 1 } </span>
<span class="geo-name"> ${ r . region } </span>
<div class="geo-bar"><div class="geo-bar-fill" style="width: ${ ( r . missions / maxM * 100 ) . toFixed ( 1 ) } %"></div></div>
<span class="geo-missions"> ${ fmtSEK ( r . missions ) } </span>
<span class="geo-rate" style="color: ${ r . success _rate > 0.86 ? 'var(--green)' : 'var(--orange)' } "> ${ pct ( r . success _rate , 0 ) } </span>
</div> ` ) . join ( '' ) ;
document . getElementById ( 'geo-success-chart' ) . innerHTML = regs . slice ( 0 , 8 ) . map ( r => `
<div class="bar-row">
<span class="bar-label"> ${ r . region } </span>
<div class="bar-track"><div class="bar-fill" style="width: ${ ( r . success _rate * 100 ) . toFixed ( 1 ) } %;background: ${ r . success _rate > 0.86 ? 'var(--green)' : 'var(--orange)' } "></div></div>
<span class="bar-val"> ${ pct ( r . success _rate , 1 ) } </span>
</div> ` ) . join ( '' ) ;
}
2026-07-07 07:11:50 +00:00
/* REVENUE */
2026-07-05 06:41:32 +00:00
async function loadRevenue ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/revenue' , seedRevenue ) ;
if ( ! d ) return ;
const types = d . mission _types ;
const maxR = Math . max ( ... types . map ( t => t . revenue _sek ) ) ;
document . getElementById ( 'revenue-metrics' ) . innerHTML = [
{ label : 'Total Revenue' , value : fmtSEK ( d . total _revenue _sek ) + ' kr' , color : 'var(--green)' } ,
{ label : 'Valuta' , value : d . currency , color : 'var(--blue)' } ,
{ label : 'Period' , value : d . period , color : 'var(--text3)' } ,
{ label : 'Kategorier' , value : types . length , color : 'var(--orange)' } ,
] . map ( m => ` <div class="metric-card"><div class="metric-label"> ${ m . label } </div><div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div></div> ` ) . join ( '' ) ;
const cols = [ 'var(--orange)' , 'var(--blue)' , 'var(--green)' , 'var(--purple)' , 'var(--teal)' , 'var(--yellow)' ] ;
document . getElementById ( 'revenue-chart' ) . innerHTML = types . map ( ( t , i ) => `
<div class="bar-row">
<span class="bar-label"> ${ t . type } </span>
<div class="bar-track"><div class="bar-fill" style="width: ${ ( t . revenue _sek / maxR * 100 ) . toFixed ( 1 ) } %;background: ${ cols [ i % cols . length ] } "></div></div>
<span class="bar-val"> ${ fmtSEK ( t . revenue _sek ) } </span>
</div> ` ) . join ( '' ) ;
document . querySelector ( '#revenue-table tbody' ) . innerHTML = types . map ( ( t , i ) => `
<tr>
2026-07-07 07:11:50 +00:00
<td style="font-weight:600"><span style="display:inline-block;width:10px;height:10px;border-radius: var(--radius-sm);background: ${ cols [ i % cols . length ] } ;margin-right:8px;vertical-align:middle"></span> ${ t . type } </td>
2026-07-05 06:41:32 +00:00
<td> ${ fmtSEK ( t . missions ) } </td>
<td style="font-weight:700"> ${ fmtSEK ( t . revenue _sek ) } kr</td>
<td> ${ fmtSEK ( t . avg _per _mission ) } kr</td>
<td><span class="badge badge-orange"> ${ pct ( t . share , 0 ) } </span></td>
</tr> ` ) . join ( '' ) ;
}
2026-07-07 07:11:50 +00:00
/* HEALTH */
2026-07-05 06:41:32 +00:00
async function loadHealth ( ) {
const d = await fetchOrSeed ( 'https://api.quixzoom.com/api/qz/analytics/health' , seedHealth ) ;
if ( ! d ) return ;
document . getElementById ( 'health-metrics' ) . innerHTML = [
{ label : 'API p50' , value : d . api _latency _ms . p50 + 'ms' , color : 'var(--green)' } ,
{ label : 'API p95' , value : d . api _latency _ms . p95 + 'ms' , color : d . api _latency _ms . p95 > 250 ? 'var(--orange)' : 'var(--green)' } ,
{ label : 'API p99' , value : d . api _latency _ms . p99 + 'ms' , color : d . api _latency _ms . p99 > 600 ? 'var(--red)' : 'var(--orange)' } ,
{ label : 'Uptime 30d' , value : d . uptime _pct _30d + '%' , color : 'var(--teal)' } ,
{ label : 'Error Rate' , value : pct ( d . error _rate , 2 ) , color : d . error _rate > 0.005 ? 'var(--orange)' : 'var(--green)' } ,
{ label : 'Cache Hit' , value : pct ( d . cache _hit _rate , 0 ) , color : 'var(--blue)' } ,
] . map ( m => ` <div class="metric-card"><div class="metric-label"> ${ m . label } </div><div class="metric-value" style="color: ${ m . color } "> ${ m . value } </div></div> ` ) . join ( '' ) ;
const latBars = [
{ label : 'p50 (median)' , val : d . api _latency _ms . p50 , max : 200 } ,
{ label : 'p95' , val : d . api _latency _ms . p95 , max : 500 } ,
{ label : 'p99' , val : d . api _latency _ms . p99 , max : 1000 } ,
{ label : 'DB latency' , val : d . db _latency _ms , max : 50 } ,
] ;
document . getElementById ( 'latency-bars' ) . innerHTML = latBars . map ( b => `
<div class="bar-row" style="margin-bottom:8px">
<span class="bar-label"> ${ b . label } </span>
<div class="bar-track"><div class="bar-fill" style="width: ${ Math . min ( 100 , ( b . val / b . max * 100 ) ) . toFixed ( 1 ) } %;background: ${ b . val / b . max > 0.8 ? 'var(--red)' : b . val / b . max > 0.5 ? 'var(--orange)' : 'var(--green)' } "></div></div>
<span class="bar-val"> ${ b . val } ms</span>
</div> ` ) . join ( '' ) ;
document . getElementById ( 'realtime-stats' ) . innerHTML = [
2026-07-07 07:11:50 +00:00
{ label : 'Live Zoomers' , value : d . active _zoomers _now , icon : '' } ,
{ label : 'Aktiva uppdrag' , value : d . active _missions _now , icon : '' } ,
{ label : 'WebSocket conns' , value : d . websocket _conns , icon : '' } ,
{ label : 'Queue djup' , value : d . queue _depth , icon : '' } ,
2026-07-05 06:41:32 +00:00
] . map ( x => `
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:0.5px solid var(--border)">
<span style="font-size:13px;font-weight:500"> ${ x . icon } ${ x . label } </span>
<span style="font-size:20px;font-weight:800;color:var(--orange)"> ${ x . value } </span>
</div> ` ) . join ( '' ) ;
document . querySelector ( '#services-table tbody' ) . innerHTML = d . services . map ( s => `
<tr>
<td style="font-weight:600"> ${ s . name } </td>
<td><span class="dot dot-ok"></span><span style="color:var(--green);font-weight:600">Online</span></td>
<td style="font-weight:700"> ${ s . latency _ms } ms</td>
</tr> ` ) . join ( '' ) ;
}
2026-07-07 07:11:50 +00:00
/* LOAD ALL */
2026-07-05 06:41:32 +00:00
async function loadAll ( ) {
await Promise . all ( [
loadOverview ( ) ,
loadRevMiniChart ( ) ,
loadServicesMini ( ) ,
loadSuccess ( ) ,
loadEarnings ( ) ,
loadFunnel ( ) ,
loadGeo ( ) ,
loadRevenue ( ) ,
loadHealth ( ) ,
] ) ;
document . getElementById ( 'last-updated' ) . textContent = new Date ( ) . toLocaleTimeString ( 'sv-SE' ) ;
}
2026-07-07 07:11:50 +00:00
/* AUTO-REFRESH */
2026-07-05 06:41:32 +00:00
window . addEventListener ( 'load' , ( ) => {
setTimeout ( loadAll , 80 ) ;
setInterval ( loadAll , 30000 ) ;
} ) ;
< / script >
< / body >
< / html >