- Settings: when the Pi is on the phone's hotspot (mode phone), show the Pi's hotspot IP (http://<ip>) with an LTE-only hint instead of the AP URL - BUG-15 documents the phone dual-role routing loop (host hotspot + connect to AP simultaneously breaks the uplink)
404 lines
24 KiB
HTML
404 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% 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 %}
|
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
|
<div>
|
|
<h3 class="text-dark mb-0 fw-bold">Settings</h3>
|
|
<p class="text-muted small mb-0">Stored locally on this Pi.</p>
|
|
</div>
|
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
|
<button class="btn btn-outline-secondary btn-sm" id="remote-config-test" type="button" title="Test remote server connection">
|
|
<i class="fas fa-plug"></i> Test connection
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" id="remote-config-sync-now" type="button" title="Sync device config from server now">
|
|
<i class="fas fa-rotate"></i> Sync now
|
|
</button>
|
|
<button class="btn btn-primary btn-sm d-none d-md-inline-flex align-items-center gap-1 settings-save"
|
|
type="submit" form="settings-form" disabled>
|
|
<i class="fas fa-save"></i> Save settings
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="remote-config-message" class="alert py-2" role="status" hidden></div>
|
|
|
|
<div id="settings-message" class="alert" role="status" hidden></div>
|
|
|
|
<form id="settings-form">
|
|
|
|
{# ═══ Car & identity ═══════════════════════════════════════════════ #}
|
|
<div class="settings-section">Car & identity</div>
|
|
<div class="card shadow-sm mb-3 settings-card">
|
|
<div class="card-header py-2">
|
|
<i class="fas fa-car"></i>
|
|
<p class="text-dark m-0 fw-bold small">Device identity</p>
|
|
</div>
|
|
<div class="card-body py-3">
|
|
<div class="form-check form-switch mb-3">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="device-sync-enabled">
|
|
<label class="form-check-label" for="device-sync-enabled">
|
|
Sync with server
|
|
<span class="text-muted small d-block">If enabled, the server is the source of truth and these fields become read-only.</span>
|
|
</label>
|
|
</div>
|
|
<div id="device-config-fields">
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label form-label-sm" for="device-owner-name">Owner name</label>
|
|
<input class="form-control form-control-sm" id="device-owner-name" maxlength="100" required>
|
|
<div class="invalid-feedback" data-error-for="owner_name"></div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label form-label-sm" for="device-car-model">Car model</label>
|
|
<input class="form-control form-control-sm" id="device-car-model" maxlength="100" required>
|
|
<div class="invalid-feedback" data-error-for="car_model"></div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label form-label-sm" for="device-avatar-path">Avatar path</label>
|
|
<input class="form-control form-control-sm" id="device-avatar-path" maxlength="255" placeholder="img/yaris.png" required>
|
|
<div class="invalid-feedback" data-error-for="avatar_path"></div>
|
|
<div class="form-text">Relative path from <code>static/</code> directory.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<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">
|
|
<div class="invalid-feedback" data-error-for="device_name"></div>
|
|
<div class="form-text">Shown in the server portal.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<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">
|
|
<div class="invalid-feedback" data-error-for="odometer_baseline"></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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ═══ Connectivity ══════════════════════════════════════════════════ #}
|
|
<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">
|
|
<i class="fas fa-wifi"></i>
|
|
<p class="text-dark m-0 fw-bold small">In-car hotspot</p>
|
|
</div>
|
|
<div class="card-body py-3">
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-sm-6">
|
|
<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">
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label form-label-sm" for="ap-password">Password</label>
|
|
<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">
|
|
<div class="form-text">Write-only — never shown again.</div>
|
|
</div>
|
|
</div>
|
|
<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="in-car-hotspot-info" hidden>
|
|
<p class="mb-2 fw-semibold">Your phone can reach the Pi at:</p>
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<code class="fs-5" id="hotspot-url">http://10.42.0.1</code>
|
|
<button class="btn btn-outline-secondary btn-sm" id="copy-hotspot-url" type="button" title="Copy">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
<div class="alert alert-warning py-2 mb-0 small">
|
|
<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.
|
|
</div>
|
|
</div>
|
|
<div id="in-car-phone-info" hidden>
|
|
<p class="mb-2 fw-semibold">Your phone can reach the Pi at:</p>
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<code class="fs-5" id="phone-url">http://10.21.0.112</code>
|
|
<button class="btn btn-outline-secondary btn-sm" id="copy-phone-url" type="button" title="Copy">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
<div class="alert alert-info py-2 mb-0 small">
|
|
<i class="fas fa-info-circle me-1"></i>
|
|
The Pi is on your phone's hotspot. Keep the phone on <strong>mobile data (LTE)</strong> — don't also connect it to the Pi's WiFi.
|
|
</div>
|
|
</div>
|
|
<div id="in-car-wifi-info" hidden>
|
|
<p class="mb-1 small text-muted">The Pi is connected to WiFi. Open <code>http://yaris.local</code> from any device on the same network.</p>
|
|
</div>
|
|
<div id="in-car-disconnected-info" hidden>
|
|
<p class="mb-0 small text-muted">Waiting for network…</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3 settings-card">
|
|
<div class="card-header py-2">
|
|
<i class="fas fa-mobile-alt"></i>
|
|
<p class="text-dark m-0 fw-bold small">Phone hotspot internet</p>
|
|
</div>
|
|
<div class="card-body py-3">
|
|
<p id="uplink-status" class="mb-2 small fw-semibold">Checking network mode…</p>
|
|
<p id="last-phone-address" class="small mb-3" hidden>
|
|
Last phone IP: <a id="last-phone-address-link" href="#"></a>
|
|
<span class="text-muted">(may change after reconnecting)</span>
|
|
</p>
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-sm-6">
|
|
<label class="form-label form-label-sm" for="phone-hotspot-ssid">Hotspot name</label>
|
|
<input class="form-control form-control-sm" id="phone-hotspot-ssid" maxlength="32" autocomplete="off">
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<label class="form-label form-label-sm" for="phone-hotspot-password">Password</label>
|
|
<input class="form-control form-control-sm" id="phone-hotspot-password" type="password"
|
|
minlength="8" maxlength="63" autocomplete="new-password">
|
|
</div>
|
|
</div>
|
|
<div id="uplink-message" class="alert py-2 mt-2 mb-0" role="status" hidden></div>
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
<button class="btn btn-sm btn-primary" id="phone-hotspot-save" type="button">Save</button>
|
|
<button class="btn btn-sm btn-outline-primary" id="phone-hotspot-connect" type="button" disabled>Wait for hotspot</button>
|
|
<button class="btn btn-sm btn-outline-danger" id="phone-hotspot-remove" type="button" disabled>Remove</button>
|
|
</div>
|
|
<p class="form-text small mb-0">
|
|
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.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{# ═══ Server & uploads ══════════════════════════════════════════════ #}
|
|
<div class="settings-section">Server & 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 & 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 & 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 & 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"
|
|
style="cursor:pointer;" data-bs-toggle="collapse" data-bs-target="#connectivity-body"
|
|
aria-expanded="false" aria-controls="connectivity-body">
|
|
<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 & notifications</p>
|
|
</div>
|
|
<i class="fas fa-chevron-down text-muted small collapse-chevron"></i>
|
|
</div>
|
|
<div class="collapse" id="connectivity-body">
|
|
<div class="card-body py-3">
|
|
<p class="fw-semibold small text-secondary text-uppercase mb-2">SIM868 Cellular</p>
|
|
<div class="form-check form-switch mb-2">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="cellular-enabled">
|
|
<label class="form-check-label" for="cellular-enabled">Enable SIM868 cellular fallback</label>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label form-label-sm" for="cellular-apn">Carrier APN</label>
|
|
<input class="form-control form-control-sm" id="cellular-apn" maxlength="100"
|
|
autocomplete="off" placeholder="example.apn">
|
|
<div class="invalid-feedback" data-error-for="cellular_apn"></div>
|
|
<div class="form-text">Small trip-log batches only. Dashcam video never uses cellular.</div>
|
|
</div>
|
|
<hr class="my-3">
|
|
<p class="fw-semibold small text-secondary text-uppercase mb-2">ntfy push notifications</p>
|
|
<div class="form-check form-switch mb-2">
|
|
<input class="form-check-input" type="checkbox" role="switch" id="ntfy-enabled">
|
|
<label class="form-check-label" for="ntfy-enabled">Send alerts via ntfy</label>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label form-label-sm" for="ntfy-url">Topic URL</label>
|
|
<input class="form-control form-control-sm" id="ntfy-url" type="url" maxlength="2048"
|
|
autocomplete="off" placeholder="https://ntfy.sh/my-topic">
|
|
<div class="invalid-feedback" data-error-for="ntfy_url"></div>
|
|
<div class="form-text" id="ntfy-url-state">Leave blank to keep saved URL.</div>
|
|
<div class="form-check mt-1">
|
|
<input class="form-check-input" type="checkbox" id="clear-ntfy-url">
|
|
<label class="form-check-label small" for="clear-ntfy-url">Remove saved URL</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-0">
|
|
<label class="form-label form-label-sm" for="ntfy-token">Access token</label>
|
|
<input class="form-control form-control-sm" id="ntfy-token" type="password"
|
|
maxlength="512" autocomplete="new-password" placeholder="Optional">
|
|
<div class="invalid-feedback" data-error-for="ntfy_token"></div>
|
|
<div class="form-text" id="ntfy-token-state">Leave blank to keep saved token.</div>
|
|
<div class="form-check mt-1">
|
|
<input class="form-check-input" type="checkbox" id="clear-ntfy-token">
|
|
<label class="form-check-label small" for="clear-ntfy-token">Remove saved token</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3 settings-card">
|
|
<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"
|
|
aria-expanded="false" aria-controls="thresholds-body">
|
|
<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>
|
|
</div>
|
|
<div class="collapse" id="thresholds-body">
|
|
<div class="card-body py-3">
|
|
<div class="row g-3">
|
|
<div class="col-6">
|
|
<label class="form-label form-label-sm" for="temperature-c">Pi temp</label>
|
|
<div class="input-group input-group-sm">
|
|
<input class="form-control" id="temperature-c" type="number" min="40" max="100" step="0.1" required>
|
|
<span class="input-group-text">°C</span>
|
|
</div>
|
|
<div class="invalid-feedback" data-error-for="temperature_c"></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label form-label-sm" for="disk-percent">Disk usage</label>
|
|
<div class="input-group input-group-sm">
|
|
<input class="form-control" id="disk-percent" type="number" min="50" max="100" step="0.1" required>
|
|
<span class="input-group-text">%</span>
|
|
</div>
|
|
<div class="invalid-feedback" data-error-for="disk_percent"></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label form-label-sm" for="tracking-backlog">Tracking backlog</label>
|
|
<input class="form-control form-control-sm" id="tracking-backlog" type="number" min="1" max="1000000" step="1" required>
|
|
<div class="invalid-feedback" data-error-for="tracking_backlog"></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label form-label-sm" for="video-backlog">Video backlog</label>
|
|
<input class="form-control form-control-sm" id="video-backlog" type="number" min="1" max="100000" step="1" required>
|
|
<div class="invalid-feedback" data-error-for="video_backlog"></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label form-label-sm" for="gps-loss-seconds">GPS loss</label>
|
|
<div class="input-group input-group-sm">
|
|
<input class="form-control" id="gps-loss-seconds" type="number" min="10" max="3600" step="1" required>
|
|
<span class="input-group-text">s</span>
|
|
</div>
|
|
<div class="invalid-feedback" data-error-for="gps_loss_seconds"></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label form-label-sm" for="repeat-seconds">Repeat interval</label>
|
|
<div class="input-group input-group-sm">
|
|
<input class="form-control" id="repeat-seconds" type="number" min="60" max="604800" step="1" required>
|
|
<span class="input-group-text">s</span>
|
|
</div>
|
|
<div class="invalid-feedback" data-error-for="repeat_seconds"></div>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted small mt-3 mb-0">Web UI alerts are always shown regardless of ntfy.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn btn-primary w-100 d-md-none settings-save mb-4" type="submit" disabled>
|
|
<i class="fas fa-save me-1"></i>Save settings
|
|
</button>
|
|
</form>
|
|
|
|
<script>
|
|
// Rotate chevrons on collapsible card headers
|
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(function (trigger) {
|
|
const targetId = trigger.getAttribute('data-bs-target');
|
|
const target = document.querySelector(targetId);
|
|
if (!target) return;
|
|
const chevron = trigger.querySelector('.collapse-chevron');
|
|
if (!chevron) return;
|
|
target.addEventListener('show.bs.collapse', function () { chevron.style.transform = 'rotate(180deg)'; });
|
|
target.addEventListener('hide.bs.collapse', function () { chevron.style.transform = ''; });
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
|
|
{% endblock %}
|