Browse Source

Add JSON editor for device config, fix config merging

- Add Interactive/JSON tabs to device edit modal (like default config)
- In interactive mode: merge changes with original config to preserve
  ssh_tunnel, dashboard_tunnel, and other fields not in the form
- In JSON mode: save config as-is for full control
- Remove hardcoded 192.168.5.4 defaults (was overwriting real values)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
root 2 weeks ago
parent
commit
201977fd6c
1 changed files with 77 additions and 8 deletions
  1. 77 8
      frontend/src/views/superadmin/DevicesView.vue

+ 77 - 8
frontend/src/views/superadmin/DevicesView.vue

@@ -151,7 +151,16 @@
           <h2>Device #{{ editingDevice?.simple_id }} - {{ editingDevice?.mac_address }}</h2>
           <button @click="closeModal" class="btn-close">×</button>
         </div>
+        <div class="modal-tabs">
+          <button type="button" @click="configTab = 'interactive'" :class="['tab-button', { active: configTab === 'interactive' }]">
+            Interactive
+          </button>
+          <button type="button" @click="configTab = 'json'" :class="['tab-button', { active: configTab === 'json' }]">
+            JSON
+          </button>
+        </div>
         <form @submit.prevent="saveDevice" class="modal-body">
+          <div v-if="configTab === 'interactive'">
           <!-- WiFi Scanner Section -->
           <div class="config-section">
             <h3>WiFi Scanner</h3>
@@ -267,6 +276,20 @@
               </label>
             </div>
           </div>
+          </div>
+
+          <div v-if="configTab === 'json'">
+            <div class="form-group">
+              <label>Device Config JSON (edit carefully!)</label>
+              <textarea
+                v-model="configJson"
+                class="config-editor"
+                rows="20"
+                @input="validateConfigJson"
+              ></textarea>
+              <div v-if="configJsonError" class="form-error">{{ configJsonError }}</div>
+            </div>
+          </div>
 
           <div class="modal-footer">
             <button type="button" @click="deleteDevice" class="btn-danger btn-delete" :disabled="saving">
@@ -274,7 +297,7 @@
             </button>
             <div style="flex: 1"></div>
             <button type="button" @click="closeModal" class="btn-secondary">{{ $t('common.cancel') }}</button>
-            <button type="submit" :disabled="saving" class="btn-primary">
+            <button type="submit" :disabled="saving || (configTab === 'json' && !!configJsonError)" class="btn-primary">
               {{ saving ? $t('common.loading') : $t('common.save') }}
             </button>
           </div>
@@ -488,6 +511,10 @@ const onlineOnly = ref(false)
 const sortColumn = ref('simple_id')
 const sortDirection = ref('desc')
 const ntpServersText = ref('')
+const configTab = ref('interactive')
+const configJson = ref('')
+const configJsonError = ref(null)
+const originalDeviceConfig = ref(null)
 const tunnelLoading = ref({})
 const defaultConfigModalVisible = ref(false)
 const defaultConfigTab = ref('interactive')
@@ -678,7 +705,11 @@ function formatRelativeTime(dateStr) {
 
 function showEditModal(device) {
   editingDevice.value = device
-  // Deep copy device config with defaults
+
+  // Store original config for merging (don't lose ssh_tunnel, etc)
+  originalDeviceConfig.value = JSON.parse(JSON.stringify(device.config || {}))
+
+  // Extract only fields we edit in interactive mode
   config.value = {
     force_cloud: device.config?.force_cloud ?? false,
     cfg_polling_timeout: device.config?.cfg_polling_timeout ?? 30,
@@ -710,12 +741,30 @@ function showEditModal(device) {
   // Convert NTP servers array to comma-separated string
   ntpServersText.value = config.value.net.ntp.servers.join(', ')
 
+  // JSON editor shows FULL config
+  configJson.value = JSON.stringify(device.config || {}, null, 2)
+  configJsonError.value = null
+  configTab.value = 'interactive'
+
   modalVisible.value = true
 }
 
 function closeModal() {
   modalVisible.value = false
   editingDevice.value = null
+  originalDeviceConfig.value = null
+  configTab.value = 'interactive'
+  configJson.value = ''
+  configJsonError.value = null
+}
+
+function validateConfigJson() {
+  try {
+    JSON.parse(configJson.value)
+    configJsonError.value = null
+  } catch (e) {
+    configJsonError.value = 'Invalid JSON: ' + e.message
+  }
 }
 
 function onWifiClientChange() {
@@ -735,13 +784,33 @@ function onWifiMonitorChange() {
 async function saveDevice() {
   saving.value = true
   try {
-    // Parse NTP servers from comma-separated string to array
-    config.value.net.ntp.servers = ntpServersText.value
-      .split(',')
-      .map(s => s.trim())
-      .filter(s => s.length > 0)
+    let configToSave
+
+    if (configTab.value === 'json') {
+      // JSON mode: use JSON as-is
+      configToSave = JSON.parse(configJson.value)
+    } else {
+      // Interactive mode: merge with original config to preserve ssh_tunnel, etc
+      config.value.net.ntp.servers = ntpServersText.value
+        .split(',')
+        .map(s => s.trim())
+        .filter(s => s.length > 0)
+
+      // Deep merge: original config + interactive changes
+      configToSave = {
+        ...originalDeviceConfig.value,
+        ...config.value,
+        ble: { ...originalDeviceConfig.value?.ble, ...config.value.ble },
+        wifi: { ...originalDeviceConfig.value?.wifi, ...config.value.wifi },
+        dashboard: { ...originalDeviceConfig.value?.dashboard, ...config.value.dashboard },
+        net: {
+          ...originalDeviceConfig.value?.net,
+          ntp: { ...originalDeviceConfig.value?.net?.ntp, ...config.value.net.ntp }
+        }
+      }
+    }
 
-    await devicesApi.updateSuperadmin(editingDevice.value.id, { config: config.value })
+    await devicesApi.updateSuperadmin(editingDevice.value.id, { config: configToSave })
     await loadDevices()
     closeModal()
   } catch (err) {