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:
+52
-35
@@ -10,18 +10,14 @@ import threading
|
||||
import time
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from . import app_network, app_sql
|
||||
from . import app_network, app_remote_config, app_sql
|
||||
|
||||
|
||||
_SYNC_THREAD = None
|
||||
_SYNC_STOP = False
|
||||
_SYNC_CONFIG_VERSION = 0 # Track last-synced version to detect changes
|
||||
_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():
|
||||
@@ -39,16 +35,15 @@ 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_name, device_key
|
||||
credentials = app_remote_config.server_credentials()
|
||||
device_key = os.getenv("YARIS_DEVICE_KEY") or credentials["api_key"]
|
||||
return credentials["server_url"], credentials["device_name"], device_key
|
||||
|
||||
|
||||
def start_sync():
|
||||
"""Start background device config sync thread."""
|
||||
global _SYNC_THREAD, _SYNC_STOP
|
||||
if _SYNC_THREAD is not None:
|
||||
if _SYNC_THREAD is not None and _SYNC_THREAD.is_alive():
|
||||
return
|
||||
|
||||
_SYNC_STOP = False
|
||||
@@ -70,36 +65,13 @@ def stop_sync():
|
||||
|
||||
def _sync_loop():
|
||||
"""Periodically sync device config from server when internet available."""
|
||||
global _SYNC_CONFIG_VERSION
|
||||
|
||||
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:
|
||||
if _has_internet():
|
||||
response = requests.get(
|
||||
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()
|
||||
new_version = config.get("config_version", 0)
|
||||
if new_version != _SYNC_CONFIG_VERSION:
|
||||
_SYNC_CONFIG_VERSION = new_version
|
||||
_store_synced_config(config)
|
||||
_on_config_changed()
|
||||
|
||||
result = sync_now()
|
||||
if result["status"] in {"synced", "no_change"}:
|
||||
time.sleep(sync_interval)
|
||||
else:
|
||||
time.sleep(offline_interval)
|
||||
@@ -109,6 +81,39 @@ def _sync_loop():
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
def sync_now():
|
||||
"""Perform one immediate sync attempt."""
|
||||
global _SYNC_CONFIG_VERSION
|
||||
|
||||
server_url, device_name, device_key = _server_credentials()
|
||||
if not server_url or not device_name or not device_key:
|
||||
return {"status": "not_configured", "message": "Remote server credentials are incomplete."}
|
||||
if not _has_internet():
|
||||
return {"status": "offline", "message": "Internet is not currently reachable."}
|
||||
|
||||
response = requests.get(
|
||||
f"{server_url}/api/device-config",
|
||||
headers={
|
||||
"X-Device-Name": device_name,
|
||||
"X-Api-Key": device_key,
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
response.raise_for_status()
|
||||
config = response.json()
|
||||
new_version = config.get("config_version", 0)
|
||||
|
||||
if new_version != _SYNC_CONFIG_VERSION:
|
||||
_SYNC_CONFIG_VERSION = new_version
|
||||
_store_synced_config(config)
|
||||
_acknowledge_sync(server_url, device_name, device_key, new_version)
|
||||
_on_config_changed()
|
||||
return {"status": "synced", "message": f"Synced config version {new_version}."}
|
||||
|
||||
_acknowledge_sync(server_url, device_name, device_key, new_version)
|
||||
return {"status": "no_change", "message": "Already up to date."}
|
||||
|
||||
|
||||
def _store_synced_config(config):
|
||||
"""Store synced config in SQLite database."""
|
||||
app_sql.set_setting("device_owner_name", config.get("owner_name", ""))
|
||||
@@ -131,6 +136,18 @@ def is_sync_enabled():
|
||||
return bool(server_url) and bool(device_name) and bool(device_key)
|
||||
|
||||
|
||||
def _acknowledge_sync(server_url, device_name, device_key, config_version):
|
||||
requests.post(
|
||||
f"{server_url}/api/v1/device-config/ack",
|
||||
json={"config_version": config_version},
|
||||
headers={
|
||||
"X-Device-Name": device_name,
|
||||
"X-Api-Key": device_key,
|
||||
},
|
||||
timeout=5,
|
||||
).raise_for_status()
|
||||
|
||||
|
||||
def get_synced_config_version():
|
||||
"""Get the last-synced config version number."""
|
||||
return int(app_sql.get_setting("device_config_version", "0"))
|
||||
|
||||
Reference in New Issue
Block a user