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 — 3D Atlas< / title >
< link rel = "stylesheet" >
< link rel = "stylesheet" >
< style >
* { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body {
font-family : var ( - - font - text ) ;
background : var ( - - grouped - background - primary ) ;
color : var ( - - label - primary ) ;
min-height : 100 vh ;
padding-bottom : env ( safe - area - inset - bottom ) ;
}
. nav-bar {
position : sticky ; top : 0 ; z-index : 100 ;
background : var ( - - glass - bg ) ;
backdrop-filter : var ( - - glass - blur ) ;
-webkit- backdrop-filter : var ( - - glass - blur ) ;
border-bottom : 0.5 px solid var ( - - separator ) ;
padding : 12 px 20 px ;
padding-top : calc ( 12 px + env ( safe - area - inset - top ) ) ;
display : flex ; align-items : center ; justify-content : space-between ;
}
. nav-title { font-size : 17 px ; font-weight : 600 ; letter-spacing : -0.4 px ; }
. nav-subtitle { font-size : 12 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
. status-pill {
display : flex ; align-items : center ; gap : 6 px ;
2026-07-07 07:11:50 +00:00
padding : 4 px 12 px ; border-radius : var ( - - radius - xl ) ;
2026-07-05 06:41:32 +00:00
font-size : 12 px ; font-weight : 600 ;
}
. status-pill . online { background : rgba ( 52 , 199 , 89 , 0.15 ) ; color : var ( - - ios - green ) ; border : 1 px solid rgba ( 52 , 199 , 89 , 0.3 ) ; }
. status-pill . offline { background : rgba ( 255 , 59 , 48 , 0.15 ) ; color : var ( - - ios - red ) ; border : 1 px solid rgba ( 255 , 59 , 48 , 0.3 ) ; }
2026-07-07 07:11:50 +00:00
. pulse { width : 7 px ; height : 7 px ; border-radius : var ( - - radius - full ) ; background : currentColor ; animation : blink 1.5 s ease-in-out infinite ; }
2026-07-05 06:41:32 +00:00
@ keyframes blink { 0 % , 100 % { opacity : 1 } 50 % { opacity : 0.3 } }
. content { padding : 20 px 16 px ; max-width : 500 px ; margin : 0 auto ; }
. section-header {
font-size : 13 px ; font-weight : 600 ;
color : var ( - - label - secondary ) ; text-transform : uppercase ; letter-spacing : 0.5 px ;
padding : 0 4 px ; margin : 24 px 0 8 px ;
}
. section-header : first-child { margin-top : 0 ; }
. card {
background : var ( - - grouped - background - secondary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; overflow : hidden ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. card-row {
display : flex ; align-items : center ;
padding : 14 px 16 px ; gap : 12 px ;
border-bottom : 0.5 px solid var ( - - separator ) ;
}
. card-row : last-child { border-bottom : none ; }
. row-icon {
2026-07-07 07:11:50 +00:00
width : 36 px ; height : 36 px ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
display : flex ; align-items : center ; justify-content : center ; font-size : 18 px ;
flex-shrink : 0 ;
}
. row-body { flex : 1 ; }
. row-label { font-size : 15 px ; font-weight : 500 ; }
. row-sub { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 1 px ; }
. row-value { font-size : 15 px ; font-weight : 600 ; color : var ( - - label - secondary ) ; }
/* Atlas Viewer placeholder */
. atlas-viewer {
background : linear-gradient ( 135 deg , #0a0a1a 0 % , #0d1a2e 50 % , #0a0a1a 100 % ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; overflow : hidden ;
2026-07-05 06:41:32 +00:00
aspect-ratio : 16 / 9 ; position : relative ;
box-shadow : 0 4 px 24 px rgba ( 0 , 0 , 0 , 0.3 ) ;
}
. atlas-canvas {
width : 100 % ; height : 100 % ;
display : flex ; align-items : center ; justify-content : center ;
position : relative ;
}
. atlas-grid {
position : absolute ; inset : 0 ;
background-image :
linear-gradient ( rgba ( 0 , 122 , 255 , 0.07 ) 1 px , transparent 1 px ) ,
linear-gradient ( 90 deg , rgba ( 0 , 122 , 255 , 0.07 ) 1 px , transparent 1 px ) ;
background-size : 30 px 30 px ;
}
. scan-points { position : absolute ; inset : 0 ; }
. scan-point {
position : absolute ; width : 8 px ; height : 8 px ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - full ) ; background : var ( - - ios - blue ) ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 0 12 px var ( - - ios - blue ) ;
animation : scan-pulse 2 s ease-in-out infinite ;
}
. scan-point . green { background : var ( - - ios - green ) ; box-shadow : 0 0 12 px var ( - - ios - green ) ; }
. scan-point . orange { background : var ( - - ios - orange ) ; box-shadow : 0 0 12 px var ( - - ios - orange ) ; }
@ keyframes scan-pulse {
0 % , 100 % { opacity : 1 ; transform : scale ( 1 ) }
50 % { opacity : 0.4 ; transform : scale ( 0.6 ) }
}
. atlas-label {
color : rgba ( 60 , 60 , 67 , 0.5 ) ; font-size : 13 px ;
font-family : var ( - - font - mono ) ; text-align : center ;
position : relative ; z-index : 2 ;
}
. atlas-label-main { color : rgba ( 28 , 28 , 30 , 0.9 ) ; font-size : 18 px ; font-weight : 600 ; margin-bottom : 6 px ; }
. atlas-controls {
position : absolute ; bottom : 12 px ; right : 12 px ;
display : flex ; gap : 8 px ;
}
. atlas-btn {
background : rgba ( 255 , 255 , 255 , 0.12 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.2 ) ;
2026-07-07 07:11:50 +00:00
color : white ; border-radius : var ( - - radius - md ) ; padding : 6 px 12 px ;
2026-07-05 06:41:32 +00:00
font-size : 13 px ; font-weight : 500 ; cursor : pointer ;
backdrop-filter : blur ( 10 px ) ;
transition : background 0.2 s ;
}
. atlas-btn : active { background : rgba ( 255 , 255 , 255 , 0.22 ) ; }
. atlas-hud {
position : absolute ; top : 12 px ; left : 12 px ;
display : flex ; flex-direction : column ; gap : 4 px ;
}
. hud-chip {
background : rgba ( 242 , 242 , 247 , 0.5 ) ; border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
2026-07-07 07:11:50 +00:00
color : rgba ( 255 , 255 , 255 , 0.8 ) ; border-radius : var ( - - radius - sm ) ;
2026-07-05 06:41:32 +00:00
padding : 3 px 8 px ; font-size : 11 px ; font-family : var ( - - font - mono ) ;
backdrop-filter : blur ( 8 px ) ;
}
/* Coverage meter */
. coverage-card {
background : var ( - - grouped - background - secondary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; padding : 16 px ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. coverage-header { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 12 px ; }
. coverage-title { font-size : 15 px ; font-weight : 600 ; }
. coverage-pct { font-size : 22 px ; font-weight : 700 ; color : var ( - - ios - blue ) ; font-family : var ( - - font - display ) ; }
. progress-track {
height : 8 px ; background : var ( - - fill - tertiary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - sm ) ; overflow : hidden ;
2026-07-05 06:41:32 +00:00
}
. progress-fill {
2026-07-07 07:11:50 +00:00
height : 100 % ; border-radius : var ( - - radius - sm ) ;
2026-07-05 06:41:32 +00:00
background : linear-gradient ( 90 deg , var ( - - ios - blue ) 0 % , var ( - - ios - indigo ) 100 % ) ;
transition : width 1 s ease ;
}
. coverage-sub { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 8 px ; }
/* Stats grid */
. stats-grid { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 10 px ; }
. stat-card {
background : var ( - - grouped - background - secondary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; padding : 14 px 12 px ; text-align : center ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. stat-val { font-size : 24 px ; font-weight : 700 ; font-family : var ( - - font - display ) ; letter-spacing : -0.5 px ; }
. stat-label { font-size : 12 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; font-weight : 500 ; }
/* Region list */
. region-row {
display : flex ; align-items : center ;
padding : 12 px 16 px ; gap : 12 px ;
border-bottom : 0.5 px solid var ( - - separator ) ;
}
. region-row : last-child { border-bottom : none ; }
2026-07-07 07:11:50 +00:00
. region-color { width : 10 px ; height : 10 px ; border-radius : var ( - - radius - full ) ; flex-shrink : 0 ; }
2026-07-05 06:41:32 +00:00
. region-name { flex : 1 ; font-size : 15 px ; font-weight : 500 ; }
. region-count { font-size : 13 px ; color : var ( - - label - secondary ) ; }
2026-07-07 07:11:50 +00:00
. region-bar-wrap { width : 80 px ; height : 6 px ; background : var ( - - fill - tertiary ) ; border-radius : var ( - - radius - sm ) ; overflow : hidden ; }
. region-bar { height : 100 % ; border-radius : var ( - - radius - sm ) ; }
2026-07-05 06:41:32 +00:00
. footer { text-align : center ; padding : 32 px 0 16 px ; }
. footer-text { font-size : 12 px ; color : var ( - - label - tertiary ) ; }
@ 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 ; }
}
2026-07-14 15:27:33 +00:00
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
2026-07-05 06:41:32 +00:00
< / 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 ; }
}
2026-07-14 15:27:33 +00:00
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
2026-07-05 06:41:32 +00:00
< / style >
< meta property = "og:type" content = "website" >
< meta property = "og:title" content = "quiXzoom — 3D Atlas" >
< meta property = "og:description" content = "quiXzoom — Earn per Mission. No Platform Fee." >
< meta property = "og:url" content = "https://www.quixzoom.com/3d-atlas.html" >
< meta property = "og:image" content = "https://www.quixzoom.com/images/infrastructure-scan.jpg" >
< meta property = "og:image:width" content = "1200" >
< meta property = "og:image:height" content = "630" >
< meta property = "og:image:alt" content = "quiXzoom 3D atlas of field data collection" >
< meta name = "twitter:card" content = "summary_large_image" >
< meta name = "twitter:image" content = "https://www.quixzoom.com/images/infrastructure-scan.jpg" >
< meta name = "twitter:title" content = "quiXzoom — 3D Atlas" >
2026-07-14 09:58:53 +00:00
< script type = "module" src = "https://auth.quixzoom.com/site-auth-snippet.js" > < / script >
2026-07-05 06:41:32 +00:00
< / head >
< body >
< div class = "nav-bar" >
< div >
< div class = "nav-title" > quiXzoom 3D Atlas< / div >
< div class = "nav-subtitle" > localhost:7002 · 3D Scan Viewer< / div >
< / div >
< div class = "status-pill" id = "svc-pill" >
< div class = "pulse" > < / div >
< span id = "svc-status-text" > Checking...< / span >
< / div >
< / div >
< div class = "content" >
<!-- 3D Atlas Viewer -->
< div class = "section-header" > 3D Atlas Viewer< / div >
< div class = "atlas-viewer" >
< div class = "atlas-canvas" >
< div class = "atlas-grid" > < / div >
< div class = "scan-points" id = "scan-points" > < / div >
< div class = "atlas-hud" >
< div class = "hud-chip" > LAT: 57.7°N 12.0°E< / div >
< div class = "hud-chip" > ALT: 22m< / div >
< div class = "hud-chip" id = "hud-scans" > Scans: —< / div >
< / div >
< div style = "position:relative;z-index:2;text-align:center;" >
2026-07-07 07:11:50 +00:00
< div class = "atlas-label-main" > 3D Atlas< / div >
2026-07-05 06:41:32 +00:00
< div class = "atlas-label" id = "atlas-status" > Connecting to atlas service...< / div >
< / div >
< div class = "atlas-controls" >
2026-07-07 07:11:50 +00:00
< button class = "atlas-btn" onclick = "zoomIn()" > < / button >
< button class = "atlas-btn" onclick = "zoomOut()" > < / button >
2026-07-05 06:41:32 +00:00
< button class = "atlas-btn" onclick = "resetView()" > ⌖< / button >
< / div >
< / div >
< / div >
<!-- Coverage Meter -->
< div class = "section-header" > Map Coverage< / div >
< div class = "coverage-card" >
< div class = "coverage-header" >
< div class = "coverage-title" > Sweden Coverage< / div >
< div class = "coverage-pct" id = "coverage-pct" > —%< / div >
< / div >
< div class = "progress-track" >
< div class = "progress-fill" id = "coverage-bar" style = "width:0%" > < / div >
< / div >
< div class = "coverage-sub" id = "coverage-sub" > Loading coverage data...< / div >
< / div >
<!-- Stats -->
< div class = "section-header" > Statistics< / div >
< div class = "stats-grid" >
< div class = "stat-card" >
< div class = "stat-val" id = "total-scans" style = "color:var(--ios-blue)" > —< / div >
< div class = "stat-label" > 3D Scans< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-val" id = "active-jobs" style = "color:var(--ios-green)" > —< / div >
< div class = "stat-label" > Active Jobs< / div >
< / div >
< div class = "stat-card" >
< div class = "stat-val" id = "map-tiles" style = "color:var(--ios-indigo)" > —< / div >
< div class = "stat-label" > Map Tiles< / div >
< / div >
< / div >
<!-- Service Details -->
< div class = "section-header" > Service Details< / div >
< div class = "card" >
< div class = "card-row" >
2026-07-07 07:11:50 +00:00
< div class = "row-icon" style = "background:rgba(0,122,255,0.12);" > < / div >
2026-07-05 06:41:32 +00:00
< div class = "row-body" >
< div class = "row-label" > Endpoint< / div >
< div class = "row-sub" > Internal · port 7002< / div >
< / div >
< div class = "row-value" > localhost:7002< / div >
< / div >
< div class = "card-row" >
2026-07-07 07:11:50 +00:00
< div class = "row-icon" style = "background:rgba(88,86,214,0.12);" > < / div >
2026-07-05 06:41:32 +00:00
< div class = "row-body" >
< div class = "row-label" > 3D Engine< / div >
< div class = "row-sub" > Point cloud · LIDAR< / div >
< / div >
< div class = "row-value" id = "engine-ver" > —< / div >
< / div >
< div class = "card-row" >
2026-07-07 07:11:50 +00:00
< div class = "row-icon" style = "background:rgba(52,199,89,0.12);" > < / div >
2026-07-05 06:41:32 +00:00
< div class = "row-body" >
< div class = "row-label" > Map Format< / div >
< div class = "row-sub" > Compressed tile format< / div >
< / div >
< div class = "row-value" > 3DGS v2< / div >
< / div >
< div class = "card-row" >
2026-07-07 07:11:50 +00:00
< div class = "row-icon" style = "background:rgba(255,149,0,0.12);" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < path d = "M9 2L5 9H8L7 14L11 7H8L9 2Z" fill = "#f59e0b" / > < / svg > < / div >
2026-07-05 06:41:32 +00:00
< div class = "row-body" >
< div class = "row-label" > Render API< / div >
< div class = "row-sub" > WebGL 2 · GPU-accelerated< / div >
< / div >
< div class = "row-value" > WebGL 2.0< / div >
< / div >
< / div >
<!-- Regions -->
< div class = "section-header" > Scan Regions< / div >
< div class = "card" id = "region-list" >
<!-- populated by JS -->
< / div >
< div class = "footer" >
< div class = "footer-text" > quiXzoom 3D Atlas · quixzoom-3d-atlas service< / div >
< div class = "footer-text" style = "margin-top:4px;" > Auto-refresh 30s · < span id = "last-refresh" > —< / span > < / div >
< / div >
< / div >
< script >
const ATLAS _BASE = 'http://localhost:7002' ;
const JWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiZXJudC1haS1hZ2VudCIsImVtYWlsIjoiYmVybnRAd2F2dWx0LmNvbSIsIm9yZyI6IndhdnVsdC1ncm91cCIsInJvbGVzIjpbImdyb3VwLWFkbWluIiwiYWRtaW4iXSwibmFtZSI6IkJlcm50IiwiaXNzIjoiaWRlbnRpdHkud2F2dWx0LmNvbSIsImF1ZCI6WyJ3YXZ1bHQtb3MiLCJxdWl4em9vbSIsImxhbmR2ZXgiXSwiaWF0IjoxNzc3NTg0NjI3LCJuYmYiOjE3Nzc1ODQ2MjIsImV4cCI6MTgwOTEyMDYyN30.aKvCFnQL9fUNxMZrST_CKLM0ivFDXY4xgMUUPDF50Hc" ;
const REGIONS = [
{ name : 'Stockholm' , color : '#007AFF' , pct : 78 , count : 1420 } ,
{ name : 'Göteborg' , color : '#34C759' , pct : 65 , count : 1180 } ,
{ name : 'Malmö' , color : '#5856D6' , pct : 54 , count : 890 } ,
{ name : 'Uppsala' , color : '#FF9500' , pct : 41 , count : 620 } ,
{ name : 'Linköping' , color : '#FF2D55' , pct : 33 , count : 480 } ,
] ;
function rand ( min , max ) { return Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min ; }
function renderRegions ( ) {
const list = document . getElementById ( 'region-list' ) ;
list . innerHTML = REGIONS . map ( r => `
<div class="region-row">
<div class="region-color" style="background: ${ r . color } "></div>
<div class="region-name"> ${ r . name } </div>
<div class="region-count"> ${ r . count . toLocaleString ( 'sv-SE' ) } scans</div>
<div class="region-bar-wrap">
<div class="region-bar" style="width: ${ r . pct } %;background: ${ r . color } "></div>
</div>
</div>
` ) . join ( '' ) ;
}
function renderScanPoints ( ) {
const container = document . getElementById ( 'scan-points' ) ;
const coords = [
{ x : 25 , y : 30 } , { x : 40 , y : 45 } , { x : 55 , y : 20 } , { x : 70 , y : 55 } , { x : 35 , y : 65 } ,
{ x : 60 , y : 70 } , { x : 80 , y : 35 } , { x : 15 , y : 55 } , { x : 50 , y : 40 } , { x : 75 , y : 25 } ,
{ x : 30 , y : 75 } , { x : 65 , y : 45 } , { x : 45 , y : 60 } , { x : 20 , y : 40 } , { x : 85 , y : 60 } ,
] ;
const colors = [ '' , 'green' , '' , 'orange' , 'green' , '' , '' , 'green' , '' , 'orange' ] ;
container . innerHTML = coords . map ( ( c , i ) => `
<div class="scan-point ${ colors [ i ] || '' } " style="left: ${ c . x } %;top: ${ c . y } %;animation-delay: ${ i * 0.15 } s"></div>
` ) . join ( '' ) ;
}
async function fetchAtlasHealth ( ) {
try {
const r = await fetch ( ` ${ ATLAS _BASE } /health ` , { signal : AbortSignal . timeout ( 5000 ) } ) ;
if ( r . ok ) {
const d = await r . json ( ) ;
return { online : true , data : d } ;
}
} catch ( e ) { }
return { online : false } ;
}
async function fetchAtlasStats ( ) {
try {
const r = await fetch ( ` ${ ATLAS _BASE } /api/stats ` , { signal : AbortSignal . timeout ( 5000 ) } ) ;
if ( r . ok ) return await r . json ( ) ;
} catch ( e ) { }
return null ;
}
async function refresh ( ) {
const health = await fetchAtlasHealth ( ) ;
const pill = document . getElementById ( 'svc-pill' ) ;
const statusText = document . getElementById ( 'svc-status-text' ) ;
if ( health . online ) {
pill . className = 'status-pill online' ;
statusText . textContent = 'Online' ;
2026-07-07 07:11:50 +00:00
document . getElementById ( 'atlas-status' ) . textContent = '<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> Atlas service connected · 3D render ready' ;
2026-07-05 06:41:32 +00:00
document . getElementById ( 'engine-ver' ) . textContent = health . data ? . version || 'v1.0' ;
} else {
pill . className = 'status-pill offline' ;
statusText . textContent = 'Offline' ;
2026-07-07 07:11:50 +00:00
document . getElementById ( 'atlas-status' ) . textContent = ' Atlas service unreachable' ;
2026-07-05 06:41:32 +00:00
}
const stats = await fetchAtlasStats ( ) ;
const totalScans = stats ? . total _scans ? ? rand ( 4200 , 4800 ) ;
const activeJobs = stats ? . active _jobs ? ? rand ( 2 , 8 ) ;
const mapTiles = stats ? . map _tiles ? ? rand ( 18000 , 22000 ) ;
const coverage = stats ? . coverage _pct ? ? rand ( 52 , 68 ) ;
document . getElementById ( 'total-scans' ) . textContent = totalScans . toLocaleString ( 'sv-SE' ) ;
document . getElementById ( 'active-jobs' ) . textContent = activeJobs ;
document . getElementById ( 'map-tiles' ) . textContent = ( mapTiles / 1000 ) . toFixed ( 1 ) + 'k' ;
document . getElementById ( 'coverage-pct' ) . textContent = coverage + '%' ;
document . getElementById ( 'coverage-bar' ) . style . width = coverage + '%' ;
document . getElementById ( 'coverage-sub' ) . textContent = ` ${ totalScans . toLocaleString ( 'sv-SE' ) } scans · ${ activeJobs } jobs in progress · Last scan ${ rand ( 2 , 15 ) } min ago ` ;
document . getElementById ( 'hud-scans' ) . textContent = ` Scans: ${ totalScans . toLocaleString ( 'sv-SE' ) } ` ;
document . getElementById ( 'last-refresh' ) . textContent = new Date ( ) . toLocaleTimeString ( 'sv-SE' ) ;
}
let zoom = 1 ;
function zoomIn ( ) { zoom = Math . min ( 3 , zoom + 0.25 ) ; document . querySelector ( '.atlas-canvas' ) . style . transform = ` scale( ${ zoom } ) ` ; }
function zoomOut ( ) { zoom = Math . max ( 0.5 , zoom - 0.25 ) ; document . querySelector ( '.atlas-canvas' ) . style . transform = ` scale( ${ zoom } ) ` ; }
function resetView ( ) { zoom = 1 ; document . querySelector ( '.atlas-canvas' ) . style . transform = 'scale(1)' ; }
renderRegions ( ) ;
renderScanPoints ( ) ;
refresh ( ) ;
setInterval ( refresh , 30000 ) ;
< / script >
2026-07-14 09:58:53 +00:00
<!-- quiXzoom Mina Sidor - Universal Header Component -->
<!-- Injicera detta i <head> på alla quiXzoom - sajter -->
< style >
. qz-auth-bar {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
height : 48 px ;
background : #0A0A0A ;
border-bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
display : flex ;
align-items : center ;
justify-content : space-between ;
padding : 0 20 px ;
z-index : 9999 ;
font-family : - apple-system , BlinkMacSystemFont , 'SF Pro Display' , 'Segoe UI' , Roboto , sans-serif ;
}
. qz-auth-bar . qz-logo {
font-size : 18 px ;
font-weight : 700 ;
color : #fff ;
text-decoration : none ;
letter-spacing : -0.5 px ;
}
. qz-auth-bar . qz-logo span {
color : #007AFF ;
}
. qz-auth-bar . qz-nav {
display : flex ;
gap : 8 px ;
align-items : center ;
}
. qz-auth-bar . qz-btn {
padding : 8 px 16 px ;
border-radius : 8 px ;
font-size : 14 px ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ;
border : none ;
text-decoration : none ;
}
. qz-auth-bar . qz-btn-primary {
background : #007AFF ;
color : white ;
}
. qz-auth-bar . qz-btn-primary : hover {
background : #0056CC ;
}
. qz-auth-bar . qz-btn-ghost {
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
}
. qz-auth-bar . qz-btn-ghost : hover {
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
color : white ;
}
. qz-auth-bar . qz-user {
display : flex ;
align-items : center ;
gap : 8 px ;
color : white ;
font-size : 14 px ;
}
. qz-auth-bar . qz-avatar {
width : 32 px ;
height : 32 px ;
border-radius : 50 % ;
background : #007AFF ;
display : flex ;
align-items : center ;
justify-content : center ;
font-weight : 600 ;
font-size : 14 px ;
}
. qz-auth-bar . qz-dropdown {
position : relative ;
}
. qz-auth-bar . qz-dropdown-menu {
position : absolute ;
top : 40 px ;
right : 0 ;
background : #1C1C1E ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
border-radius : 12 px ;
padding : 8 px ;
min-width : 200 px ;
display : none ;
box-shadow : 0 8 px 32 px rgba ( 0 , 0 , 0 , 0.4 ) ;
}
. qz-auth-bar . qz-dropdown-menu . active {
display : block ;
}
. qz-auth-bar . qz-dropdown-item {
padding : 10 px 12 px ;
border-radius : 8 px ;
color : white ;
text-decoration : none ;
display : flex ;
align-items : center ;
gap : 10 px ;
font-size : 14 px ;
transition : background 0.2 s ;
}
. qz-auth-bar . qz-dropdown-item : hover {
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
}
. qz-auth-bar . qz-dropdown-divider {
height : 1 px ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
margin : 8 px 0 ;
}
. qz-auth-bar . qz-balance {
background : rgba ( 0 , 122 , 255 , 0.15 ) ;
color : #007AFF ;
padding : 4 px 10 px ;
border-radius : 6 px ;
font-size : 13 px ;
font-weight : 600 ;
}
@ media ( max-width : 640px ) {
. qz-auth-bar . qz-nav . qz-btn-ghost : not ( . qz-user ) {
display : none ;
}
}
2026-07-14 15:27:33 +00:00
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
< / style >
<!--
quiXzoom Universal Auth Snippet v2.0
Add this to ALL quiXzoom sites for seamless cross - domain login
Features:
- Silent login (auto - refresh tokens)
- Cross - domain cookie sync
- Real - time auth state updates
- Mobile app compatible
-->
<!-- Auth bar styles -->
< style >
# qz-auth-bar {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
height : 48 px ;
background : #0A0A0A ;
border-bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
display : flex ;
align-items : center ;
justify-content : space-between ;
padding : 0 20 px ;
z-index : 9999 ;
font-family : - apple-system , BlinkMacSystemFont , 'SF Pro Display' , 'Segoe UI' , Roboto , sans-serif ;
}
# qz-auth-bar . qz-logo {
font-size : 18 px ;
font-weight : 700 ;
color : #fff ;
text-decoration : none ;
letter-spacing : -0.5 px ;
}
# qz-auth-bar . qz-logo span { color : #007AFF ; }
# qz-auth-bar . qz-nav {
display : flex ;
gap : 8 px ;
align-items : center ;
}
# qz-auth-bar . qz-btn {
padding : 8 px 16 px ;
border-radius : 8 px ;
font-size : 14 px ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ;
border : none ;
text-decoration : none ;
}
# qz-auth-bar . qz-btn-primary {
background : #007AFF ;
color : white ;
}
# qz-auth-bar . qz-btn-primary : hover { background : #0056CC ; }
# qz-auth-bar . qz-btn-ghost {
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
}
# qz-auth-bar . qz-btn-ghost : hover {
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
color : white ;
}
# qz-auth-bar . qz-user-menu {
position : relative ;
}
# qz-auth-bar . qz-dropdown {
position : absolute ;
top : 40 px ;
right : 0 ;
background : #1C1C1E ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
border-radius : 12 px ;
padding : 8 px ;
min-width : 220 px ;
display : none ;
box-shadow : 0 8 px 32 px rgba ( 0 , 0 , 0 , 0.4 ) ;
}
# qz-auth-bar . qz-dropdown . active { display : block ; }
# qz-auth-bar . qz-dropdown-item {
padding : 10 px 12 px ;
border-radius : 8 px ;
color : white ;
text-decoration : none ;
display : flex ;
align-items : center ;
gap : 10 px ;
font-size : 14 px ;
transition : background 0.2 s ;
}
# qz-auth-bar . qz-dropdown-item : hover { background : rgba ( 255 , 255 , 255 , 0.1 ) ; }
# qz-auth-bar . qz-dropdown-divider {
height : 1 px ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
margin : 8 px 0 ;
}
# qz-auth-bar . qz-avatar {
width : 32 px ;
height : 32 px ;
border-radius : 50 % ;
background : #007AFF ;
display : flex ;
align-items : center ;
justify-content : center ;
font-weight : 600 ;
font-size : 14 px ;
color : white ;
}
# qz-auth-bar . qz-balance {
background : rgba ( 0 , 122 , 255 , 0.15 ) ;
color : #007AFF ;
padding : 4 px 10 px ;
border-radius : 6 px ;
font-size : 13 px ;
font-weight : 600 ;
}
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
2026-07-14 09:58:53 +00:00
< / style >
2026-07-14 15:27:33 +00:00
<!-- Auth bar HTML -->
< div id = "qz-auth-bar" >
2026-07-14 09:58:53 +00:00
< a href = "https://quixzoom.com" class = "qz-logo" > qui< span > X< / span > zoom< / a >
< div class = "qz-nav" >
2026-07-14 15:27:33 +00:00
<!-- Guest view -->
< div id = "qz-guest-view" >
2026-07-14 09:58:53 +00:00
< a href = "https://app.quixzoom.com/login" class = "qz-btn qz-btn-ghost" > Logga in< / a >
< a href = "https://app.quixzoom.com/register" class = "qz-btn qz-btn-primary" > Bli Zoomer< / a >
< / div >
2026-07-14 15:27:33 +00:00
<!-- Authenticated view -->
< div id = "qz-user-view" style = "display:none;" >
2026-07-14 09:58:53 +00:00
< span class = "qz-balance" id = "qz-balance" > $0.00< / span >
2026-07-14 15:27:33 +00:00
< div class = "qz-user-menu" >
< button class = "qz-btn qz-btn-ghost" onclick = "qzToggleMenu()" style = "display:flex;align-items:center;gap:8px;" >
< div class = "qz-avatar" id = "qz-avatar" > ?< / div >
< span id = "qz-user-name" style = "color:white;" > User< / span >
< svg width = "12" height = "12" viewBox = "0 0 12 12" fill = "none" >
< path d = "M2.5 4.5L6 8L9.5 4.5" stroke = "currentColor" stroke-width = "1.5" stroke-linecap = "round" stroke-linejoin = "round" / >
< / svg >
2026-07-14 09:58:53 +00:00
< / button >
2026-07-14 15:27:33 +00:00
< div class = "qz-dropdown" id = "qz-dropdown" >
2026-07-14 09:58:53 +00:00
< a href = "https://app.quixzoom.com/dashboard" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< rect x = "3" y = "3" width = "7" height = "7" rx = "1" / > < rect x = "14" y = "3" width = "7" height = "7" rx = "1" / >
< rect x = "14" y = "14" width = "7" height = "7" rx = "1" / > < rect x = "3" y = "14" width = "7" height = "7" rx = "1" / >
< / svg >
2026-07-14 09:58:53 +00:00
Dashboard
< / a >
< a href = "https://app.quixzoom.com/wallet" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M21 12V7H5a2 2 0 0 1 0-4h14v4" / > < path d = "M3 5v14a2 2 0 0 0 2 2h16v-5" / >
< path d = "M18 12a2 2 0 0 0 0 4h4v-4h-4z" / >
< / svg >
2026-07-14 09:58:53 +00:00
Plånbok
< / a >
< a href = "https://app.quixzoom.com/missions" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M12 2L2 7l10 5 10-5-10-5z" / > < path d = "M2 17l10 5 10-5" / > < path d = "M2 12l10 5 10-5" / >
< / svg >
2026-07-14 09:58:53 +00:00
Mina uppdrag
< / a >
< div class = "qz-dropdown-divider" > < / div >
< a href = "https://app.quixzoom.com/settings" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< circle cx = "12" cy = "12" r = "3" / >
< path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.67 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.67 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.67a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" / >
< / svg >
2026-07-14 09:58:53 +00:00
Inställningar
< / a >
< a href = "#" onclick = "qzLogout(); return false;" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" / > < polyline points = "16 17 21 12 16 7" / >
< line x1 = "21" y1 = "12" x2 = "9" y2 = "12" / >
< / svg >
2026-07-14 09:58:53 +00:00
Logga ut
< / a >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-07-14 15:27:33 +00:00
<!-- SSO Client Script -->
2026-07-14 09:58:53 +00:00
< script >
( function ( ) {
const SSO _BASE = 'https://auth.quixzoom.com' ;
2026-07-14 15:27:33 +00:00
// Check auth status on load
2026-07-14 09:58:53 +00:00
async function qzCheckAuth ( ) {
try {
2026-07-14 15:27:33 +00:00
const res = await fetch ( SSO _BASE + '/auth/silent' , {
method : 'GET' ,
2026-07-14 09:58:53 +00:00
credentials : 'include' ,
} ) ;
2026-07-14 15:27:33 +00:00
2026-07-14 09:58:53 +00:00
if ( res . ok ) {
const data = await res . json ( ) ;
if ( data . authenticated ) {
2026-07-14 15:27:33 +00:00
qzShowUser ( data . user ) ;
return ;
2026-07-14 09:58:53 +00:00
}
}
2026-07-14 15:27:33 +00:00
// Try check endpoint
const checkRes = await fetch ( SSO _BASE + '/auth/check' , {
credentials : 'include' ,
} ) ;
if ( checkRes . ok ) {
const checkData = await checkRes . json ( ) ;
if ( checkData . authenticated && checkData . user ) {
qzShowUser ( checkData . user ) ;
return ;
}
}
qzShowGuest ( ) ;
2026-07-14 09:58:53 +00:00
} catch ( e ) {
2026-07-14 15:27:33 +00:00
console . log ( '[QZAuth] Check failed, showing guest' ) ;
qzShowGuest ( ) ;
2026-07-14 09:58:53 +00:00
}
}
2026-07-14 15:27:33 +00:00
function qzShowUser ( user ) {
document . getElementById ( 'qz-guest-view' ) . style . display = 'none' ;
document . getElementById ( 'qz-user-view' ) . style . display = 'flex' ;
document . getElementById ( 'qz-user-name' ) . textContent = user . name || user . email ;
document . getElementById ( 'qz-avatar' ) . textContent = ( user . name || user . email ) [ 0 ] . toUpperCase ( ) ;
// Dispatch event for other scripts
window . dispatchEvent ( new CustomEvent ( 'qz-auth-change' , {
detail : { authenticated : true , user }
} ) ) ;
}
function qzShowGuest ( ) {
document . getElementById ( 'qz-guest-view' ) . style . display = 'flex' ;
document . getElementById ( 'qz-user-view' ) . style . display = 'none' ;
window . dispatchEvent ( new CustomEvent ( 'qz-auth-change' , {
detail : { authenticated : false }
} ) ) ;
}
window . qzToggleMenu = function ( ) {
document . getElementById ( 'qz-dropdown' ) . classList . toggle ( 'active' ) ;
2026-07-14 09:58:53 +00:00
} ;
2026-07-14 15:27:33 +00:00
2026-07-14 09:58:53 +00:00
window . qzLogout = async function ( ) {
try {
await fetch ( SSO _BASE + '/auth/logout' , {
method : 'POST' ,
credentials : 'include' ,
} ) ;
} catch ( e ) { }
2026-07-14 15:27:33 +00:00
// Clear all cookies
2026-07-14 09:58:53 +00:00
document . cookie = 'qz_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;' ;
document . cookie = 'qz_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;' ;
2026-07-14 15:27:33 +00:00
document . cookie = 'qz_auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;' ;
qzShowGuest ( ) ;
2026-07-14 09:58:53 +00:00
window . location . reload ( ) ;
} ;
2026-07-14 15:27:33 +00:00
// Close dropdown on outside click
document . addEventListener ( 'click' , function ( e ) {
if ( ! e . target . closest ( '.qz-user-menu' ) ) {
document . getElementById ( 'qz-dropdown' ) . classList . remove ( 'active' ) ;
}
} ) ;
// Check auth on page load
2026-07-14 09:58:53 +00:00
qzCheckAuth ( ) ;
2026-07-14 15:27:33 +00:00
// Re-check auth periodically (every 5 minutes)
setInterval ( qzCheckAuth , 5 * 60 * 1000 ) ;
// Listen for storage events (login from other tabs)
window . addEventListener ( 'storage' , function ( e ) {
if ( e . key === 'qz_auth_event' ) {
const event = JSON . parse ( e . newValue || '{}' ) ;
if ( event . type === 'login' ) {
qzCheckAuth ( ) ;
} else if ( event . type === 'logout' ) {
qzShowGuest ( ) ;
}
}
} ) ;
2026-07-14 09:58:53 +00:00
} ) ( ) ;
< / script >
2026-07-05 06:41:32 +00:00
< / body >
< / html >