|
|
@@ -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) {
|