feat: add remote device settings portal and Pi hot reload
- add /settings/device portal page and portal device-config GET endpoint - add portal navigation and client-side device settings editor UI - hot-reload Account/Car display config after local saves and server syncs - document portal workflow and Phase 25.5b completion - add runtime reload tests for account, car, and sync callback Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+8
-1
@@ -65,6 +65,10 @@ so a permissive origin can't be used to steal a logged-in session.
|
||||
docker compose ps
|
||||
curl http://localhost:8098/health
|
||||
```
|
||||
5. Sign in to the remote portal and open `/settings/device` to edit the synced
|
||||
owner/car/device/avatar values for the configured `DEVICE_NAME`. If the Pi
|
||||
has local **Sync with server** enabled, those values hot-reload into the
|
||||
running Pi UI after the next successful sync.
|
||||
|
||||
### Upgrading an existing unraid deployment
|
||||
|
||||
@@ -80,7 +84,10 @@ docker compose up -d --build
|
||||
|
||||
Phase 25.5a adds the device identity sync columns (`owner_name`, `car_model`,
|
||||
`device_name`, `avatar_path`, `config_version`, `config_updated_at`) used by the
|
||||
Pi/server config sync flow.
|
||||
Pi/server config sync flow. The browser portal page at `/settings/device` edits
|
||||
those values; the Pi applies them on its next successful sync when local
|
||||
**Sync with server** is enabled, and Phase 25.5b now hot-reloads the in-process
|
||||
UI fields instead of requiring an app restart.
|
||||
|
||||
## Registering a device and getting an API key
|
||||
|
||||
|
||||
+26
-1
@@ -1,6 +1,6 @@
|
||||
"""yaris-obd remote server: receives trip/GPS log batches and dashcam files
|
||||
uploaded by the Pi, storing them in Postgres + on-disk file storage.
|
||||
Also serves a read-only web portal for browsing trip history and dashcam archive.
|
||||
Also serves the remote web portal for browsing and managing the configured car.
|
||||
"""
|
||||
import functools
|
||||
import hashlib
|
||||
@@ -414,6 +414,15 @@ def portal_dashcam():
|
||||
return render_template("dashcam.html")
|
||||
|
||||
|
||||
@app.get("/settings/device")
|
||||
@require_login
|
||||
def portal_device_settings_page():
|
||||
return render_template(
|
||||
"device_settings.html",
|
||||
configured_device_name=os.environ.get("DEVICE_NAME", "yaris-2013"),
|
||||
)
|
||||
|
||||
|
||||
# ── Portal JSON API ────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/v1/portal/stats")
|
||||
@@ -621,6 +630,22 @@ def portal_trip_compat(trip_id):
|
||||
|
||||
# ── Phase 25.5a: Device configuration sync ──────────────────────────────────
|
||||
|
||||
@app.get("/api/v1/portal/device-config")
|
||||
@require_login
|
||||
def portal_device_config_get():
|
||||
"""Get the configured device identity for the remote portal."""
|
||||
device_id = _portal_device_id()
|
||||
if device_id is None:
|
||||
return jsonify(error="device not configured"), 404
|
||||
|
||||
config = device_config.get_device_config(device_id)
|
||||
if not config:
|
||||
return jsonify(error="device not found"), 404
|
||||
|
||||
config["configured_device_name"] = os.environ.get("DEVICE_NAME", "yaris-2013")
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
@app.get("/api/device-config")
|
||||
@require_device_auth
|
||||
def api_device_config_get():
|
||||
|
||||
@@ -49,6 +49,11 @@
|
||||
<i class="fas fa-video fa-fw"></i><span>Dashcam archive</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if request.endpoint == 'portal_device_settings_page' %} active{% endif %}" href="{{ url_for('portal_device_settings_page') }}">
|
||||
<i class="fas fa-sliders-h fa-fw"></i><span>Device settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-3 pb-2 mt-auto text-muted d-flex align-items-center gap-1 sidebar-user-footer" style="font-size:.75rem;">
|
||||
<i class="fas fa-user fa-fw flex-shrink-0"></i>
|
||||
@@ -108,6 +113,10 @@
|
||||
<i class="fas fa-video"></i>
|
||||
<span>Dashcam</span>
|
||||
</a>
|
||||
<a class="mobile-nav-item{% if request.endpoint == 'portal_device_settings_page' %} active{% endif %}" href="{{ url_for('portal_device_settings_page') }}">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
<span>Device</span>
|
||||
</a>
|
||||
<a class="mobile-nav-item" href="{{ url_for('logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Device settings – Yaris OBD Remote{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<h3 class="text-dark mb-1 fw-bold">Device settings</h3>
|
||||
<p class="text-muted mb-0">
|
||||
Edit the identity synced to the Pi for <code>{{ configured_device_name }}</code>.
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="device-settings-save" type="submit" form="device-settings-form" disabled>
|
||||
<i class="fas fa-save me-1"></i>Save device settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="device-settings-message" class="alert" role="status" hidden></div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="text-primary fw-bold m-0">Synced identity</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="device-settings-form" novalidate>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="owner_name">Owner name</label>
|
||||
<input class="form-control" id="owner_name" name="owner_name" maxlength="100" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="car_model">Car model</label>
|
||||
<input class="form-control" id="car_model" name="car_model" maxlength="100" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="device_name">Device name</label>
|
||||
<input class="form-control" id="device_name" name="device_name" maxlength="100">
|
||||
<div class="form-text">Used by the remote portal and future multi-device views.</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="avatar_path">Avatar path</label>
|
||||
<input class="form-control" id="avatar_path" name="avatar_path" maxlength="255" placeholder="img/yaris.png">
|
||||
<div class="form-text">Relative to the shared <code>static/</code> folder.</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="text-primary fw-bold m-0">Sync behavior</h6>
|
||||
</div>
|
||||
<div class="card-body small text-muted">
|
||||
<p class="mb-2">
|
||||
The Pi picks these values up on its next successful internet-backed sync.
|
||||
</p>
|
||||
<ul class="mb-0 ps-3">
|
||||
<li>Expected propagation delay: up to ~5 minutes while online.</li>
|
||||
<li>The Pi must have <strong>Sync with server</strong> enabled locally.</li>
|
||||
<li>Offline cars keep the last synced config until connectivity returns.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header py-2">
|
||||
<h6 class="text-primary fw-bold m-0">Preview</h6>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<img id="avatar-preview" alt="Avatar preview" class="rounded-circle border mb-3" src="{{ url_for('static', filename='img/yaris.png') }}" style="width:96px;height:96px;object-fit:cover;">
|
||||
<div class="small text-muted mb-1">Config version</div>
|
||||
<div class="fw-bold fs-5" id="config-version">—</div>
|
||||
<div class="small text-muted mt-3" id="preview-caption">Preview updates as you type.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/portal-device-settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user