2026-10-28 10:54:15 -05:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >< meta name = "viewport" content = "width=device-width,initial-scale=1" >
< title > Macker · Work car</ title >
< link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" >
< style >
* { box-sizing : border-box } body { margin : 0 ; background : #f3f5f2 ; color : #182d26 ; font : 16 px system-ui , sans-serif } header { padding : 24 px 5 vw ; display : flex ; justify-content : space-between ; align-items : center } h1 { font-size : 24 px ; margin : 0 } header p { margin : 4 px 0 ; color : #607269 } main { margin : 0 5 vw 28 px }. stats { display : flex ; gap : 16 px ; margin-bottom : 20 px }. card { background : white ; border : 1 px solid #dce4dd ; border-radius : 12 px ; padding : 20 px ; flex : 1 }. label { font-size : 12 px ; text-transform : uppercase ; letter-spacing : .1 em ; color : #607269 }. value { display : block ; font-size : 32 px ; margin : 8 px 0 } small { color : #607269 }# map { height : 65 vh ; min-height : 320 px ; border-radius : 14 px ; border : 1 px solid #cbd8cd } button { background : #1c593f ; color : white ; border : 0 ; border-radius : 8 px ; padding : 12 px ; cursor : pointer }# status { margin : 12 px 0 ; min-height : 24 px }@ media ( max-width : 540px ) {. stats { flex-direction : column ; gap : 10 px }. card { padding : 14 px }. value { font-size : 26 px } header { gap : 12 px }# map { height : 50 vh }}
</ style >
</ head >
< body >
< header >< div >< h1 > Macker</ h1 >< p > Work car · Live location</ p ></ div >< button id = "follow" type = "button" > Follow car: on</ button ></ header >
2026-10-28 13:43:27 -05:00
< main >< div class = "stats" >< section class = "card" >< span class = "label" > Current speed</ span >< strong class = "value" id = "speed" > —</ strong >< small id = "metric" > Waiting for GPS</ small ></ section >< section class = "card" >< span class = "label" > Last update</ span >< strong class = "value" id = "age" > —</ strong >< small id = "time" > No location received yet</ small ></ section ></ div >< p id = "status" role = "status" > Waiting for the car’ s first GPS fix…</ p >< p id = "uncertainty" >< small > GPS uncertainty unavailable</ small ></ p >< div id = "map" aria-label = "Car location map with GPS uncertainty circle" ></ div ></ main >
2026-10-28 10:54:15 -05:00
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" ></ script >
< script >
const el = id => document . getElementById ( id );
2026-10-28 13:43:27 -05:00
let map , marker , uncertaintyCircle , latest , follow = true , failed = false ;
2026-10-28 10:54:15 -05:00
if ( window . L ){ map = L . map ( 'map' ). setView ([ 20 , 0 ], 2 ); L . tileLayer ( 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' ,{ maxZoom : 19 , attribution : '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }). addTo ( map );}
el ( 'follow' ). onclick = ()=>{ follow =! follow ; el ( 'follow' ). textContent = `Follow car: ${ follow ? 'on' : 'off' } ` ; if ( follow && marker ) map . panTo ( marker . getLatLng ());};
2026-10-28 13:43:27 -05:00
// gpsd axis errors are already in metres. Enclose their horizontal error box.
// Zero also represents an omitted estimate in the client's gpsd dependency.
function uncertaintyRadius ( point ){
const { LatErr , LongErr } = point ;
if ( ! Number . isFinite ( LatErr ) ||! Number . isFinite ( LongErr ) || LatErr <= 0 || LongErr <= 0 ) return null ;
const radius = Math . hypot ( LatErr , LongErr );
return Number . isFinite ( radius ) ? radius : null ;
}
function updateUncertainty ( point , pos ){
const radius = uncertaintyRadius ( point );
el ( 'uncertainty' ). textContent = radius === null ? 'GPS uncertainty unavailable' : `Estimated GPS uncertainty: ${ radius . toFixed ( 1 ) } m radius · shaded circle` ;
if ( radius === null ){
if ( uncertaintyCircle ){ map . removeLayer ( uncertaintyCircle ); uncertaintyCircle = null ;}
return ;
}
if ( ! uncertaintyCircle ){
uncertaintyCircle = L . circle ( pos ,{ radius , color : '#287a57' , weight : 2 , fillColor : '#409b73' , fillOpacity : 0.16 , interactive : false }). addTo ( map );
} else { uncertaintyCircle . setLatLng ( pos ). setRadius ( radius );}
}
2026-10-28 10:54:15 -05:00
function render (){
if ( ! latest ){ el ( 'status' ). textContent = failed ? 'Cannot reach server. Retrying…' : 'Waiting for the car’ s first GPS fix…' ; return ;}
const seconds = Math . max ( 0 , Math . floor (( Date . now () - Date . parse ( latest . updated_at )) / 1000 ));
const stale = seconds > 30 , p = latest . point ;
el ( 'age' ). textContent = seconds < 60 ? ` ${ seconds } s ago` : seconds < 3600 ? ` ${ Math . floor ( seconds / 60 ) } min ago` : ` ${ Math . floor ( seconds / 3600 ) } h ago` ;
el ( 'time' ). textContent = new Date ( latest . updated_at ). toLocaleString ();
el ( 'speed' ). textContent = stale || p . Speed == null ? '—' : ` ${ ( p . Speed * 2.236936 ). toFixed ( 1 ) } mph` ;
el ( 'metric' ). textContent = stale ? 'Last fix is stale' : p . Speed == null ? 'Speed unavailable' : ` ${ ( p . Speed * 3.6 ). toFixed ( 1 ) } km/h` ;
el ( 'status' ). textContent = failed ? 'Connection lost · showing last known location' : stale ? 'No recent GPS fix · showing last known location' : `GPS active · ${ p . Lat . toFixed ( 5 ) } , ${ p . Long . toFixed ( 5 ) } ` ;
if ( ! map ) el ( 'status' ). textContent += ' · Map failed to load; reload to retry' ;
}
2026-10-28 13:43:27 -05:00
async function poll (){ try { const res = await fetch ( '/api/location' ,{ cache : 'no-store' , signal : AbortSignal . timeout ( 8000 )}); if ( ! res . ok ) throw Error ( res . status ); latest = await res . json (); failed = false ; if ( latest && map ){ const pos = [ latest . point . Lat , latest . point . Long ]; updateUncertainty ( latest . point , pos ); if ( ! marker ){ marker = L . marker ( pos ). addTo ( map ); map . setView ( pos , 15 );} else { marker . setLatLng ( pos ); if ( follow ) map . panTo ( pos );}}} catch ( e ){ failed = true ;} render (); setTimeout ( poll , 2000 );}
2026-10-28 10:54:15 -05:00
setInterval ( render , 1000 ); poll ();
</ script >
</ body ></ html >