- 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]>
47 lines
2.3 KiB
HTML
47 lines
2.3 KiB
HTML
<!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>Login – Yaris OBD Remote</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') }}">
|
||
</head>
|
||
<body class="bg-light d-flex align-items-center justify-content-center" style="min-height:100vh;">
|
||
<div class="card shadow-sm p-4" style="width:100%;max-width:380px;">
|
||
<div class="text-center mb-4">
|
||
<i class="fas fa-car fa-2x text-primary mb-2"></i>
|
||
<h5 class="fw-bold mb-0">Yaris OBD</h5>
|
||
<div class="badge bg-warning text-dark mt-1">Remote server</div>
|
||
</div>
|
||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||
{% if messages %}
|
||
{% for category, message in messages %}
|
||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} py-2 small" role="alert">
|
||
{{ message }}
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
<form method="post" action="{{ url_for('login_page') }}">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
<div class="mb-3">
|
||
<label for="username" class="form-label small fw-semibold">Username</label>
|
||
<input type="text" class="form-control" id="username" name="username"
|
||
autocomplete="username" autofocus required>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="password" class="form-label small fw-semibold">Password</label>
|
||
<input type="password" class="form-control" id="password" name="password"
|
||
autocomplete="current-password" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary w-100">
|
||
<i class="fas fa-sign-in-alt me-1"></i> Sign in
|
||
</button>
|
||
</form>
|
||
</div>
|
||
<script src="{{ url_for('static', filename='bootstrap/js/bootstrap.min.js') }}"></script>
|
||
</body>
|
||
</html>
|