fix: reuse upload credentials for device config sync
- load server sync creds from db/upload_creds.env like uploads do - fall back from YARIS_DEVICE_KEY to existing YARIS_API_KEY - document shared credential flow for server-synced identity - add test coverage for sync credential fallback Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -1504,7 +1504,8 @@ phase roadmap.
|
|||||||
- Fetches config from `/api/device-config` endpoint (device_key auth)
|
- Fetches config from `/api/device-config` endpoint (device_key auth)
|
||||||
- Detects changes via config_version
|
- Detects changes via config_version
|
||||||
- Stores synced config in SQLite
|
- Stores synced config in SQLite
|
||||||
- Environment vars: `YARIS_SERVER_URL`, `YARIS_DEVICE_KEY`
|
- Reuses the existing upload credentials from `db/upload_creds.env`
|
||||||
|
(`YARIS_SERVER_URL` + `YARIS_API_KEY`; `YARIS_DEVICE_KEY` also accepted as an override)
|
||||||
- Ready for hot-reload on config change (future: signal handler)
|
- Ready for hot-reload on config change (future: signal handler)
|
||||||
|
|
||||||
- **Pi startup** (`app.py`):
|
- **Pi startup** (`app.py`):
|
||||||
@@ -1525,7 +1526,7 @@ phase roadmap.
|
|||||||
#### Testing
|
#### Testing
|
||||||
- Server endpoints accessible via `/api/device-config`
|
- Server endpoints accessible via `/api/device-config`
|
||||||
- Pi thread starts on boot (check logs for connectivity attempts)
|
- Pi thread starts on boot (check logs for connectivity attempts)
|
||||||
- Sync disabled if `YARIS_SERVER_URL` or `YARIS_DEVICE_KEY` not set
|
- Sync disabled if `YARIS_SERVER_URL` or an API key (`YARIS_API_KEY` / `YARIS_DEVICE_KEY`) is not set
|
||||||
- **2026-08-10 access verification**: SSH access confirmed to Pi at `[email protected]`
|
- **2026-08-10 access verification**: SSH access confirmed to Pi at `[email protected]`
|
||||||
(new WiFi card/subnet) and to unraid via `ssh unraid` (`[email protected]`).
|
(new WiFi card/subnet) and to unraid via `ssh unraid` (`[email protected]`).
|
||||||
Follow-up deployment succeeded the same day: Pi fast-forwarded from `d7b9bb6` to `cd004fb`,
|
Follow-up deployment succeeded the same day: Pi fast-forwarded from `d7b9bb6` to `cd004fb`,
|
||||||
|
|||||||
@@ -203,6 +203,12 @@ nano /home/yaris/app/yaris-obd/config/device.json
|
|||||||
- **`device_name`**: Reserved for server-side display (Phase 25.5 feature, not yet used)
|
- **`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)
|
- **`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.
|
||||||
|
|
||||||
After editing, restart the app:
|
After editing, restart the app:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from . import app_network, app_sql
|
from . import app_network, app_sql
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ _SYNC_THREAD = None
|
|||||||
_SYNC_STOP = False
|
_SYNC_STOP = False
|
||||||
_SYNC_CONFIG_VERSION = 0 # Track last-synced version to detect changes
|
_SYNC_CONFIG_VERSION = 0 # Track last-synced version to detect changes
|
||||||
_RELOAD_CALLBACK = None
|
_RELOAD_CALLBACK = None
|
||||||
|
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
load_dotenv(os.path.join(_BASE_DIR, "db", "upload_creds.env"))
|
||||||
|
|
||||||
|
|
||||||
def _has_internet():
|
def _has_internet():
|
||||||
@@ -34,6 +38,12 @@ def _has_internet():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _server_credentials():
|
||||||
|
server_url = os.getenv("YARIS_SERVER_URL", "").rstrip("/")
|
||||||
|
device_key = os.getenv("YARIS_DEVICE_KEY") or os.getenv("YARIS_API_KEY", "")
|
||||||
|
return server_url, device_key
|
||||||
|
|
||||||
|
|
||||||
def start_sync():
|
def start_sync():
|
||||||
"""Start background device config sync thread."""
|
"""Start background device config sync thread."""
|
||||||
global _SYNC_THREAD, _SYNC_STOP
|
global _SYNC_THREAD, _SYNC_STOP
|
||||||
@@ -61,8 +71,7 @@ def _sync_loop():
|
|||||||
"""Periodically sync device config from server when internet available."""
|
"""Periodically sync device config from server when internet available."""
|
||||||
global _SYNC_CONFIG_VERSION
|
global _SYNC_CONFIG_VERSION
|
||||||
|
|
||||||
server_url = os.getenv("YARIS_SERVER_URL", "")
|
server_url, device_key = _server_credentials()
|
||||||
device_key = os.getenv("YARIS_DEVICE_KEY", "")
|
|
||||||
|
|
||||||
if not server_url or not device_key:
|
if not server_url or not device_key:
|
||||||
return # Sync disabled if server URL or device key not configured
|
return # Sync disabled if server URL or device key not configured
|
||||||
@@ -125,7 +134,8 @@ def _on_config_changed():
|
|||||||
|
|
||||||
def is_sync_enabled():
|
def is_sync_enabled():
|
||||||
"""Check if device config sync from server is enabled."""
|
"""Check if device config sync from server is enabled."""
|
||||||
return bool(os.getenv("YARIS_SERVER_URL")) and bool(os.getenv("YARIS_DEVICE_KEY"))
|
server_url, device_key = _server_credentials()
|
||||||
|
return bool(server_url) and bool(device_key)
|
||||||
|
|
||||||
|
|
||||||
def get_synced_config_version():
|
def get_synced_config_version():
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
import os
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from modules import app_account, app_car, app_device_sync
|
from modules import app_account, app_car, app_device_sync
|
||||||
@@ -55,6 +56,13 @@ class DeviceSyncReloadCallbackTest(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
app_device_sync._RELOAD_CALLBACK = original
|
app_device_sync._RELOAD_CALLBACK = original
|
||||||
|
|
||||||
|
@patch.dict(os.environ, {"YARIS_SERVER_URL": "https://example.test", "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()
|
||||||
|
|
||||||
|
self.assertEqual(server_url, "https://example.test")
|
||||||
|
self.assertEqual(device_key, "upload-key")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user