feat: FEAT-05 — DTC history sync to remote server
- Server: dtc_events table (migrate_phase27_dtc.sql), db.py helpers (upsert_dtc_snapshot, list_dtc_events, delete_dtc_event) - Server API: POST /api/v1/dtc/batch (device auth), GET/DELETE /api/v1/portal/dtc - Server portal: /dtc page with code, description, MIL, status, timestamps, delete - Server nav: DTC history in sidebar + mobile bottom nav - Pi: _upload_dtc_snapshot() in app_upload.py — uploads when snapshot changes, skips when OBD disconnected or unchanged
This commit is contained in:
@@ -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_dtc_page' %} active{% endif %}" href="{{ url_for('portal_dtc_page') }}">
|
||||
<i class="fas fa-exclamation-triangle fa-fw"></i><span>DTC history</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>
|
||||
@@ -117,6 +122,10 @@
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
<span>Device</span>
|
||||
</a>
|
||||
<a class="mobile-nav-item{% if request.endpoint == 'portal_dtc_page' %} active{% endif %}" href="{{ url_for('portal_dtc_page') }}">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span>DTC</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,94 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}DTC history – Yaris OBD Remote{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h3 class="text-dark mb-3 fw-bold">DTC history</h3>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
||||
<h6 class="text-primary fw-bold m-0">Diagnostic trouble codes</h6>
|
||||
<span id="dtc-count" class="badge bg-secondary">0</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="dtc-loading" class="text-center text-muted py-4">
|
||||
<span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
|
||||
Loading…
|
||||
</div>
|
||||
<div id="dtc-error" class="alert alert-danger m-3" role="alert" hidden></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0" id="dtc-table" hidden>
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Code</th>
|
||||
<th>Description</th>
|
||||
<th>MIL</th>
|
||||
<th>Status</th>
|
||||
<th>First seen</th>
|
||||
<th>Last seen</th>
|
||||
<th class="pe-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dtc-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="dtc-empty" class="text-muted text-center py-4 small" hidden>No DTC events recorded yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
const loading = document.getElementById("dtc-loading");
|
||||
const error = document.getElementById("dtc-error");
|
||||
const table = document.getElementById("dtc-table");
|
||||
const tbody = document.getElementById("dtc-body");
|
||||
const empty = document.getElementById("dtc-empty");
|
||||
const count = document.getElementById("dtc-count");
|
||||
|
||||
function fmt(iso) { return iso ? new Date(iso).toLocaleString() : "—"; }
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const r = await fetch("/api/v1/portal/dtc");
|
||||
if (!r.ok) throw new Error("Cannot load DTC events.");
|
||||
const events = await r.json();
|
||||
loading.hidden = true;
|
||||
count.textContent = events.length;
|
||||
if (!events.length) { empty.hidden = false; return; }
|
||||
tbody.innerHTML = events.map(e => `
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold font-monospace ${e.is_active ? 'text-danger' : 'text-muted'}">${e.code}</td>
|
||||
<td>${e.description || "—"}</td>
|
||||
<td>${e.mil_on ? '<span class="badge text-bg-warning">ON</span>' : '<span class="badge text-bg-secondary">OFF</span>'}</td>
|
||||
<td>${e.is_active ? '<span class="badge text-bg-danger">Active</span>' : '<span class="badge text-bg-success">Cleared</span>'}</td>
|
||||
<td class="small text-muted">${fmt(e.first_seen_at)}</td>
|
||||
<td class="small text-muted">${fmt(e.last_seen_at)}</td>
|
||||
<td class="pe-3"><button class="btn btn-sm btn-outline-danger" data-dtc-id="${e.id}" title="Delete">🗑</button></td>
|
||||
</tr>
|
||||
`).join("");
|
||||
table.hidden = false;
|
||||
|
||||
tbody.querySelectorAll("[data-dtc-id]").forEach(btn => {
|
||||
btn.addEventListener("click", async () => {
|
||||
if (!confirm("Delete this DTC event?")) return;
|
||||
const r = await fetch("/api/v1/portal/dtc/" + btn.dataset.dtcId, {method: "DELETE"});
|
||||
if (!r.ok) { error.textContent = "Could not delete."; error.hidden = false; return; }
|
||||
load();
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
loading.hidden = true;
|
||||
error.textContent = e.message;
|
||||
error.hidden = false;
|
||||
}
|
||||
}
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user