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,31 @@
|
||||
{% macro data_table(id, columns, empty_title, empty_message) %}
|
||||
<div class="data-list" id="{{ id }}" data-data-list>
|
||||
<div class="table-responsive data-list-table" data-data-list-table hidden>
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in columns %}
|
||||
<th scope="col"{% if column.class %} class="{{ column.class }}"{% endif %}{% if column.sortable %} aria-sort="none"{% endif %}>
|
||||
{% if column.sortable %}
|
||||
<button class="data-list-sort" type="button" data-sort-key="{{ column.key }}">
|
||||
{{ column.label }}
|
||||
<i class="fas fa-sort" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
{{ column.label }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{# Each injected row must define data-<column.key> for sortable columns. #}
|
||||
<tbody data-data-list-body></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="data-list-empty text-center" data-data-list-empty>
|
||||
<i class="fas fa-inbox fa-2x text-gray-300 mb-3" aria-hidden="true"></i>
|
||||
<h6 class="text-dark mb-1">{{ empty_title }}</h6>
|
||||
<p class="text-muted mb-0">{{ empty_message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user