2026-03-21 10:34:04 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" >
< title > OpusBike — ProActys< / title >
<!-- PWA -->
< link rel = "manifest" href = "manifest.json" >
< meta name = "theme-color" content = "#F5F7FA" >
<!-- Apple PWA meta tags -->
< meta name = "apple-mobile-web-app-capable" content = "yes" >
< meta name = "apple-mobile-web-app-status-bar-style" content = "default" >
< meta name = "apple-mobile-web-app-title" content = "OpusBike" >
< link rel = "apple-touch-icon" href = "icons/apple-touch-icon.png" >
<!-- Favicon -->
< link rel = "icon" type = "image/svg+xml" href = "icons/favicon.svg" >
<!-- Styles -->
< link rel = "stylesheet" href = "css/style.css" >
< / head >
< body >
<!-- Connect View -->
< div id = "connect-view" class = "view active" >
< div class = "connect-container" >
< div class = "logo-header" >
< img src = "icons/icon-192.png" alt = "OpusBike" class = "app-logo" >
< h1 > OpusBike< / h1 >
< p class = "subtitle" > Bosch Smart System Monitor< / p >
< / div >
< div id = "connection-status" class = "status-badge disconnected" >
< span class = "status-dot" > < / span >
< span id = "status-text" > Disconnected< / span >
< / div >
< div id = "mode-label" class = "mode-role-label" > < / div >
< div class = "connect-actions" >
< button id = "btn-connect" class = "btn btn-primary" onclick = "app.connect()" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M6.5 6.5l11 11M17.5 6.5l-11 11M12 2v4M12 18v4M2 12h4M18 12h4" / >
< / svg >
Connect via Bluetooth
< / button >
< button id = "btn-demo" class = "btn btn-secondary" onclick = "app.startDemo()" >
Demo Mode
< / button >
< / div >
< div id = "ble-unsupported" class = "info-card hidden" >
< p > Web Bluetooth is not available in this browser.< / p >
< div id = "ios-bluefy-hint" class = "bluefy-hint hidden" >
< p > < strong > iPhone / iPad?< / strong > < / p >
< p > Install < a href = "https://apps.apple.com/app/bluefy-web-ble-browser/id1492822055" target = "_blank" rel = "noopener" > Bluefy< / a > (free) for direct Bluetooth access.< / p >
< a href = "https://apps.apple.com/app/bluefy-web-ble-browser/id1492822055" target = "_blank" rel = "noopener" class = "btn-bluefy" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "currentColor" > < path d = "M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z" / > < / svg >
Get Bluefy (free)
< / a >
< / div >
< p class = "relay-fallback-text" > Or view live data via WebSocket relay:< / p >
< div class = "ws-status" >
< span class = "status-dot" > < / span >
< span id = "ws-status-text" > Connecting to relay...< / span >
< / div >
< / div >
<!-- BLE Troubleshooting -->
< details id = "ble-troubleshoot" class = "troubleshoot-section" >
< summary > Don't see your bike?< / summary >
< div class = "troubleshoot-content" >
< p > < strong > Web Bluetooth uses its own pairing flow.< / strong > If your bike is paired in iOS Settings, the Web Bluetooth picker won't find it.< / p >
< ol >
< li > Go to < strong > Settings → Bluetooth< / strong > < / li >
< li > Find < strong > "smart system eBike"< / strong > (or similar Bosch name)< / li >
< li > Tap the < strong > (i)< / strong > icon → < strong > Forget This Device< / strong > < / li >
< li > Come back here and tap < strong > Connect via Bluetooth< / strong > < / li >
< li > The Web Bluetooth picker will appear — select your bike< / li >
< / ol >
< p class = "troubleshoot-note" > Make sure the Bosch system is powered on (turn the crank or press the button on the display).< / p >
< / div >
< / details >
< div class = "qr-section" >
< p class = "qr-label" > Share with your team< / p >
< img src = "icons/qr-code.png" alt = "QR Code" class = "qr-image" >
< canvas id = "qr-canvas" width = "200" height = "200" > < / canvas >
< p class = "qr-url" > monitor.proactys.swiss/opusbike< / p >
< / div >
< p class = "brand-footer" >
< img src = "icons/proactys-logo.png" alt = "ProActys" / >
ProActys GmbH
< / p >
< / div >
< / div >
<!-- Ride View (Dashboard) -->
< div id = "ride-view" class = "view" >
< div class = "dashboard" >
< div class = "dashboard-header" >
< div class = "header-left" >
< h2 id = "ride-title" > OpusBike< / h2 >
< div id = "dash-connection-status" class = "status-badge connected" >
< span class = "status-dot" > < / span >
< span id = "dash-status-text" > Connected< / span >
< / div >
< span id = "demo-badge" class = "demo-badge" > DEMO< / span >
< / div >
< img src = "icons/proactys-logo.png" alt = "ProActys" class = "proactys-logo-sm" >
< button id = "btn-disconnect" class = "btn btn-small btn-danger" onclick = "app.disconnect()" >
Disconnect
< / button >
< / div >
<!-- Main metrics grid -->
< div class = "metrics-grid" >
<!-- Speed = hero metric -->
< div class = "metric-card metric-speed" >
< div class = "metric-label" > Speed< / div >
< div class = "metric-value" >
< span id = "val-speed" > 0.0< / span >
< span class = "metric-unit" > km/h< / span >
< / div >
< / div >
2026-03-21 15:38:28 +00:00
< div class = "metric-card metric-cadence" >
< div class = "metric-label" > Cadence< / div >
2026-03-21 10:34:04 +00:00
< div class = "metric-value" >
2026-03-21 15:38:28 +00:00
< span id = "val-cadence" > 0< / span >
< span class = "metric-unit" > rpm< / span >
2026-03-21 10:34:04 +00:00
< / div >
< / div >
2026-03-21 15:38:28 +00:00
< div class = "metric-card metric-humanpower" >
< div class = "metric-label" > Human Power< / div >
2026-03-21 10:34:04 +00:00
< div class = "metric-value" >
2026-03-21 15:38:28 +00:00
< span id = "val-humanpower" > 0< / span >
< span class = "metric-unit" > W< / span >
< / div >
< / div >
< div class = "metric-card metric-motorpower" >
< div class = "metric-label" > Motor Power< / div >
< div class = "metric-value" >
< span id = "val-motorpower" > 0< / span >
< span class = "metric-unit" > W< / span >
2026-03-21 10:34:04 +00:00
< / div >
< / div >
2026-03-21 15:38:28 +00:00
< div class = "metric-card metric-battery" >
< div class = "metric-label" > Battery< / div >
2026-03-21 10:34:04 +00:00
< div class = "metric-value" >
2026-03-21 15:38:28 +00:00
< span id = "val-battery" > —< / span >
< span class = "metric-unit" > %< / span >
< / div >
< div class = "battery-bar" > < div class = "battery-fill" id = "battery-bar" style = "width:0%" > < / div > < / div >
< / div >
< div class = "metric-card metric-time" >
< div class = "metric-label" > Ride Time< / div >
< div class = "metric-value" >
< span id = "val-time" > 00:00:00< / span >
2026-03-21 10:34:04 +00:00
< / div >
< / div >
< div class = "metric-card metric-mode" >
< div class = "metric-label" > Mode< / div >
< div class = "metric-value" >
< span id = "val-mode" > OFF< / span >
< / div >
< div class = "mode-indicator" id = "mode-indicator" >
< span class = "mode-dot" data-mode = "OFF" > OFF< / span >
< span class = "mode-dot" data-mode = "ECO" > ECO< / span >
< span class = "mode-dot" data-mode = "TOUR" > TOUR< / span >
< span class = "mode-dot" data-mode = "EMTB" > eMTB< / span >
< span class = "mode-dot" data-mode = "TURBO" > TURBO< / span >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Tab bar -->
< nav id = "tab-bar" class = "tab-bar hidden" >
< button class = "tab active" data-view = "ride-view" onclick = "app.switchTab('ride-view')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "currentColor" > < path d = "M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" / > < / svg >
< span > Dashboard< / span >
< / button >
< button class = "tab" data-view = "connect-view" onclick = "app.switchTab('connect-view')" >
< svg width = "20" height = "20" viewBox = "0 0 24 24" fill = "currentColor" > < path d = "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" / > < / svg >
< span > Connect< / span >
< / button >
< / nav >
<!-- Scripts -->
< script src = "js/boschProtocol.js" > < / script >
< script src = "js/bleService.js" > < / script >
< script src = "js/wsRelay.js" > < / script >
< script src = "js/app.js" > < / script >
<!-- Service Worker registration -->
< script >
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('js/sw.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.warn('SW registration failed:', err));
}
< / script >
< / body >
< / html >