diff --git a/PLAN.md b/PLAN.md index 4fc3a9b..bd633ed 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1505,7 +1505,8 @@ phase roadmap. - Detects changes via config_version - Stores synced config in SQLite - Reuses the existing upload credentials from `db/upload_creds.env` - (`YARIS_SERVER_URL` + `YARIS_API_KEY`; `YARIS_DEVICE_KEY` also accepted as an override) + (`YARIS_SERVER_URL` + `YARIS_DEVICE_NAME` + `YARIS_API_KEY`; + `YARIS_DEVICE_KEY` also accepted as an override for the key) - Ready for hot-reload on config change (future: signal handler) - **Pi startup** (`app.py`): @@ -1526,7 +1527,8 @@ phase roadmap. #### Testing - Server endpoints accessible via `/api/device-config` - Pi thread starts on boot (check logs for connectivity attempts) -- Sync disabled if `YARIS_SERVER_URL` or an API key (`YARIS_API_KEY` / `YARIS_DEVICE_KEY`) is not set +- Sync disabled if `YARIS_SERVER_URL`, `YARIS_DEVICE_NAME`, or an API key + (`YARIS_API_KEY` / `YARIS_DEVICE_KEY`) is not set - **2026-08-10 access verification**: SSH access confirmed to Pi at `yaris@192.168.50.213` (new WiFi card/subnet) and to unraid via `ssh unraid` (`root@192.168.10.10`). Follow-up deployment succeeded the same day: Pi fast-forwarded from `d7b9bb6` to `cd004fb`, diff --git a/deploy/readme.md b/deploy/readme.md index d378720..f93bbbb 100644 --- a/deploy/readme.md +++ b/deploy/readme.md @@ -204,10 +204,11 @@ nano /home/yaris/app/yaris-obd/config/device.json - **`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) If you want the remote server to become the source of truth instead, keep the Pi -upload credentials in `db/upload_creds.env` (`YARIS_SERVER_URL` + `YARIS_API_KEY`) -and enable **Sync with server** in the local Settings page. The same creds file is -reused for config sync; when the server pushes new identity values, the running Pi -UI hot-reloads them on the next successful sync. +upload credentials in `db/upload_creds.env` (`YARIS_SERVER_URL`, +`YARIS_DEVICE_NAME`, and `YARIS_API_KEY`) and enable **Sync with server** in the +local Settings page. The same creds file is reused for config sync; when the +server pushes new identity values, the running Pi UI hot-reloads them on the next +successful sync. After editing, restart the app: diff --git a/modules/app_device_sync.py b/modules/app_device_sync.py index d335101..a32a789 100644 --- a/modules/app_device_sync.py +++ b/modules/app_device_sync.py @@ -40,8 +40,9 @@ def _has_internet(): def _server_credentials(): server_url = os.getenv("YARIS_SERVER_URL", "").rstrip("/") + device_name = os.getenv("YARIS_DEVICE_NAME") or os.getenv("YARIS_UPLOAD_DEVICE_NAME", "") device_key = os.getenv("YARIS_DEVICE_KEY") or os.getenv("YARIS_API_KEY", "") - return server_url, device_key + return server_url, device_name, device_key def start_sync(): @@ -49,7 +50,7 @@ def start_sync(): global _SYNC_THREAD, _SYNC_STOP if _SYNC_THREAD is not None: return - + _SYNC_STOP = False _SYNC_THREAD = threading.Thread(target=_sync_loop, daemon=True) _SYNC_THREAD.start() @@ -70,48 +71,40 @@ def stop_sync(): def _sync_loop(): """Periodically sync device config from server when internet available.""" global _SYNC_CONFIG_VERSION - - server_url, device_key = _server_credentials() - - if not server_url or not device_key: - return # Sync disabled if server URL or device key not configured - - # Sync every 5 minutes (300 sec) when connected, backoff to 60 sec when offline + + server_url, device_name, device_key = _server_credentials() + + if not server_url or not device_name or not device_key: + return + sync_interval = 300 offline_interval = 60 - + while not _SYNC_STOP: try: - is_connected = _has_internet() - - if is_connected: - # Fetch device config from server - headers = {"Authorization": f"Bearer {device_key}"} + if _has_internet(): response = requests.get( - f"{server_url.rstrip('/')}/api/device-config", - headers=headers, - timeout=5 + f"{server_url}/api/device-config", + headers={ + "X-Device-Name": device_name, + "X-Api-Key": device_key, + }, + timeout=5, ) - + if response.status_code == 200: config = response.json() - - # Check if config changed new_version = config.get("config_version", 0) if new_version != _SYNC_CONFIG_VERSION: _SYNC_CONFIG_VERSION = new_version - # Store in database _store_synced_config(config) - # Signal that config changed (for hot-reload) _on_config_changed() - + time.sleep(sync_interval) else: - # Offline: backoff to less frequent checks time.sleep(offline_interval) - + except Exception as e: - # Log error but don't crash sync thread print(f"Device config sync error: {e}") time.sleep(60) @@ -134,8 +127,8 @@ def _on_config_changed(): def is_sync_enabled(): """Check if device config sync from server is enabled.""" - server_url, device_key = _server_credentials() - return bool(server_url) and bool(device_key) + server_url, device_name, device_key = _server_credentials() + return bool(server_url) and bool(device_name) and bool(device_key) def get_synced_config_version(): diff --git a/server/README.md b/server/README.md index 0f14736..beebd22 100644 --- a/server/README.md +++ b/server/README.md @@ -97,8 +97,8 @@ Run this once per device (e.g. once for the Pi) after the stack is up: docker compose exec api python3 scripts/create_device.py yaris-2013 ``` -This prints a one-time API key — copy the `YARIS_UPLOAD_DEVICE_NAME` / -`YARIS_UPLOAD_API_KEY` values into the Pi's upload-client config. Only the +This prints a one-time API key — copy the `YARIS_DEVICE_NAME` / +`YARIS_API_KEY` values into the Pi's upload-client config. Only the key's sha256 hash is stored server-side; the raw key is never shown again (re-run the script with the same device name to rotate/replace the key). diff --git a/server/scripts/create_device.py b/server/scripts/create_device.py index 3a288e0..0f7dbc9 100644 --- a/server/scripts/create_device.py +++ b/server/scripts/create_device.py @@ -40,9 +40,9 @@ def main(): print(f"Device '{device_name}' registered (id={device_id}).") print() - print("Save these on the Pi (e.g. in server/.env-style local config, gitignored):") - print(f" YARIS_UPLOAD_DEVICE_NAME={device_name}") - print(f" YARIS_UPLOAD_API_KEY={raw_key}") + print("Save these on the Pi (e.g. in db/upload_creds.env, gitignored):") + print(f" YARIS_DEVICE_NAME={device_name}") + print(f" YARIS_API_KEY={raw_key}") print() print("This key will not be shown again - only its hash is stored server-side.") diff --git a/tests/test_app_device_config_runtime.py b/tests/test_app_device_config_runtime.py index 926e1f7..aa7e2b4 100644 --- a/tests/test_app_device_config_runtime.py +++ b/tests/test_app_device_config_runtime.py @@ -56,11 +56,20 @@ class DeviceSyncReloadCallbackTest(unittest.TestCase): finally: app_device_sync._RELOAD_CALLBACK = original - @patch.dict(os.environ, {"YARIS_SERVER_URL": "https://example.test", "YARIS_API_KEY": "upload-key"}, clear=True) + @patch.dict( + os.environ, + { + "YARIS_SERVER_URL": "https://example.test", + "YARIS_DEVICE_NAME": "yaris-2013", + "YARIS_API_KEY": "upload-key", + }, + clear=True, + ) def test_sync_credentials_fall_back_to_upload_api_key(self): - server_url, device_key = app_device_sync._server_credentials() + server_url, device_name, device_key = app_device_sync._server_credentials() self.assertEqual(server_url, "https://example.test") + self.assertEqual(device_name, "yaris-2013") self.assertEqual(device_key, "upload-key")