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:
+92
-1
@@ -8,6 +8,9 @@ const phonePassword = document.getElementById('phone-hotspot-password');
|
||||
const phoneSave = document.getElementById('phone-hotspot-save');
|
||||
const phoneConnect = document.getElementById('phone-hotspot-connect');
|
||||
const phoneRemove = document.getElementById('phone-hotspot-remove');
|
||||
const remoteConfigMessage = document.getElementById('remote-config-message');
|
||||
const remoteConfigTestButton = document.getElementById('remote-config-test');
|
||||
const remoteConfigSyncNowButton = document.getElementById('remote-config-sync-now');
|
||||
const lastPhoneAddress = document.getElementById('last-phone-address');
|
||||
const lastPhoneAddressLink = document.getElementById('last-phone-address-link');
|
||||
let initialUplinkStatusLoaded = false;
|
||||
@@ -35,6 +38,10 @@ const fields = {
|
||||
deviceCarModel: document.getElementById('device-car-model'),
|
||||
deviceAvatarPath: document.getElementById('device-avatar-path'),
|
||||
deviceName: document.getElementById('device-name'),
|
||||
remoteServerUrl: document.getElementById('remote-server-url'),
|
||||
remoteDeviceName: document.getElementById('remote-device-name'),
|
||||
remoteApiKey: document.getElementById('remote-api-key'),
|
||||
clearRemoteApiKey: document.getElementById('clear-remote-api-key'),
|
||||
};
|
||||
|
||||
function showMessage(text, kind) {
|
||||
@@ -49,6 +56,12 @@ function showUplinkMessage(text, kind) {
|
||||
uplinkMessage.hidden = false;
|
||||
}
|
||||
|
||||
function showRemoteConfigMessage(text, kind) {
|
||||
remoteConfigMessage.textContent = text;
|
||||
remoteConfigMessage.className = `alert alert-${kind} py-2 mb-3`;
|
||||
remoteConfigMessage.hidden = false;
|
||||
}
|
||||
|
||||
function setSaving(disabled) {
|
||||
saveButtons.forEach(button => {
|
||||
button.disabled = disabled;
|
||||
@@ -78,6 +91,9 @@ function applyErrors(errors) {
|
||||
car_model: fields.deviceCarModel,
|
||||
avatar_path: fields.deviceAvatarPath,
|
||||
device_name: fields.deviceName,
|
||||
server_url: fields.remoteServerUrl,
|
||||
remote_device_name: fields.remoteDeviceName,
|
||||
api_key: fields.remoteApiKey,
|
||||
};
|
||||
Object.entries(errors).forEach(([name, text]) => {
|
||||
const input = errorElements[name];
|
||||
@@ -123,6 +139,16 @@ function populateDeviceConfig(deviceConfig) {
|
||||
updateDeviceConfigFieldsState();
|
||||
}
|
||||
|
||||
function populateRemoteConfig(remoteConfig) {
|
||||
fields.remoteServerUrl.value = remoteConfig.server_url || '';
|
||||
fields.remoteDeviceName.value = remoteConfig.device_name || '';
|
||||
fields.remoteApiKey.value = '';
|
||||
fields.clearRemoteApiKey.checked = false;
|
||||
document.getElementById('remote-api-key-state').textContent = remoteConfig.api_key_configured
|
||||
? 'An API key is saved. Leave blank to keep it unchanged.'
|
||||
: 'No API key is saved yet.';
|
||||
}
|
||||
|
||||
function updateDeviceConfigFieldsState() {
|
||||
const syncEnabled = fields.deviceSyncEnabled.checked;
|
||||
const configFields = [fields.deviceOwnerName, fields.deviceCarModel, fields.deviceAvatarPath, fields.deviceName];
|
||||
@@ -165,6 +191,15 @@ function deviceConfigPayload() {
|
||||
};
|
||||
}
|
||||
|
||||
function remoteConfigPayload() {
|
||||
return {
|
||||
server_url: fields.remoteServerUrl.value,
|
||||
device_name: fields.remoteDeviceName.value,
|
||||
api_key: fields.remoteApiKey.value,
|
||||
clear_api_key: fields.clearRemoteApiKey.checked,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadUploadHealth() {
|
||||
const element = document.getElementById('upload-health');
|
||||
try {
|
||||
@@ -321,12 +356,54 @@ async function loadDeviceConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRemoteConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/remote-config');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
populateRemoteConfig(await response.json());
|
||||
} catch (error) {
|
||||
showMessage(`Could not load remote config: ${error.message}`, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
fields.deviceSyncEnabled.addEventListener('change', updateDeviceConfigFieldsState);
|
||||
|
||||
remoteConfigTestButton.addEventListener('click', async () => {
|
||||
remoteConfigMessage.hidden = true;
|
||||
remoteConfigTestButton.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/remote-config/test', {method: 'POST'});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
|
||||
showRemoteConfigMessage(result.message || 'Remote server connection looks good.', 'success');
|
||||
} catch (error) {
|
||||
showRemoteConfigMessage(error.message, 'danger');
|
||||
} finally {
|
||||
remoteConfigTestButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
remoteConfigSyncNowButton.addEventListener('click', async () => {
|
||||
remoteConfigMessage.hidden = true;
|
||||
remoteConfigSyncNowButton.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/remote-config/sync-now', {method: 'POST'});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.message || `HTTP ${response.status}`);
|
||||
await loadDeviceConfig();
|
||||
showRemoteConfigMessage(result.message || 'Sync completed.', 'success');
|
||||
} catch (error) {
|
||||
showRemoteConfigMessage(error.message, 'danger');
|
||||
} finally {
|
||||
remoteConfigSyncNowButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
clearErrors();
|
||||
message.hidden = true;
|
||||
remoteConfigMessage.hidden = true;
|
||||
setSaving(true);
|
||||
try {
|
||||
// Save main settings
|
||||
@@ -341,7 +418,20 @@ form.addEventListener('submit', async event => {
|
||||
throw new Error(result.error || `HTTP ${response.status}`);
|
||||
}
|
||||
populate(result);
|
||||
|
||||
|
||||
// Save remote server credentials before device sync toggle validation
|
||||
const remoteConfigResponse = await fetch('/api/remote-config', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(remoteConfigPayload()),
|
||||
});
|
||||
const remoteConfigResult = await remoteConfigResponse.json();
|
||||
if (!remoteConfigResponse.ok) {
|
||||
applyErrors(remoteConfigResult.fields || {});
|
||||
throw new Error(remoteConfigResult.error || `HTTP ${remoteConfigResponse.status}`);
|
||||
}
|
||||
populateRemoteConfig(remoteConfigResult);
|
||||
|
||||
// Save device config
|
||||
const deviceConfigResponse = await fetch('/api/device-config', {
|
||||
method: 'POST',
|
||||
@@ -365,6 +455,7 @@ form.addEventListener('submit', async event => {
|
||||
|
||||
loadSettings();
|
||||
loadDeviceConfig();
|
||||
loadRemoteConfig();
|
||||
loadUploadHealth();
|
||||
loadUplinkStatus();
|
||||
setInterval(loadUplinkStatus, 15000);
|
||||
|
||||
Reference in New Issue
Block a user