Skip to content

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.
  • _echo suffix (<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 tooltiponclick="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 from getStoredPassword() 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-wordmark with <span class="wm-X">X</span><span class="wm-rest">Engineering</span>.
  • Field-status cluster#field-status text (ON / OFF / LIMP / WAITING_CLOUD / FAULT), #charge-stage chip (BULK / ABS / FLOAT / IDLE / MAINTAIN / TARGET-V / MANUAL or hidden). The wrapping <div> gets .field-status-active / .field-status-inactive / .field-status-limp / .field-status-cloud-wait / .field-status-fault classes depending on state — CSS uses these for color.
  • Sensor chips — Battery V / SOC, Alt A / Batt A, Temp, RPM / IGN. Each chip uses .reading-duo for the label / number / unit triple.
  • Alternator Enable toggle — slider checkbox #header-alternator-enable; onchange submits the OnOff=0/1 form to /get.
  • Protections-disabled banner#protections-banner, hidden by default. JS shows it when testProtectionsEnabled == 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) — clears worstSession on every FuncTiming struct.
  • 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, not absorptionCompleteTime).
  • 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