HTML Structure¶
web_src/index.html (~9740 lines) is the single page served at / after gzipping. Everything the dashboard does happens inside this one document — there are no other HTML pages, no router, no view templates. Tabs and sub-tabs are CSS-driven show/hide on sibling divs.
Edit web_src/index.html directly; data/index.html.gz is auto-generated by compress_web.sh and must never be edited by hand.
File Layout¶
<head> metadata, viewport, links to /uPlot.iife.min.js, /uPlot.min.css, /script.js
<body>
<iframe name="hidden-form"> ← form submit target so /get URLs don't navigate the page
<div class="permanent-header"> always visible — wordmark, live readings, alternator-enable toggle, settings unlock
.header-brand-row wordmark + live field-status / charge-stage chips
.sensor-control-row battery V / SOC / Alt-A / Batt-A / Temp / RPM / IGN chips, plus alt-enable toggle
#protections-banner conditional yellow "PROTECTIONS DISABLED" banner (shown via JS toggle)
.settings-access-tier password unlock form
</div>
<div class="main-tabs"> six top-level tab buttons
<div id="livedata" class="tab-content"> … </div>
<div id="settings" class="tab-content"> … </div>
<div id="tuning" class="tab-content"> … </div>
<div id="plots" class="tab-content"> … </div>
<div id="console" class="tab-content"> … </div>
<div id="cloudfeatures" class="tab-content"> … </div>
</body>
The six tab divs are siblings; the .tab-content CSS class hides them by default and .tab-content.active shows them. showMainTab(name) (in script.js) toggles .active.
Six Top-Level Tabs¶
id |
What lives here |
|---|---|
livedata |
Live readings split into sub-tabs (Alternator, Battery, Engine, Boat & Voyage, Battery Monitor, Cloud, Diagnostics, ESP32 Stats, Status, Lifetime). Cells populated from CSV1/2 |
settings |
All user-tunable parameters as form rows. Sub-tabs: Vessel Info, Alternator (with sub-sub-tabs: Setup, Tables, Limits, Other, Protections), Engine, Battery, Weather, Accelerometer, Alarms, Battery Monitor, System |
tuning |
Plant Delay step test, Current loop tuning, Voltage loop tuning, Temperature loop tuning, About / How It Works |
plots |
uPlot charts: Volts, Amps, Duty, PID terms, IMU heel/pitch, IMU accel, RPM, Field-efficiency matrix |
console |
Live serial-equivalent log fed by the "console" SSE event |
cloudfeatures |
Account registration, vessel info, "Upload Now" button, leaderboard / fleet stats, firmware update controls |
Within each top-level tab there's a sub-tab navigation row. The pattern is fractal — top tabs hide/show .tab-content, sub-tabs hide/show .sub-tab-content, sub-sub-tabs (only inside Settings → Alternator and Tuning → Voltage) hide/show .alt-tab-content via .alt-tab-btn.
Two Recurring Form-Row Patterns¶
Display-only readout (live data)¶
<div class="reading-row">
<span class="reading-label">Battery Voltage:</span>
<span class="reading-value">
<span id="IBV">—</span>
<span class="reading-unit">V</span>
</span>
</div>
The inner <span id="IBV"> is what script.js writes into. The — placeholder shows during the initial connection / stale-data window; the staleness wrapper greys out the entire row when the corresponding IDX_* ages out (see JavaScript Logic → Staleness System).
Element ID conventions:
- Bare variable name (
<span id="IBV">) — the live value, one-to-one with the firmware variable. _echosuffix (<span id="BulkVoltage_echo">) — the read-back display next to a form input, populated by the CSV3 echo system._ID#numerical suffix (<span id="dutyCycleID3">) — secondary instance of the same value on a different layout location (the header chip + the live-data tab both show duty cycle). Numeric suffix differentiates so both spans get updated.
User-editable setting¶
<div class="form-row">
<div class="form-label" style="display: flex; align-items: center; justify-content: space-between;">
<span>Absorption Completion Time (sec) (<span id="absorptionCompleteTime_echo">?</span>):</span>
<span class="tooltip" onclick="this.classList.toggle('active')">ℹ️
<span class="tooltip-box">Tooltip text. Explains what this setting does, what units, what range is sensible, what happens at extremes.</span>
</span>
</div>
<div class="form-input">
<form action="/get" method="GET" target="hidden-form">
<input type="hidden" name="password" class="password_field">
<input name="absorptionCompleteTime" type="number" step="1" min="1" max="999999" />
<input onclick="submitMessage()" type="submit" value="Set" class="btn-primary" />
</form>
</div>
</div>
What each piece does:
- Label with embedded echo span —
<span id="<name>_echo">?</span>inside the label gets updated by the CSV3 handler. - Inline tooltip —
onclick="this.classList.toggle('active')"makes mobile-friendly tap-to-show tooltips (no hover on touchscreens). target="hidden-form"— the form submission goes to the iframe at the top of<body>, so the page itself doesn't navigate.<input type="hidden" name="password" class="password_field">— JS fills this fromgetStoredPassword()before submission. Every form has it.- Submit button calls
submitMessage()— clears the value field after a brief delay and shows a "Setting saved" toast.
Tooltip rules¶
onclick="this.classList.toggle('active')" is required on every .tooltip because hover doesn't work on touch. The tooltip body lives inside <span class="tooltip-box">. CSS positions and styles it.
Header / Status Bar¶
The .permanent-header is rendered above the tab navigation and stays visible across all tabs. It carries:
- Wordmark —
.header-wordmarkwith<span class="wm-X">X</span><span class="wm-rest">Engineering</span>. - Field-status cluster —
#field-statustext (ON/OFF/LIMP/WAITING_CLOUD/FAULT),#charge-stagechip (BULK/ABS/FLOAT/IDLE/MAINTAIN/TARGET-V/MANUALor hidden). The wrapping<div>gets.field-status-active/.field-status-inactive/.field-status-limp/.field-status-cloud-wait/.field-status-faultclasses depending on state — CSS uses these for color. - Sensor chips — Battery V / SOC, Alt A / Batt A, Temp, RPM / IGN. Each chip uses
.reading-duofor the label / number / unit triple. - Alternator Enable toggle — slider checkbox
#header-alternator-enable;onchangesubmits theOnOff=0/1form to/get. - Protections-disabled banner —
#protections-banner, hidden by default. JS shows it whentestProtectionsEnabled == 0. - Settings unlock — password input; locked state hides the gear icon / settings menu until correct password is entered.
The header is fixed-height; the tab content below scrolls independently.
Tab Navigation¶
<div class="main-tabs">
<button class="main-tab" onclick="showMainTab('livedata')">Live Data</button>
<button class="main-tab" onclick="showMainTab('settings')">Settings</button>
<button class="main-tab" onclick="showMainTab('tuning')">Tuning</button>
<button class="main-tab" onclick="showMainTab('plots')">Plots</button>
<button class="main-tab" onclick="showMainTab('console')">Console</button>
</div>
Buttons style themselves .active when their target .tab-content is shown. showMainTab(name) clears .active from all buttons + tab-contents, then sets it on the target pair. Sub-tab navigation uses showSubTab(parent, name) analogously.
URL hash preservation: location.hash is set to #<tab>/<sub> when navigating. On page reload the hash is read and tab state restored. Otherwise the default opens Live Data → Alternator.
Forms and /get¶
The form action is always action="/get" method="GET". The firmware-side handler walks request->hasParam(...) in order — see Network & Web System → Settings endpoint.
Multiple inputs in one form are submitted together; the handler runs through them in order. So when a tab has a "Save" button at the bottom that submits a 12-field form, all 12 fields land in one request and the dashboard sees a single CSV3 echo update covering all of them.
<input type="hidden" name="password" class="password_field"> — JS fills this on submission with the saved password. Required for every settings form. Locked-settings state disables all submit buttons.
Reserved special inputs¶
name="OnOff"— Alternator master enable (header toggle).name="ManualFieldToggle",name="ManualDutyTarget"— Manual-mode controls.name="LimpHome"— Emergency limp-home toggle.name="ResetPerfCounters"(momentary) — clearsworstSessionon everyFuncTimingstruct.name="ResetAlarmLatch"(momentary) — clears the buzzer-latch state.name="testProtectionsEnabled"— Tuning-mode safety override (drives the#protections-banner).name="ResetDynamicShuntGain",name="ResetDynamicAltZero"(momentary) — auto-cal reset buttons.name="forceCloudFlushPending"(momentary) — "Upload Now" button.
Momentary fields are set to 1 on the submit, then the firmware clears them to 0 after acting. No echo expected.
Special UI Components¶
Field-bucketer matrix (Plots → Field Efficiency)¶
3-D matrix rendered as a 2-D table with a third dimension as a dropdown selector. Built dynamically from the EffMatrix SSE event (see Field Bucketer). Each cell color-codes by state (0 = empty grey, 1 = populated yellow, 2 = reference green). A red dot overlays the cell the system is currently in.
Tuning tabs¶
Each tuning tab has a similar structure: parameter inputs at top, a "Start Test" button, live score readout, a score-log table that fetches /tuninglog / /cvtuninglog / /thermaltuninglog JSON and renders into a table.
Console pane¶
Simple <div id="console-output"> that JS appends to. Bounded buffer — older lines drop off after N messages. "Clear" button empties it. Console timestamps shown are browser receive time, not firmware queue time.
Plots¶
Each plot is a <div id="plot-X"> that uPlot mounts into. Series visibility is controlled by checkboxes above the plot. Time-axis mode (relative vs absolute) toggled via toggleTimeAxisMode(). Theme inherits from the global dark/light toggle.
Mobile / Capacitor Wrapper¶
The page is responsive — CSS media queries collapse multi-column form rows to single-column at narrow widths. Touch-target sizes are sized for thumbs (44 px minimum where possible).
When wrapped in the Capacitor iOS app, the page is loaded from a remote URL (http://alternator.local) rather than bundled into the app. See JavaScript Logic → Reconnect Logic for the IS_CAPACITOR URL switch. The Capacitor iOS project lives at /Users/joeceo/Documents/alternator regulator x engineering/CapacitorStuff/ (developer machine), separate from the firmware repo.
iOS-specific meta tags in the <head> enable home-screen-app behavior:
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="X Regulator">
Editing Conventions¶
<hr>between each parameter inside a tab — visual separation. Don't forget it when adding a new form row.- Tooltips for every setting, brief, plain-English. Use the UI label name, not the firmware identifier (
absorption time, notabsorptionCompleteTime). - Echo span inside the label with leading
?placeholder. - Hidden password field on every form that submits to
/get. - Submit button calls
submitMessage()via onclick. - Form posts to the hidden iframe via
target="hidden-form".
Cross-references¶
- SSE event listeners, data flow into DOM → JavaScript Logic
- Adding a new setting (firmware side) → Important Functions → Pattern B
- CSS theming, dark/light mode, layout → CSS Styling
- HTTP endpoint table → Network & Web System