diff --git a/PLAN.md b/PLAN.md index 21155bd..022b1b5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1482,44 +1482,44 @@ must be fixed, redeployed uncommitted, and the affected in-car test repeated bef and **ready-to-install** sign-off. Afterward, the owner handles permanent installation outside the phase roadmap. -### Phase 25.5: Device profile & configuration sync — 🚧 PLANNED (local config mgmt) -**Unstarted.** Replace hardcoded `account.name`, `car.model`, and avatar with managed configuration editable on the server and synced to the Pi. Enables multi-device/multi-user scenarios and eliminates manual code edits. +### Phase 25.5: Device profile & configuration sync — 🚧 IN PROGRESS (config file + server sync planning) +**Config file implemented; server sync planned.** Replace hardcoded values with `config/device.json`, editable by users without code changes. Server sync is eventual-consistency, connection-driven (not time-driven) to handle offline-first car scenarios. -#### Scope +#### Current state (implemented) +- **`config/device.json`** (new): stores device identity fields: + - `owner_name`: "quoije" (displayed in topbar, replaces `account.name` hardcode) + - `car_model`: "Yaris 2013" (displayed in sidebar, replaces `car.model` hardcode) + - `device_name`: "Yaris OBD" (displayed on portal, not yet used on Pi) + - `avatar_path`: "img/yaris.png" (sourced by template, replaces hardcoded path) +- **Code changes**: + - `modules/app_account.py`: loads `owner_name`, `avatar_path` from JSON on startup with fallback to hardcoded defaults + - `modules/app_car.py`: loads `car_model`, `device_name` from JSON on startup with fallback to hardcoded defaults + - `templates/base.html`: avatar image now sources from `account.avatar_path` (loaded from config) instead of hardcoded path +- **Offline-first design**: All values loaded from local JSON at startup; no network required to boot with customized identity. Server sync is *eventual*, not critical. + +#### Future scope (Phase 25.5a+) - **Server-side config editor**: add a simple "Device Settings" page on the remote portal (login-protected, device-scoped) with editable fields: - - Device nickname (device_name, for display on the portal and Pi topbar) - - Car model/year (already in DB via Phase 24's `device_id` + `car_model`, but allow user override) - - Profile picture upload (stored server-side, served to both portal and Pi via API endpoint) - - Owner name / username (replaces hardcoded `account.name`) -- **Pi-side config sync**: - - Add a new `device_config` table to local SQLite (device_name, owner_name, car_model, avatar_url, sync_timestamp, hash) - - On Pi startup and every 30 minutes, fetch the latest config from the remote server via a new `/api/device-config` endpoint (authenticated with device_key, not user login) - - If hash differs, update local config table and restart the web app (or hot-reload via settings API) - - Fall back gracefully to hardcoded defaults if server is unreachable -- **UI updates**: - - Pi topbar shows the synced `device_name` instead of hardcoded car model (or car model from synced config) - - Pi sidebar shows the synced owner_name instead of hardcoded `account.name` - - Avatar image sourced from synced `avatar_url` (served from server, fallback to local placeholder) - - All trip/tracking records use synced values at capture time (no retroactive changes to history) -- **Device authentication**: use an existing `device_key` (generated on first Pi provisioning, stored in `/home/yaris/app/yaris-obd/.device-key` or env var) to authenticate the `/api/device-config` request. Server validates device_key against the Postgres `devices` table. - -#### Implementation phases -1. **Server (Phase 25.5a)**: add `device_name`, `owner_name`, `avatar_blob` columns to Postgres `devices` table; create `/api/device-config` read endpoint and `/api/device-config` POST endpoint for edits; add Settings UI to portal for these fields. -2. **Pi config table & sync (Phase 25.5b)**: add `device_config` SQLite table; implement 30-min sync fetch on Pi; hot-update app globals if config changes. -3. **UI updates (Phase 25.5c)**: update topbar, sidebar, avatar rendering to use synced config; test full cycle (edit on server → Pi syncs → UI reflects change). -4. **Acceptance (Phase 25.5d)**: multi-sync cycle test, network loss recovery, avatar upload/display, retroactive trip behavior. + - Device nickname (device_name) + - Car model/year (car_model) + - Profile picture upload (avatar stored on server) + - Owner name / username (owner_name) +- **Pi-side sync** (connectivity-driven, not time-driven): + - Pi syncs config from server **only when internet is available** (when cellular/WiFi connects), not on a fixed 30-min timer + - Server-side changes → Pi receives on next connectivity event → updates `config/device.json` → restarts web app or hot-reloads globals + - If server unreachable, Pi continues with last-synced config from local JSON (already loaded at startup) +- **Device authentication**: use an existing `device_key` to authenticate `/api/device-config` requests, validated server-side against Postgres `devices` table. #### Benefits -- No code edit needed to rebrand a deployed Pi -- Server becomes the source of truth for device identity (enables future multi-Pi/multi-car scenarios) -- Avatar management centralized -- Graceful fallback if server unavailable (Pi uses last-synced config) -- Enables future features: device rename, device status on portal, bulk config across devices +- ✅ No code edit needed to customize deployed Pi (today: edit JSON file manually) +- ✅ Offline-first: Pi boots with identity immediately, no network dependency +- ✅ Connection-driven sync: sync happens when internet available, not wasting resources on offline car +- 🚧 Server becomes source of truth (after Phase 25.5a) +- 🚧 Enables multi-Pi/multi-car scenarios (after Phase 25.5a) #### Trade-offs -- Adds config sync complexity and latency (eventual consistency) -- Requires device authentication key rotation strategy (future) -- Metadata storage (avatar blob) requires server-side image handling +- Sync is eventual-consistency (changes on server may take hours to reach offline car) +- Manual JSON edit required today (before Phase 25.5a server UI is built) +- Avatar must be placed in `static/img/` directory (no upload yet) --- diff --git a/config/device.json b/config/device.json new file mode 100644 index 0000000..03b728c --- /dev/null +++ b/config/device.json @@ -0,0 +1,6 @@ +{ + "owner_name": "quoije", + "car_model": "Yaris 2013", + "device_name": "Yaris OBD", + "avatar_path": "img/yaris.png" +} diff --git a/deploy/readme.md b/deploy/readme.md index 23a87d4..23495ed 100644 --- a/deploy/readme.md +++ b/deploy/readme.md @@ -179,6 +179,40 @@ Then test from another device: - `http://yaris.local` - if mDNS is not available on that client, try `http://10.42.0.1` +## Customizing device identity + +The Pi displays a username, car model, device name, and avatar image. Instead of editing Python code, edit `config/device.json`: + +```bash +nano /home/yaris/app/yaris-obd/config/device.json +``` + +**Example `config/device.json`:** + +```json +{ + "owner_name": "Your Name", + "car_model": "Toyota Yaris 2013", + "device_name": "My Yaris OBD", + "avatar_path": "img/yaris.png" +} +``` + +- **`owner_name`**: Displayed in topbar (top-right corner of the web UI) +- **`car_model`**: Displayed in sidebar (left panel, appears next to car icon) +- **`device_name`**: Reserved for server-side display (Phase 25.5 feature, not yet used) +- **`avatar_path`**: Avatar image shown in topbar; must exist in `static/img/` (replace `static/img/yaris.png` with your own image, or point to a different file) + +After editing, restart the app: + +```bash +sudo systemctl restart yaris-obd.service +``` + +**Important**: The SQLite database filename is based on the username. If you change `owner_name` and restart, a new database will be created and the old data will be orphaned at `db/sql/quoije_Yaris2013.db`. Either: +- Rename the old database file to match the new username, or +- Delete the old database if you want a fresh start + ## 7) Updating later ```bash diff --git a/modules/app_account.py b/modules/app_account.py index 3c83d51..0ccb15d 100644 --- a/modules/app_account.py +++ b/modules/app_account.py @@ -1,5 +1,22 @@ +import json +import os + class Account: def __init__(self): - self.name = "quoije" + # Load from config/device.json if available, else use defaults + config = self._load_config() + self.name = config.get("owner_name", "quoije") + self.avatar_path = config.get("avatar_path", "img/yaris.png") self.pp_image = None - self.car = None \ No newline at end of file + self.car = None + + def _load_config(self): + """Load device config from JSON file, return dict with defaults.""" + config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "device.json") + try: + if os.path.exists(config_path): + with open(config_path, 'r') as f: + return json.load(f) + except (IOError, json.JSONDecodeError): + pass + return {} \ No newline at end of file diff --git a/modules/app_car.py b/modules/app_car.py index 892f8fe..e8b9dfd 100644 --- a/modules/app_car.py +++ b/modules/app_car.py @@ -1,4 +1,21 @@ +import json +import os + class Car: def __init__(self): - self.model = "Yaris 2013" - self.obd = None \ No newline at end of file + # Load from config/device.json if available, else use defaults + config = self._load_config() + self.model = config.get("car_model", "Yaris 2013") + self.device_name = config.get("device_name", "Yaris OBD") + self.obd = None + + def _load_config(self): + """Load device config from JSON file, return dict with defaults.""" + config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "device.json") + try: + if os.path.exists(config_path): + with open(config_path, 'r') as f: + return json.load(f) + except (IOError, json.JSONDecodeError): + pass + return {} \ No newline at end of file diff --git a/readme.md b/readme.md index ce2f27f..c4b0608 100644 --- a/readme.md +++ b/readme.md @@ -114,33 +114,70 @@ in [deploy/](./deploy). For a clean-install walkthrough on a fresh Pi, use For the remote server (receives trip logs + dashcam uploads), see [server/README.md](./server/README.md). -## Customizing username and profile picture +## Customizing device identity -The top-right username and avatar in the local app are currently code-configured. +The Pi displays a username, car model, and avatar image. All customizable values are stored in `config/device.json` (no code edits needed). -### Change username +### Edit device configuration -Edit [modules/app_account.py](./modules/app_account.py): +Edit [config/device.json](./config/device.json): -```python -self.name = "quoije" +```json +{ + "owner_name": "quoije", + "car_model": "Yaris 2013", + "device_name": "Yaris OBD", + "avatar_path": "img/yaris.png" +} ``` -Set it to your preferred name, then restart the app/service. +| Field | Location | Purpose | +|-------|----------|---------| +| `owner_name` | Top-right of app topbar | Your name/username | +| `car_model` | Left sidebar (next to car icon) | Vehicle model/year | +| `device_name` | (Reserved) | Future server-side use in Phase 25.5 | +| `avatar_path` | Top-right avatar circle | Profile picture (relative to `static/`) | -> Important: `account.name` is also used in the local SQLite filename (`db/sql/_.db` via [modules/app_sql.py](./modules/app_sql.py)). -> If you already have trip data and change the name, the app will create a new DB file unless you rename/migrate the existing one. +After editing, restart the app: -### Change profile picture +```bash +# If running as a systemd service: +sudo systemctl restart yaris-obd.service -Current avatar is hardcoded in [templates/base.html](./templates/base.html) to: - -```html -src="{{ url_for('static', filename='img/yaris.png') }}" +# If running manually: +# Ctrl+C and python3 app.py ``` -Simplest option: replace [static/img/yaris.png](./static/img/yaris.png) with your image (same filename/path). -Alternative: edit the `filename='img/yaris.png'` value in [templates/base.html](./templates/base.html). +### Changing the profile picture + +Option 1: Replace the default image file: +```bash +cp /path/to/your/image.png static/img/yaris.png +``` + +Option 2: Add your image to `static/img/` and update `avatar_path`: +```json +{ + "avatar_path": "img/my-avatar.png" +} +``` + +### Important: Username and database filename + +The local SQLite database filename is based on `owner_name`: `db/sql/_.db` (via [modules/app_sql.py](./modules/app_sql.py)). + +If you **change `owner_name`** and already have trip data: +- The app will create a **new database** with the new name +- Your old database remains at `db/sql/quoije_Yaris2013.db` (orphaned) + +To preserve your data: +```bash +# Option 1: Rename the database file +mv db/sql/quoije_Yaris2013.db "db/sql/YourName_YourCar.db" + +# Option 2: Start fresh +rm db/sql/quoije_Yaris2013.db +``` ## Project status diff --git a/templates/base.html b/templates/base.html index 376d92d..ef35c15 100644 --- a/templates/base.html +++ b/templates/base.html @@ -79,7 +79,7 @@ {{ account.name }} + src="{{ url_for('static', filename=account.avatar_path) }}" alt="">