feat: add multi-device provisioning and local remote-config UI

- add multi-device portal device management, key rotation, and sync request APIs
- add Pi settings UI for remote server URL, device name, and API key
- add local test-connection and sync-now actions
- make upload and config sync reuse dynamic shared credentials
- add Phase 25.5c migration and docs updates

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 18:02:09 -04:00
co-authored by Copilot
parent fbc41a5db5
commit ba682fbf3a
23 changed files with 1357 additions and 167 deletions
+27 -19
View File
@@ -41,17 +41,9 @@ import time
from threading import Thread
import requests
from dotenv import load_dotenv
from requests_toolbelt import MultipartEncoder
from modules import app_sql, app_dash, app_cellular, app_video
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
load_dotenv(os.path.join(_BASE_DIR, "db", "upload_creds.env"))
SERVER_URL = os.getenv("YARIS_SERVER_URL", "").rstrip("/")
DEVICE_NAME = os.getenv("YARIS_DEVICE_NAME", "")
API_KEY = os.getenv("YARIS_API_KEY", "")
from modules import app_sql, app_dash, app_cellular, app_remote_config, app_video
UPLOAD_INTERVAL = 30 # seconds between upload attempts once caught up
CATCHUP_INTERVAL = 1 # seconds between batches while draining a backlog
@@ -75,12 +67,24 @@ _last_success_at = None
_last_network_path = None # "wifi" or "cellular" - last path a batch actually went out on
def _headers():
return {"X-Device-Name": DEVICE_NAME, "X-Api-Key": API_KEY}
def _credentials():
return app_remote_config.server_credentials()
def _headers(credentials):
return {
"X-Device-Name": credentials["device_name"],
"X-Api-Key": credentials["api_key"],
}
def is_configured():
return bool(SERVER_URL and DEVICE_NAME and API_KEY)
credentials = _credentials()
return bool(
credentials["server_url"]
and credentials["device_name"]
and credentials["api_key"]
)
def is_dashcam_upload_enabled():
@@ -151,6 +155,7 @@ def _upload_tracking_batch():
rows = app_sql.get_unsent_tracking_rows(limit=TRACKING_BATCH_SIZE)
if not rows:
return 0
credentials = _credentials()
points = [
{
@@ -168,9 +173,9 @@ def _upload_tracking_batch():
try:
resp = requests.post(
f"{SERVER_URL}/api/v1/tracking/batch",
f"{credentials['server_url']}/api/v1/tracking/batch",
json={"points": points},
headers=_headers(),
headers=_headers(credentials),
timeout=15,
)
resp.raise_for_status()
@@ -182,7 +187,9 @@ def _upload_tracking_batch():
body_bytes = json.dumps({"points": points}).encode()
try:
status_code, _body = app_cellular.http_post_json(
f"{SERVER_URL}/api/v1/tracking/batch", body_bytes, _headers()
f"{credentials['server_url']}/api/v1/tracking/batch",
body_bytes,
_headers(credentials),
)
except app_cellular.CellularError as cell_err:
# Neither path worked - raise the original WiFi error so the
@@ -207,10 +214,11 @@ def _upload_video_chunked(abs_path, rel_path, sha256):
re-query /status and resume from the first missing chunk instead of
starting over.
"""
credentials = _credentials()
status_resp = requests.get(
f"{SERVER_URL}/api/v1/dashcam/upload/status",
f"{credentials['server_url']}/api/v1/dashcam/upload/status",
params={"relative_path": rel_path, "sha256": sha256},
headers=_headers(),
headers=_headers(credentials),
timeout=15,
)
status_resp.raise_for_status()
@@ -241,9 +249,9 @@ def _upload_video_chunked(abs_path, rel_path, sha256):
}
)
resp = requests.post(
f"{SERVER_URL}/api/v1/dashcam/upload/chunk",
f"{credentials['server_url']}/api/v1/dashcam/upload/chunk",
data=encoder,
headers={**_headers(), "Content-Type": encoder.content_type},
headers={**_headers(credentials), "Content-Type": encoder.content_type},
timeout=120,
)
resp.raise_for_status()