ui: rework Pi settings page — grouped sections, icons, clearer labels

- Four logical sections: Car & identity, Connectivity, Server & uploads, Alerts
- FontAwesome icons on every card header for visual scanning
- Reordered cards (identity → in-car AP → phone hotspot → server → uploads → alerts)
- settings-section + settings-card styles added via block styles
- Improved helper text (device name, odometer, hotspot password)
- All element IDs preserved — JS behavior unchanged
This commit is contained in:
Mathieu Moran
2026-08-12 16:43:04 -04:00
parent 2cfbe67a86
commit cfd9d03e81
+123 -95
View File
@@ -2,6 +2,24 @@
{% block title %}Settings - Yaris OBD{% endblock %} {% block title %}Settings - Yaris OBD{% endblock %}
{% block styles %}
<style>
/* Section headers group related cards */
.settings-section {
font-size: .7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: #6c757d;
margin: 1.25rem 0 .5rem;
}
.settings-section:first-of-type { margin-top: 0; }
.settings-card .card-header { display: flex; align-items: center; gap: .5rem; }
.settings-card .card-header i { color: #4e73df; width: 1.1rem; text-align: center; }
.settings-card .card-header .badge { margin-left: auto; }
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3"> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div> <div>
@@ -28,17 +46,19 @@
<form id="settings-form"> <form id="settings-form">
{# ── Device identity ────────────────────────────────────────────── #} {# ═══ Car & identity ═══════════════════════════════════════════════ #}
<div class="card shadow-sm mb-3"> <div class="settings-section">Car &amp; identity</div>
<div class="card shadow-sm mb-3 settings-card">
<div class="card-header py-2"> <div class="card-header py-2">
<p class="text-primary m-0 fw-bold small">Device identity</p> <i class="fas fa-car"></i>
<p class="text-dark m-0 fw-bold small">Device identity</p>
</div> </div>
<div class="card-body py-3"> <div class="card-body py-3">
<div class="form-check form-switch mb-3"> <div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch" id="device-sync-enabled"> <input class="form-check-input" type="checkbox" role="switch" id="device-sync-enabled">
<label class="form-check-label" for="device-sync-enabled"> <label class="form-check-label" for="device-sync-enabled">
Sync with server Sync with server
<span class="text-muted small d-block">If enabled, server is source of truth and these fields become read-only.</span> <span class="text-muted small d-block">If enabled, the server is the source of truth and these fields become read-only.</span>
</label> </label>
</div> </div>
<div id="device-config-fields"> <div id="device-config-fields">
@@ -63,107 +83,42 @@
<label class="form-label form-label-sm" for="device-name">Device name</label> <label class="form-label form-label-sm" for="device-name">Device name</label>
<input class="form-control form-control-sm" id="device-name" maxlength="100"> <input class="form-control form-control-sm" id="device-name" maxlength="100">
<div class="invalid-feedback" data-error-for="device_name"></div> <div class="invalid-feedback" data-error-for="device_name"></div>
<div class="form-text">Reserved for future server portal display.</div> <div class="form-text">Shown in the server portal.</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label form-label-sm" for="odometer-baseline">Odometer baseline (km)</label> <label class="form-label form-label-sm" for="odometer-baseline">Odometer baseline (km)</label>
<input class="form-control form-control-sm" id="odometer-baseline" type="number" inputmode="decimal" min="0" max="999999" step="0.1"> <input class="form-control form-control-sm" id="odometer-baseline" type="number" inputmode="decimal" min="0" max="999999" step="0.1">
<div class="invalid-feedback" data-error-for="odometer_baseline"></div> <div class="invalid-feedback" data-error-for="odometer_baseline"></div>
<div class="form-text">Enter your car's current odometer reading. The app adds trip distances to this baseline. Leave at 0 to use live OBD reading (not available on 2013 Yaris).</div> <div class="form-text">Your car's current odometer reading. Trip distances are added to this. Leave at 0 to use live OBD (unsupported on 2013 Yaris).</div>
</div>
</div>
<p class="text-muted small mb-0">Edit your device identity here. If server sync is enabled, these fields are read-only.</p>
</div>
</div>
</div>
{# ── Remote server credentials ────────────────────────────────────── #}
<div class="card shadow-sm mb-3">
<div class="card-header py-2">
<p class="text-primary m-0 fw-bold small">Remote server credentials</p>
</div>
<div class="card-body py-3">
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label form-label-sm" for="remote-server-url">Server URL</label>
<input class="form-control form-control-sm" id="remote-server-url" type="url" placeholder="https://your-server.example">
<div class="invalid-feedback" data-error-for="server_url"></div>
</div>
<div class="col-md-4">
<label class="form-label form-label-sm" for="remote-device-name">Device name</label>
<input class="form-control form-control-sm" id="remote-device-name" maxlength="100" placeholder="yaris-2013">
<div class="invalid-feedback" data-error-for="remote_device_name"></div>
</div>
<div class="col-md-4">
<label class="form-label form-label-sm" for="remote-api-key">API key</label>
<input class="form-control form-control-sm" id="remote-api-key" type="password" maxlength="512" autocomplete="new-password" placeholder="Paste after create/rotate">
<div class="invalid-feedback" data-error-for="api_key"></div>
<div class="form-text" id="remote-api-key-state">Leave blank to keep saved key.</div>
<div class="form-check mt-1">
<input class="form-check-input" type="checkbox" id="clear-remote-api-key">
<label class="form-check-label small" for="clear-remote-api-key">Remove saved API key</label>
</div> </div>
</div> </div>
</div> </div>
<p class="form-text small mb-0">
These values are stored only on this Pi in <code>db/upload_creds.env</code> and are reused by both uploads and config sync.
</p>
</div> </div>
</div> </div>
{# ── Upload & retention ──────────────────────────────────────────── #} {# ═══ Connectivity ══════════════════════════════════════════════════ #}
<div class="card shadow-sm mb-3"> <div class="settings-section">Connectivity</div>
<div class="card shadow-sm mb-3 settings-card" id="in-car-access-card">
<div class="card-header py-2"> <div class="card-header py-2">
<p class="text-primary m-0 fw-bold small">Uploads &amp; local video</p> <i class="fas fa-wifi"></i>
</div> <p class="text-dark m-0 fw-bold small">In-car hotspot</p>
<div class="card-body py-3">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="trips-upload-enabled">
<label class="form-check-label" for="trips-upload-enabled">Upload trips &amp; GPS logs to server</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="dashcam-upload-enabled">
<label class="form-check-label" for="dashcam-upload-enabled">Upload dashcam videos to server (WiFi only)</label>
</div>
<p id="upload-health" class="small text-muted mb-3">Checking…</p>
<div class="d-flex align-items-center gap-2">
<label class="form-label mb-0 text-nowrap" for="retention-days">Delete after</label>
<div class="input-group input-group-sm" style="max-width:10rem;">
<input class="form-control" id="retention-days" type="number" min="0" max="3650" step="1" required>
<span class="input-group-text">days</span>
</div>
<span class="small text-muted">(0 = keep forever)</span>
</div>
<div class="invalid-feedback" data-error-for="local_video_retention_days"></div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" role="switch" id="video-convert-enabled">
<label class="form-check-label" for="video-convert-enabled">
Convert imported videos for web playback
<span class="text-muted small d-block">Re-encodes MOV→MP4 using hardware encoder in background after import. Original deleted when done.</span>
</label>
</div>
</div>
</div>
{# ── In-car access ────────────────────────────────────────────────── #}
<div class="card shadow-sm mb-3" id="in-car-access-card">
<div class="card-header py-2">
<p class="text-primary m-0 fw-bold small">In-car access</p>
</div> </div>
<div class="card-body py-3"> <div class="card-body py-3">
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-sm-6"> <div class="col-sm-6">
<label class="form-label form-label-sm" for="ap-ssid">Pi hotspot name (SSID)</label> <label class="form-label form-label-sm" for="ap-ssid">Network name (SSID)</label>
<input class="form-control form-control-sm" id="ap-ssid" maxlength="32" autocomplete="off"> <input class="form-control form-control-sm" id="ap-ssid" maxlength="32" autocomplete="off">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<label class="form-label form-label-sm" for="ap-password">Pi hotspot password</label> <label class="form-label form-label-sm" for="ap-password">Password</label>
<input class="form-control form-control-sm" id="ap-password" type="password" <input class="form-control form-control-sm" id="ap-password" type="password"
minlength="8" maxlength="63" autocomplete="new-password" placeholder="Leave blank to keep current"> minlength="8" maxlength="63" autocomplete="new-password" placeholder="Leave blank to keep current">
<div class="form-text">Write-only — never sent back to the browser.</div> <div class="form-text">Write-only — never shown again.</div>
</div> </div>
</div> </div>
<button class="btn btn-sm btn-outline-primary mb-3" id="ap-save" type="button">Save hotspot</button> <button class="btn btn-sm btn-outline-primary mb-3" id="ap-save" type="button">
<i class="fas fa-save me-1"></i>Save hotspot
</button>
<div id="ap-message" class="alert py-2 mb-3" role="status" hidden></div> <div id="ap-message" class="alert py-2 mb-3" role="status" hidden></div>
<div id="in-car-hotspot-info" hidden> <div id="in-car-hotspot-info" hidden>
<p class="mb-2 fw-semibold">Your phone can reach the Pi at:</p> <p class="mb-2 fw-semibold">Your phone can reach the Pi at:</p>
@@ -176,7 +131,6 @@
<div class="alert alert-warning py-2 mb-0 small"> <div class="alert alert-warning py-2 mb-0 small">
<i class="fas fa-exclamation-triangle me-1"></i> <i class="fas fa-exclamation-triangle me-1"></i>
If the page doesn't load, try <strong>disabling cellular/mobile data</strong> on your phone. If the page doesn't load, try <strong>disabling cellular/mobile data</strong> on your phone.
Phones often keep using cell data when WiFi has no internet, blocking access to the Pi.
</div> </div>
</div> </div>
<div id="in-car-wifi-info" hidden> <div id="in-car-wifi-info" hidden>
@@ -188,10 +142,10 @@
</div> </div>
</div> </div>
{# ── Phone hotspot ───────────────────────────────────────────────── #} <div class="card shadow-sm mb-3 settings-card">
<div class="card shadow-sm mb-3">
<div class="card-header py-2"> <div class="card-header py-2">
<p class="text-primary m-0 fw-bold small">Phone hotspot internet</p> <i class="fas fa-mobile-alt"></i>
<p class="text-dark m-0 fw-bold small">Phone hotspot internet</p>
</div> </div>
<div class="card-body py-3"> <div class="card-body py-3">
<p id="uplink-status" class="mb-2 small fw-semibold">Checking network mode…</p> <p id="uplink-status" class="mb-2 small fw-semibold">Checking network mode…</p>
@@ -217,23 +171,96 @@
<button class="btn btn-sm btn-outline-danger" id="phone-hotspot-remove" type="button" disabled>Remove</button> <button class="btn btn-sm btn-outline-danger" id="phone-hotspot-remove" type="button" disabled>Remove</button>
</div> </div>
<p class="form-text small mb-0"> <p class="form-text small mb-0">
Save while connected to home WiFi, then tap <strong>Wait for hotspot</strong> and enable the phone hotspot within 2 min. Save while on home WiFi, then tap <strong>Wait for hotspot</strong> and enable the phone hotspot within 2 min.
Password is write-only. If the Pi hotspot doesn't appear via <strong>yaris.local</strong>, use the last phone IP above. Password is write-only. If the Pi hotspot doesn't appear via <strong>yaris.local</strong>, use the last phone IP above.
</p> </p>
</div> </div>
</div> </div>
{# ── Cellular + ntfy (collapsible together) ─────────────────────── #} {# ═══ Server & uploads ══════════════════════════════════════════════ #}
<div class="card shadow-sm mb-3"> <div class="settings-section">Server &amp; uploads</div>
<div class="card shadow-sm mb-3 settings-card">
<div class="card-header py-2">
<i class="fas fa-cloud"></i>
<p class="text-dark m-0 fw-bold small">Remote server</p>
</div>
<div class="card-body py-3">
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label form-label-sm" for="remote-server-url">Server URL</label>
<input class="form-control form-control-sm" id="remote-server-url" type="url" placeholder="https://your-server.example">
<div class="invalid-feedback" data-error-for="server_url"></div>
</div>
<div class="col-md-4">
<label class="form-label form-label-sm" for="remote-device-name">Device name</label>
<input class="form-control form-control-sm" id="remote-device-name" maxlength="100" placeholder="yaris-2013">
<div class="invalid-feedback" data-error-for="remote_device_name"></div>
</div>
<div class="col-md-4">
<label class="form-label form-label-sm" for="remote-api-key">API key</label>
<input class="form-control form-control-sm" id="remote-api-key" type="password" maxlength="512" autocomplete="new-password" placeholder="Paste after create/rotate">
<div class="invalid-feedback" data-error-for="api_key"></div>
<div class="form-text" id="remote-api-key-state">Leave blank to keep saved key.</div>
<div class="form-check mt-1">
<input class="form-check-input" type="checkbox" id="clear-remote-api-key">
<label class="form-check-label small" for="clear-remote-api-key">Remove saved API key</label>
</div>
</div>
</div>
<p class="form-text small mb-0">
Stored only on this Pi in <code>db/upload_creds.env</code> — reused by uploads and config sync.
</p>
</div>
</div>
<div class="card shadow-sm mb-3 settings-card">
<div class="card-header py-2">
<i class="fas fa-upload"></i>
<p class="text-dark m-0 fw-bold small">Uploads &amp; storage</p>
</div>
<div class="card-body py-3">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="trips-upload-enabled">
<label class="form-check-label" for="trips-upload-enabled">Upload trips &amp; GPS logs to server</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="dashcam-upload-enabled">
<label class="form-check-label" for="dashcam-upload-enabled">Upload dashcam videos to server (WiFi only)</label>
</div>
<p id="upload-health" class="small text-muted mb-3">Checking…</p>
<div class="d-flex align-items-center gap-2">
<label class="form-label mb-0 text-nowrap" for="retention-days">Delete after</label>
<div class="input-group input-group-sm" style="max-width:10rem;">
<input class="form-control" id="retention-days" type="number" min="0" max="3650" step="1" required>
<span class="input-group-text">days</span>
</div>
<span class="small text-muted">(0 = keep forever)</span>
</div>
<div class="invalid-feedback" data-error-for="local_video_retention_days"></div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" role="switch" id="video-convert-enabled">
<label class="form-check-label" for="video-convert-enabled">
Convert imported videos for web playback
<span class="text-muted small d-block">Re-encodes MOV→MP4 in the background after import. Original deleted when done.</span>
</label>
</div>
</div>
</div>
{# ═══ Alerts ════════════════════════════════════════════════════════ #}
<div class="settings-section">Alerts &amp; notifications</div>
<div class="card shadow-sm mb-3 settings-card">
<div class="card-header py-2 d-flex align-items-center justify-content-between" <div class="card-header py-2 d-flex align-items-center justify-content-between"
style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#connectivity-body" style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#connectivity-body"
aria-expanded="false" aria-controls="connectivity-body"> aria-expanded="false" aria-controls="connectivity-body">
<p class="text-primary m-0 fw-bold small">Cellular &amp; notifications</p> <div class="d-flex align-items-center gap-2">
<i class="fas fa-bell"></i>
<p class="text-dark m-0 fw-bold small">Cellular &amp; notifications</p>
</div>
<i class="fas fa-chevron-down text-muted small collapse-chevron"></i> <i class="fas fa-chevron-down text-muted small collapse-chevron"></i>
</div> </div>
<div class="collapse" id="connectivity-body"> <div class="collapse" id="connectivity-body">
<div class="card-body py-3"> <div class="card-body py-3">
{# Cellular #}
<p class="fw-semibold small text-secondary text-uppercase mb-2">SIM868 Cellular</p> <p class="fw-semibold small text-secondary text-uppercase mb-2">SIM868 Cellular</p>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="cellular-enabled"> <input class="form-check-input" type="checkbox" role="switch" id="cellular-enabled">
@@ -247,7 +274,6 @@
<div class="form-text">Small trip-log batches only. Dashcam video never uses cellular.</div> <div class="form-text">Small trip-log batches only. Dashcam video never uses cellular.</div>
</div> </div>
<hr class="my-3"> <hr class="my-3">
{# ntfy #}
<p class="fw-semibold small text-secondary text-uppercase mb-2">ntfy push notifications</p> <p class="fw-semibold small text-secondary text-uppercase mb-2">ntfy push notifications</p>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="ntfy-enabled"> <input class="form-check-input" type="checkbox" role="switch" id="ntfy-enabled">
@@ -279,12 +305,14 @@
</div> </div>
</div> </div>
{# ── Alert thresholds (collapsible) ──────────────────────────────── #} <div class="card shadow-sm mb-3 settings-card">
<div class="card shadow-sm mb-3">
<div class="card-header py-2 d-flex align-items-center justify-content-between" <div class="card-header py-2 d-flex align-items-center justify-content-between"
style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#thresholds-body" style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#thresholds-body"
aria-expanded="false" aria-controls="thresholds-body"> aria-expanded="false" aria-controls="thresholds-body">
<p class="text-primary m-0 fw-bold small">Alert thresholds</p> <div class="d-flex align-items-center gap-2">
<i class="fas fa-sliders-h"></i>
<p class="text-dark m-0 fw-bold small">Alert thresholds</p>
</div>
<i class="fas fa-chevron-down text-muted small collapse-chevron"></i> <i class="fas fa-chevron-down text-muted small collapse-chevron"></i>
</div> </div>
<div class="collapse" id="thresholds-body"> <div class="collapse" id="thresholds-body">