Phase 24: remote trip + dashcam portal
- Add Flask-Login browser session auth (separate from device X-Api-Key auth)
- modules/user_auth.py: User model, require_login decorator, bcrypt helpers
- /login with CSRF token, /logout
- scripts/create_user.py to provision admin user
- Add server-side trip computation from tracking_points
- db.compute_trips_for_device(): gap > 5 min = new trip, haversine distance,
upserts into trips table, auto-triggered in bg thread after each upload batch
- db.list_trips(), db.get_trip_route(): same JSON shape as Pi API
- db.get_overview_stats(): aggregated totals for overview page
- db.list_dashcam_files(), db.get_dashcam_file(): browse/stream helpers
- Add web portal to server/app.py
- Overview page (/): stat cards + recent trips + recent clips
- Trip history (/history): reuses Pi history.js + data-list.js unchanged
via /api/trips + /api/trip/<id> compatibility aliases
- Dashcam archive (/dashcam): file list + inline video player
- /api/v1/portal/dashcam/stream/<id>: Range-request streaming, path traversal guard
- /api/geocode: same Nominatim→reverse_geocoder offline fallback as Pi
- Add server templates (server/templates/)
- base.html: shared shell with 'Remote server' badge in sidebar + mobile topbar
- login.html, index.html, history.html, dashcam.html, components.html
- DB schema: users + trips tables added to init.sql (fresh deploys) +
migrate_phase24.sql (existing deployments)
- Infrastructure: Dockerfile uses repo-root build context to copy shared
static/ assets; docker-compose updated; SECRET_KEY + DEVICE_NAME in .env;
requirements: Flask-Login, bcrypt, reverse_geocoder
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="light" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{% block title %}Yaris OBD – Remote{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='yaris.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/fontawesome-all.min.css') }}">
|
||||
{% block styles %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body id="page-top">
|
||||
|
||||
{# ── Desktop sidebar ─────────────────────────────────────────────────── #}
|
||||
<nav id="app-sidebar" class="app-sidebar d-none d-md-flex flex-column">
|
||||
<a class="sidebar-brand" href="{{ url_for('portal_index') }}" aria-label="Home">
|
||||
<i class="fas fa-car sidebar-brand-icon"></i>
|
||||
<span class="sidebar-brand-text">Yaris OBD</span>
|
||||
</a>
|
||||
<div class="px-3 pb-1">
|
||||
<span class="badge bg-warning text-dark w-100 text-center" style="font-size:.65rem;letter-spacing:.04em;">
|
||||
<i class="fas fa-cloud fa-fw"></i> Remote server
|
||||
</span>
|
||||
</div>
|
||||
<hr class="sidebar-divider">
|
||||
<ul class="nav flex-column flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if request.endpoint == 'portal_index' %} active{% endif %}" href="{{ url_for('portal_index') }}">
|
||||
<i class="fas fa-chart-line fa-fw"></i><span>Overview</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if request.endpoint == 'portal_history' %} active{% endif %}" href="{{ url_for('portal_history') }}">
|
||||
<i class="fas fa-route fa-fw"></i><span>Trip history</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link{% if request.endpoint == 'portal_dashcam' %} active{% endif %}" href="{{ url_for('portal_dashcam') }}">
|
||||
<i class="fas fa-video fa-fw"></i><span>Dashcam archive</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-3 pb-2 mt-auto text-muted" style="font-size:.75rem;">
|
||||
<i class="fas fa-user fa-fw"></i> {{ current_user.username }}
|
||||
·
|
||||
<a href="{{ url_for('logout') }}" class="text-muted">Logout</a>
|
||||
</div>
|
||||
<button class="sidebar-collapse-btn" id="sidebarToggle" type="button" aria-label="Collapse sidebar">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{# ── Main content area ────────────────────────────────────────────────── #}
|
||||
<div id="content-wrapper">
|
||||
|
||||
{# Top bar #}
|
||||
<nav class="topbar navbar navbar-light bg-white shadow-sm">
|
||||
<div class="container-fluid px-3 gap-2">
|
||||
<a class="navbar-brand d-flex d-md-none align-items-center gap-2 text-decoration-none" href="{{ url_for('portal_index') }}">
|
||||
<i class="fas fa-car text-primary"></i>
|
||||
<span class="fw-bold text-dark">Yaris OBD</span>
|
||||
<span class="badge bg-warning text-dark ms-1" style="font-size:.6rem;">Remote</span>
|
||||
</a>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<span class="text-muted small d-none d-sm-inline">{{ current_user.username }}</span>
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-outline-secondary btn-sm" title="Logout">
|
||||
<i class="fas fa-sign-out-alt fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container-fluid px-3 px-md-4 main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-white border-top text-center py-3 small text-muted d-none d-md-block">
|
||||
© vroom vroom yaris de course 2023 — remote server
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{# ── Mobile bottom navigation ──────────────────────────────────────────── #}
|
||||
<nav class="mobile-bottom-nav d-flex d-md-none" aria-label="Main navigation">
|
||||
<a class="mobile-nav-item{% if request.endpoint == 'portal_index' %} active{% endif %}" href="{{ url_for('portal_index') }}">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a class="mobile-nav-item{% if request.endpoint == 'portal_history' %} active{% endif %}" href="{{ url_for('portal_history') }}">
|
||||
<i class="fas fa-route"></i>
|
||||
<span>History</span>
|
||||
</a>
|
||||
<a class="mobile-nav-item{% if request.endpoint == 'portal_dashcam' %} active{% endif %}" href="{{ url_for('portal_dashcam') }}">
|
||||
<i class="fas fa-video"></i>
|
||||
<span>Dashcam</span>
|
||||
</a>
|
||||
<a class="mobile-nav-item" href="{{ url_for('logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<script src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/bs-init.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user