citrascope 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- citrascope/hardware/abstract_astro_hardware_adapter.py +17 -0
- citrascope/hardware/adapter_registry.py +5 -0
- citrascope/hardware/devices/camera/abstract_camera.py +12 -0
- citrascope/hardware/devices/camera/usb_camera.py +20 -15
- citrascope/hardware/devices/camera/ximea_camera.py +22 -10
- citrascope/hardware/direct_hardware_adapter.py +21 -3
- citrascope/hardware/dummy_adapter.py +202 -0
- citrascope/time/__init__.py +2 -1
- citrascope/time/time_health.py +8 -1
- citrascope/time/time_monitor.py +27 -5
- citrascope/time/time_sources.py +199 -0
- citrascope/web/app.py +31 -9
- citrascope/web/static/app.js +118 -988
- citrascope/web/static/components.js +136 -0
- citrascope/web/static/config.js +209 -505
- citrascope/web/static/formatters.js +129 -0
- citrascope/web/static/store-init.js +204 -0
- citrascope/web/static/style.css +5 -0
- citrascope/web/templates/_config.html +175 -0
- citrascope/web/templates/_config_hardware.html +208 -0
- citrascope/web/templates/_monitoring.html +242 -0
- citrascope/web/templates/dashboard.html +69 -442
- {citrascope-0.8.0.dist-info → citrascope-0.9.0.dist-info}/METADATA +2 -1
- {citrascope-0.8.0.dist-info → citrascope-0.9.0.dist-info}/RECORD +27 -20
- {citrascope-0.8.0.dist-info → citrascope-0.9.0.dist-info}/WHEEL +0 -0
- {citrascope-0.8.0.dist-info → citrascope-0.9.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.8.0.dist-info → citrascope-0.9.0.dist-info}/licenses/LICENSE +0 -0
citrascope/web/static/config.js
CHANGED
|
@@ -1,59 +1,55 @@
|
|
|
1
1
|
// Configuration management for CitraScope
|
|
2
2
|
|
|
3
|
-
import { getConfig, saveConfig, getConfigStatus, getHardwareAdapters
|
|
3
|
+
import { getConfig, saveConfig, getConfigStatus, getHardwareAdapters } from './api.js';
|
|
4
4
|
import { getFilterColor } from './filters.js';
|
|
5
5
|
|
|
6
|
+
function updateStoreEnabledFilters(filters) {
|
|
7
|
+
if (typeof Alpine !== 'undefined' && Alpine.store) {
|
|
8
|
+
const enabled = Object.values(filters || {})
|
|
9
|
+
.filter(f => f.enabled !== false)
|
|
10
|
+
.map(f => ({ name: f.name, color: getFilterColor(f.name) }));
|
|
11
|
+
Alpine.store('citrascope').enabledFilters = enabled;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
// API Host constants - must match backend constants in app.py
|
|
7
16
|
const PROD_API_HOST = 'api.citra.space';
|
|
8
17
|
const DEV_API_HOST = 'dev.api.citra.space';
|
|
9
18
|
const DEFAULT_API_PORT = 443;
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Handle adapter selection change (called from Alpine store)
|
|
22
|
+
*/
|
|
23
|
+
async function handleAdapterChange(adapter) {
|
|
24
|
+
const store = Alpine.store('citrascope');
|
|
25
|
+
if (adapter) {
|
|
26
|
+
const allAdapterSettings = store.config.adapter_settings || {};
|
|
27
|
+
const newAdapterSettings = allAdapterSettings[adapter] || {};
|
|
28
|
+
|
|
29
|
+
console.log(`Switching to adapter: ${adapter}`);
|
|
30
|
+
console.log('All adapter settings:', allAdapterSettings);
|
|
31
|
+
console.log('Settings for this adapter:', newAdapterSettings);
|
|
32
|
+
|
|
33
|
+
await loadAdapterSchema(adapter, newAdapterSettings);
|
|
34
|
+
await loadFilterConfig();
|
|
35
|
+
} else {
|
|
36
|
+
store.adapterFields = [];
|
|
37
|
+
store.filterConfigVisible = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
14
40
|
|
|
15
41
|
export async function initConfig() {
|
|
42
|
+
// Attach config methods to Alpine store
|
|
43
|
+
const store = Alpine.store('citrascope');
|
|
44
|
+
store.handleAdapterChange = handleAdapterChange;
|
|
45
|
+
store.reloadAdapterSchema = reloadAdapterSchema;
|
|
46
|
+
|
|
16
47
|
// Populate hardware adapter dropdown
|
|
17
48
|
await loadAdapterOptions();
|
|
18
49
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
adapterSelect.addEventListener('change', async (e) => {
|
|
23
|
-
const adapter = e.target.value;
|
|
24
|
-
if (adapter) {
|
|
25
|
-
// Extract the NEW adapter's saved settings from the nested structure
|
|
26
|
-
const allAdapterSettings = currentConfig.adapter_settings || {};
|
|
27
|
-
const newAdapterSettings = allAdapterSettings[adapter] || {};
|
|
28
|
-
await loadAdapterSchema(adapter, newAdapterSettings);
|
|
29
|
-
populateAdapterSettings(newAdapterSettings);
|
|
30
|
-
await loadFilterConfig();
|
|
31
|
-
} else {
|
|
32
|
-
document.getElementById('adapter-settings-container').innerHTML = '';
|
|
33
|
-
const filterSection = document.getElementById('filterConfigSection');
|
|
34
|
-
if (filterSection) filterSection.style.display = 'none';
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// API endpoint selection change
|
|
40
|
-
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
41
|
-
if (apiEndpointSelect) {
|
|
42
|
-
apiEndpointSelect.addEventListener('change', (e) => {
|
|
43
|
-
const customContainer = document.getElementById('customHostContainer');
|
|
44
|
-
if (e.target.value === 'custom') {
|
|
45
|
-
customContainer.style.display = 'block';
|
|
46
|
-
} else {
|
|
47
|
-
customContainer.style.display = 'none';
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Config form submission
|
|
53
|
-
const configForm = document.getElementById('configForm');
|
|
54
|
-
if (configForm) {
|
|
55
|
-
configForm.addEventListener('submit', saveConfiguration);
|
|
56
|
-
}
|
|
50
|
+
// Config form submission handled by Alpine @submit.prevent in template
|
|
51
|
+
// Expose saveConfiguration for template access
|
|
52
|
+
window.saveConfiguration = saveConfiguration;
|
|
57
53
|
|
|
58
54
|
// Load initial config
|
|
59
55
|
await loadConfiguration();
|
|
@@ -87,21 +83,13 @@ async function checkConfigStatus() {
|
|
|
87
83
|
async function loadAdapterOptions() {
|
|
88
84
|
try {
|
|
89
85
|
const data = await getHardwareAdapters();
|
|
90
|
-
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
91
86
|
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Add options from API
|
|
99
|
-
data.adapters.forEach(adapterName => {
|
|
100
|
-
const option = document.createElement('option');
|
|
101
|
-
option.value = adapterName;
|
|
102
|
-
option.textContent = data.descriptions[adapterName] || adapterName;
|
|
103
|
-
adapterSelect.appendChild(option);
|
|
104
|
-
});
|
|
87
|
+
if (data.adapters && typeof Alpine !== 'undefined' && Alpine.store) {
|
|
88
|
+
const options = data.adapters.map(adapterName => ({
|
|
89
|
+
value: adapterName,
|
|
90
|
+
label: data.descriptions[adapterName] || adapterName
|
|
91
|
+
}));
|
|
92
|
+
Alpine.store('citrascope').hardwareAdapters = options;
|
|
105
93
|
}
|
|
106
94
|
} catch (error) {
|
|
107
95
|
console.error('Failed to load hardware adapters:', error);
|
|
@@ -114,85 +102,34 @@ async function loadAdapterOptions() {
|
|
|
114
102
|
async function loadConfiguration() {
|
|
115
103
|
try {
|
|
116
104
|
const config = await getConfig();
|
|
117
|
-
currentConfig = config; // Save for reuse when saving
|
|
118
|
-
savedAdapter = config.hardware_adapter; // Track saved adapter
|
|
119
105
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
configPathElement.textContent = config.config_file_path;
|
|
124
|
-
}
|
|
106
|
+
// Sync to Alpine store - x-model handles form population
|
|
107
|
+
if (typeof Alpine !== 'undefined' && Alpine.store) {
|
|
108
|
+
const store = Alpine.store('citrascope');
|
|
125
109
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
// Normalize boolean fields that may come as null from backend
|
|
111
|
+
if (config.file_logging_enabled === null || config.file_logging_enabled === undefined) {
|
|
112
|
+
config.file_logging_enabled = true; // Default to true
|
|
113
|
+
}
|
|
114
|
+
if (config.keep_images === null || config.keep_images === undefined) {
|
|
115
|
+
config.keep_images = false; // Default to false
|
|
116
|
+
}
|
|
117
|
+
if (config.scheduled_autofocus_enabled === null || config.scheduled_autofocus_enabled === undefined) {
|
|
118
|
+
config.scheduled_autofocus_enabled = false; // Default to false
|
|
133
119
|
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Display images directory path
|
|
137
|
-
const imagesDirElement = document.getElementById('imagesDirPath');
|
|
138
|
-
if (imagesDirElement && config.images_dir_path) {
|
|
139
|
-
imagesDirElement.textContent = config.images_dir_path;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// API endpoint selector
|
|
143
|
-
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
144
|
-
const customHostContainer = document.getElementById('customHostContainer');
|
|
145
|
-
const customHost = document.getElementById('customHost');
|
|
146
|
-
const customPort = document.getElementById('customPort');
|
|
147
|
-
const customUseSsl = document.getElementById('customUseSsl');
|
|
148
|
-
|
|
149
|
-
if (config.host === PROD_API_HOST) {
|
|
150
|
-
apiEndpointSelect.value = 'production';
|
|
151
|
-
customHostContainer.style.display = 'none';
|
|
152
|
-
} else if (config.host === DEV_API_HOST) {
|
|
153
|
-
apiEndpointSelect.value = 'development';
|
|
154
|
-
customHostContainer.style.display = 'none';
|
|
155
|
-
} else {
|
|
156
|
-
apiEndpointSelect.value = 'custom';
|
|
157
|
-
customHostContainer.style.display = 'block';
|
|
158
|
-
customHost.value = config.host || '';
|
|
159
|
-
customPort.value = config.port || DEFAULT_API_PORT;
|
|
160
|
-
customUseSsl.checked = config.use_ssl !== undefined ? config.use_ssl : true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Core fields
|
|
164
|
-
document.getElementById('personal_access_token').value = config.personal_access_token || '';
|
|
165
|
-
document.getElementById('telescopeId').value = config.telescope_id || '';
|
|
166
|
-
document.getElementById('hardwareAdapterSelect').value = config.hardware_adapter || '';
|
|
167
|
-
document.getElementById('logLevel').value = config.log_level || 'INFO';
|
|
168
|
-
document.getElementById('keep_images').checked = config.keep_images || false;
|
|
169
|
-
document.getElementById('file_logging_enabled').checked = config.file_logging_enabled !== undefined ? config.file_logging_enabled : true;
|
|
170
|
-
|
|
171
|
-
// Load autofocus settings (top-level)
|
|
172
|
-
const scheduledAutofocusEnabled = document.getElementById('scheduled_autofocus_enabled');
|
|
173
|
-
const autofocusInterval = document.getElementById('autofocus_interval_minutes');
|
|
174
|
-
if (scheduledAutofocusEnabled) {
|
|
175
|
-
scheduledAutofocusEnabled.checked = config.scheduled_autofocus_enabled || false;
|
|
176
|
-
}
|
|
177
|
-
if (autofocusInterval) {
|
|
178
|
-
autofocusInterval.value = config.autofocus_interval_minutes || 60;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Load time sync settings (monitoring always enabled)
|
|
182
|
-
const timeOffsetPause = document.getElementById('time_offset_pause_ms');
|
|
183
|
-
|
|
184
|
-
if (timeOffsetPause) {
|
|
185
|
-
timeOffsetPause.value = config.time_offset_pause_ms || 500;
|
|
186
|
-
}
|
|
187
120
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
121
|
+
store.config = config;
|
|
122
|
+
store.savedAdapter = config.hardware_adapter; // Sync savedAdapter to store
|
|
123
|
+
store.apiEndpoint =
|
|
124
|
+
config.host === PROD_API_HOST ? 'production' :
|
|
125
|
+
config.host === DEV_API_HOST ? 'development' : 'custom';
|
|
126
|
+
|
|
127
|
+
// Load adapter-specific settings if adapter is selected
|
|
128
|
+
if (config.hardware_adapter) {
|
|
129
|
+
const allAdapterSettings = config.adapter_settings || {};
|
|
130
|
+
const currentAdapterSettings = allAdapterSettings[config.hardware_adapter] || {};
|
|
131
|
+
await loadAdapterSchema(config.hardware_adapter, currentAdapterSettings);
|
|
132
|
+
}
|
|
196
133
|
}
|
|
197
134
|
} catch (error) {
|
|
198
135
|
console.error('Failed to load config:', error);
|
|
@@ -200,7 +137,16 @@ async function loadConfiguration() {
|
|
|
200
137
|
}
|
|
201
138
|
|
|
202
139
|
/**
|
|
203
|
-
*
|
|
140
|
+
* Get default value for a field type
|
|
141
|
+
*/
|
|
142
|
+
function getDefaultForType(type) {
|
|
143
|
+
if (type === 'bool') return false;
|
|
144
|
+
if (type === 'int' || type === 'float') return 0;
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Load adapter schema and merge with current values
|
|
204
150
|
*/
|
|
205
151
|
async function loadAdapterSchema(adapterName, currentSettings = {}) {
|
|
206
152
|
try {
|
|
@@ -209,190 +155,35 @@ async function loadAdapterSchema(adapterName, currentSettings = {}) {
|
|
|
209
155
|
? `?current_settings=${encodeURIComponent(JSON.stringify(currentSettings))}`
|
|
210
156
|
: '';
|
|
211
157
|
|
|
158
|
+
console.log(`Loading schema for ${adapterName} with settings:`, currentSettings);
|
|
159
|
+
|
|
212
160
|
const response = await fetch(`/api/hardware-adapters/${adapterName}/schema${settingsParam}`);
|
|
213
161
|
const data = await response.json();
|
|
214
162
|
|
|
215
|
-
|
|
216
|
-
renderAdapterSettings(currentAdapterSchema);
|
|
217
|
-
} catch (error) {
|
|
218
|
-
console.error('Failed to load adapter schema:', error);
|
|
219
|
-
showConfigError(`Failed to load settings for ${adapterName}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Render adapter-specific settings form
|
|
225
|
-
*/
|
|
226
|
-
function renderAdapterSettings(schema) {
|
|
227
|
-
const container = document.getElementById('adapter-settings-container');
|
|
228
|
-
|
|
229
|
-
if (!schema || schema.length === 0) {
|
|
230
|
-
container.innerHTML = '';
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Group fields by their 'group' property
|
|
235
|
-
const grouped = schema.reduce((acc, field) => {
|
|
236
|
-
if (field.readonly) return acc; // Skip readonly fields
|
|
237
|
-
const group = field.group || 'General';
|
|
238
|
-
if (!acc[group]) acc[group] = [];
|
|
239
|
-
acc[group].push(field);
|
|
240
|
-
return acc;
|
|
241
|
-
}, {});
|
|
242
|
-
|
|
243
|
-
let html = '<h5 class="mb-3">Adapter Settings</h5>';
|
|
244
|
-
|
|
245
|
-
// Render each group as a card
|
|
246
|
-
Object.entries(grouped).forEach(([groupName, fields]) => {
|
|
247
|
-
html += `<div class="card bg-dark border-secondary mb-3">`;
|
|
248
|
-
html += `<div class="card-header">`;
|
|
249
|
-
html += `<h6 class="mb-0"><i class="bi bi-${getGroupIcon(groupName)} me-2"></i>${groupName}</h6>`;
|
|
250
|
-
html += `</div>`;
|
|
251
|
-
html += `<div class="card-body">`;
|
|
252
|
-
html += `<div class="row g-3">`;
|
|
253
|
-
|
|
254
|
-
fields.forEach(field => {
|
|
255
|
-
const isRequired = field.required ? '<span class="text-danger">*</span>' : '';
|
|
256
|
-
const placeholder = field.placeholder || '';
|
|
257
|
-
const description = field.description || '';
|
|
258
|
-
const displayName = field.friendly_name || field.name;
|
|
259
|
-
|
|
260
|
-
html += '<div class="col-12 col-md-4">';
|
|
261
|
-
html += `<label for="adapter_${field.name}" class="form-label">${displayName} ${isRequired}</label>`;
|
|
262
|
-
|
|
263
|
-
if (field.type === 'bool') {
|
|
264
|
-
html += `<div class="form-check mt-2">`;
|
|
265
|
-
html += `<input class="form-check-input adapter-setting" type="checkbox" id="adapter_${field.name}" data-field="${field.name}" data-type="${field.type}">`;
|
|
266
|
-
html += `<label class="form-check-label" for="adapter_${field.name}">${description}</label>`;
|
|
267
|
-
html += `</div>`;
|
|
268
|
-
} else if (field.options && field.options.length > 0) {
|
|
269
|
-
html += `<select id="adapter_${field.name}" class="form-select adapter-setting" data-field="${field.name}" data-type="${field.type}" ${field.required ? 'required' : ''}>`;
|
|
270
|
-
html += `<option value="">-- Select ${displayName} --</option>`;
|
|
271
|
-
field.options.forEach(opt => {
|
|
272
|
-
// Handle both object format {value, label} and plain string options
|
|
273
|
-
const optValue = typeof opt === 'object' ? opt.value : opt;
|
|
274
|
-
const optLabel = typeof opt === 'object' ? opt.label : opt;
|
|
275
|
-
html += `<option value="${optValue}">${optLabel}</option>`;
|
|
276
|
-
});
|
|
277
|
-
html += `</select>`;
|
|
278
|
-
} else if (field.type === 'int' || field.type === 'float') {
|
|
279
|
-
const min = field.min !== undefined ? `min="${field.min}"` : '';
|
|
280
|
-
const max = field.max !== undefined ? `max="${field.max}"` : '';
|
|
281
|
-
const step = field.type === 'float' ? 'step="any"' : '';
|
|
282
|
-
html += `<input type="number" id="adapter_${field.name}" class="form-control adapter-setting" `;
|
|
283
|
-
html += `data-field="${field.name}" data-type="${field.type}" `;
|
|
284
|
-
html += `placeholder="${placeholder}" ${min} ${max} ${step} ${field.required ? 'required' : ''}>`;
|
|
285
|
-
} else {
|
|
286
|
-
// Default to text input
|
|
287
|
-
const pattern = field.pattern ? `pattern="${field.pattern}"` : '';
|
|
288
|
-
html += `<input type="text" id="adapter_${field.name}" class="form-control adapter-setting" `;
|
|
289
|
-
html += `data-field="${field.name}" data-type="${field.type}" `;
|
|
290
|
-
html += `placeholder="${placeholder}" ${pattern} ${field.required ? 'required' : ''}>`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (description && field.type !== 'bool') {
|
|
294
|
-
html += `<small class="text-muted">${description}</small>`;
|
|
295
|
-
}
|
|
296
|
-
html += '</div>';
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
html += `</div></div></div>`; // Close row, card-body, card
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
container.innerHTML = html;
|
|
303
|
-
|
|
304
|
-
// Add change listeners to device type fields to reload schema dynamically
|
|
305
|
-
const deviceTypeFields = ['camera_type', 'mount_type', 'filter_wheel_type', 'focuser_type'];
|
|
306
|
-
deviceTypeFields.forEach(fieldName => {
|
|
307
|
-
const select = document.getElementById(`adapter_${fieldName}`);
|
|
308
|
-
if (select) {
|
|
309
|
-
select.addEventListener('change', async () => {
|
|
310
|
-
// Get current adapter name
|
|
311
|
-
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
312
|
-
if (!adapterSelect || !adapterSelect.value) return;
|
|
313
|
-
|
|
314
|
-
// Collect current settings
|
|
315
|
-
const currentSettings = collectAdapterSettings();
|
|
316
|
-
|
|
317
|
-
// Reload schema with new device type selection
|
|
318
|
-
await loadAdapterSchema(adapterSelect.value, currentSettings);
|
|
163
|
+
console.log(`Schema API response:`, data);
|
|
319
164
|
|
|
320
|
-
|
|
321
|
-
populateAdapterSettings(currentSettings);
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Get Bootstrap icon name for a group
|
|
329
|
-
*/
|
|
330
|
-
function getGroupIcon(groupName) {
|
|
331
|
-
const icons = {
|
|
332
|
-
'Camera': 'camera',
|
|
333
|
-
'Mount': 'compass',
|
|
334
|
-
'Filter Wheel': 'circle',
|
|
335
|
-
'Focuser': 'eyeglasses',
|
|
336
|
-
'Connection': 'ethernet',
|
|
337
|
-
'Devices': 'hdd-network',
|
|
338
|
-
'Imaging': 'image',
|
|
339
|
-
'General': 'gear',
|
|
340
|
-
'Advanced': 'sliders'
|
|
341
|
-
};
|
|
342
|
-
return icons[groupName] || 'gear';
|
|
343
|
-
}
|
|
165
|
+
const schema = data.schema || [];
|
|
344
166
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
input.checked = value;
|
|
354
|
-
} else {
|
|
355
|
-
input.value = value;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
}
|
|
167
|
+
// Merge schema with values into enriched field objects
|
|
168
|
+
// Use Alpine.reactive to ensure nested properties are reactive
|
|
169
|
+
const enrichedFields = schema
|
|
170
|
+
.filter(field => !field.readonly) // Skip readonly fields
|
|
171
|
+
.map(field => Alpine.reactive({
|
|
172
|
+
...field, // All schema properties (name, type, options, etc.)
|
|
173
|
+
value: currentSettings[field.name] ?? field.default ?? getDefaultForType(field.type)
|
|
174
|
+
}));
|
|
360
175
|
|
|
361
|
-
|
|
362
|
-
* Collect adapter settings from form
|
|
363
|
-
*/
|
|
364
|
-
function collectAdapterSettings() {
|
|
365
|
-
const settings = {};
|
|
366
|
-
const inputs = document.querySelectorAll('.adapter-setting');
|
|
176
|
+
console.log('Loaded adapter fields:', enrichedFields.map(f => `${f.name}=${f.value} (${f.type})`));
|
|
367
177
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (input.type === 'checkbox') {
|
|
374
|
-
value = input.checked;
|
|
375
|
-
} else {
|
|
376
|
-
value = input.value;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Skip empty values for non-checkbox fields (will use backend defaults)
|
|
380
|
-
if (input.type !== 'checkbox' && (value === '' || value === null || value === undefined)) {
|
|
381
|
-
return;
|
|
178
|
+
// Update Alpine store with unified field objects
|
|
179
|
+
if (typeof Alpine !== 'undefined' && Alpine.store) {
|
|
180
|
+
const store = Alpine.store('citrascope');
|
|
181
|
+
store.adapterFields = enrichedFields;
|
|
382
182
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
} else if (fieldType === 'float') {
|
|
388
|
-
value = parseFloat(value);
|
|
389
|
-
}
|
|
390
|
-
// bool type already handled above
|
|
391
|
-
|
|
392
|
-
settings[fieldName] = value;
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
return settings;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Failed to load adapter schema:', error);
|
|
185
|
+
showConfigError(`Failed to load settings for ${adapterName}`);
|
|
186
|
+
}
|
|
396
187
|
}
|
|
397
188
|
|
|
398
189
|
/**
|
|
@@ -401,70 +192,71 @@ function collectAdapterSettings() {
|
|
|
401
192
|
async function saveConfiguration(event) {
|
|
402
193
|
event.preventDefault();
|
|
403
194
|
|
|
404
|
-
const saveButton = document.getElementById('saveConfigButton');
|
|
405
|
-
const buttonText = document.getElementById('saveButtonText');
|
|
406
|
-
const spinner = document.getElementById('saveButtonSpinner');
|
|
407
|
-
|
|
408
|
-
// Show loading state
|
|
409
|
-
saveButton.disabled = true;
|
|
410
|
-
spinner.style.display = 'inline-block';
|
|
411
|
-
buttonText.textContent = 'Saving...';
|
|
412
|
-
|
|
413
195
|
// Hide previous messages
|
|
414
196
|
hideConfigMessages();
|
|
415
197
|
|
|
198
|
+
// Get config from store (x-model keeps it up to date)
|
|
199
|
+
const store = Alpine.store('citrascope');
|
|
200
|
+
store.isSavingConfig = true;
|
|
201
|
+
const formConfig = store.config;
|
|
202
|
+
|
|
416
203
|
// Determine API host settings based on endpoint selection
|
|
417
|
-
const apiEndpoint = document.getElementById('apiEndpoint').value;
|
|
418
204
|
let host, port, use_ssl;
|
|
419
|
-
|
|
420
|
-
if (apiEndpoint === 'production') {
|
|
205
|
+
if (store.apiEndpoint === 'production') {
|
|
421
206
|
host = PROD_API_HOST;
|
|
422
207
|
port = DEFAULT_API_PORT;
|
|
423
208
|
use_ssl = true;
|
|
424
|
-
} else if (apiEndpoint === 'development') {
|
|
209
|
+
} else if (store.apiEndpoint === 'development') {
|
|
425
210
|
host = DEV_API_HOST;
|
|
426
211
|
port = DEFAULT_API_PORT;
|
|
427
212
|
use_ssl = true;
|
|
428
|
-
} else {
|
|
429
|
-
host =
|
|
430
|
-
port =
|
|
431
|
-
use_ssl =
|
|
213
|
+
} else {
|
|
214
|
+
host = formConfig.host || '';
|
|
215
|
+
port = formConfig.port || DEFAULT_API_PORT;
|
|
216
|
+
use_ssl = formConfig.use_ssl !== undefined ? formConfig.use_ssl : true;
|
|
432
217
|
}
|
|
433
218
|
|
|
219
|
+
// Convert adapterFields back to flat settings object (for current adapter only)
|
|
220
|
+
const adapterSettings = {};
|
|
221
|
+
(store.adapterFields || []).forEach(field => {
|
|
222
|
+
// Include all fields with defined values (including 0, false, empty string)
|
|
223
|
+
if (field.value !== undefined && field.value !== null) {
|
|
224
|
+
adapterSettings[field.name] = field.value;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
434
228
|
const config = {
|
|
435
|
-
personal_access_token:
|
|
436
|
-
telescope_id:
|
|
437
|
-
hardware_adapter:
|
|
438
|
-
adapter_settings:
|
|
439
|
-
log_level:
|
|
440
|
-
keep_images:
|
|
441
|
-
file_logging_enabled:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
log_retention_days: currentConfig.log_retention_days || 30,
|
|
456
|
-
last_autofocus_timestamp: currentConfig.last_autofocus_timestamp, // Preserve timestamp
|
|
229
|
+
personal_access_token: formConfig.personal_access_token || '',
|
|
230
|
+
telescope_id: formConfig.telescope_id || '',
|
|
231
|
+
hardware_adapter: formConfig.hardware_adapter || '',
|
|
232
|
+
adapter_settings: adapterSettings, // Send flat settings for current adapter
|
|
233
|
+
log_level: formConfig.log_level || 'INFO',
|
|
234
|
+
keep_images: formConfig.keep_images || false,
|
|
235
|
+
file_logging_enabled: formConfig.file_logging_enabled !== undefined ? formConfig.file_logging_enabled : true,
|
|
236
|
+
scheduled_autofocus_enabled: formConfig.scheduled_autofocus_enabled || false,
|
|
237
|
+
autofocus_interval_minutes: parseInt(formConfig.autofocus_interval_minutes || 60, 10),
|
|
238
|
+
time_check_interval_minutes: parseInt(formConfig.time_check_interval_minutes || 5, 10),
|
|
239
|
+
time_offset_pause_ms: parseFloat(formConfig.time_offset_pause_ms || 500),
|
|
240
|
+
host,
|
|
241
|
+
port,
|
|
242
|
+
use_ssl,
|
|
243
|
+
// Preserve other settings from Alpine store (the single source of truth)
|
|
244
|
+
max_task_retries: store.config.max_task_retries || 3,
|
|
245
|
+
initial_retry_delay_seconds: store.config.initial_retry_delay_seconds || 30,
|
|
246
|
+
max_retry_delay_seconds: store.config.max_retry_delay_seconds || 300,
|
|
247
|
+
log_retention_days: store.config.log_retention_days || 30,
|
|
248
|
+
last_autofocus_timestamp: store.config.last_autofocus_timestamp,
|
|
457
249
|
};
|
|
458
250
|
|
|
459
251
|
try {
|
|
460
|
-
// Validate filters BEFORE saving main config
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const enabledCount =
|
|
252
|
+
// Validate filters BEFORE saving main config
|
|
253
|
+
const filters = store.filters || {};
|
|
254
|
+
const filterCount = Object.keys(filters).length;
|
|
255
|
+
if (filterCount > 0) {
|
|
256
|
+
const enabledCount = Object.values(filters).filter(f => f.enabled).length;
|
|
465
257
|
if (enabledCount === 0) {
|
|
466
258
|
showConfigMessage('At least one filter must be enabled', 'danger');
|
|
467
|
-
return;
|
|
259
|
+
return;
|
|
468
260
|
}
|
|
469
261
|
}
|
|
470
262
|
|
|
@@ -472,7 +264,9 @@ async function saveConfiguration(event) {
|
|
|
472
264
|
|
|
473
265
|
if (result.ok) {
|
|
474
266
|
// Update saved adapter to match newly saved config
|
|
475
|
-
|
|
267
|
+
if (typeof Alpine !== 'undefined' && Alpine.store) {
|
|
268
|
+
Alpine.store('citrascope').savedAdapter = config.hardware_adapter;
|
|
269
|
+
}
|
|
476
270
|
|
|
477
271
|
// After config saved successfully, save any modified filter focus positions
|
|
478
272
|
const filterResults = await saveModifiedFilters();
|
|
@@ -499,9 +293,7 @@ async function saveConfiguration(event) {
|
|
|
499
293
|
showConfigError('Failed to save configuration: ' + error.message);
|
|
500
294
|
} finally {
|
|
501
295
|
// Reset button state
|
|
502
|
-
|
|
503
|
-
spinner.style.display = 'none';
|
|
504
|
-
buttonText.textContent = 'Save Configuration';
|
|
296
|
+
store.isSavingConfig = false;
|
|
505
297
|
}
|
|
506
298
|
}
|
|
507
299
|
|
|
@@ -604,92 +396,53 @@ export function showConfigSection() {
|
|
|
604
396
|
* Load and display filter configuration
|
|
605
397
|
*/
|
|
606
398
|
async function loadFilterConfig() {
|
|
607
|
-
const
|
|
608
|
-
const changeMessage = document.getElementById('filterAdapterChangeMessage');
|
|
609
|
-
const tableContainer = document.getElementById('filterTableContainer');
|
|
399
|
+
const store = Alpine.store('citrascope');
|
|
610
400
|
|
|
611
401
|
// Check if selected adapter matches saved adapter
|
|
612
|
-
const
|
|
613
|
-
const selectedAdapter = adapterSelect ? adapterSelect.value : null;
|
|
402
|
+
const selectedAdapter = store.config.hardware_adapter;
|
|
614
403
|
|
|
615
|
-
if (selectedAdapter && savedAdapter && selectedAdapter !== savedAdapter) {
|
|
404
|
+
if (selectedAdapter && store.savedAdapter && selectedAdapter !== store.savedAdapter) {
|
|
616
405
|
// Adapter has changed but not saved yet - show message and hide table
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (tableContainer) tableContainer.style.display = 'none';
|
|
406
|
+
store.filterConfigVisible = true;
|
|
407
|
+
store.filterAdapterChangeMessageVisible = true;
|
|
620
408
|
return;
|
|
621
409
|
}
|
|
622
410
|
|
|
623
411
|
// Hide message and show table when adapters match
|
|
624
|
-
|
|
625
|
-
if (tableContainer) tableContainer.style.display = 'block';
|
|
412
|
+
store.filterAdapterChangeMessageVisible = false;
|
|
626
413
|
|
|
627
414
|
try {
|
|
628
415
|
const response = await fetch('/api/adapter/filters');
|
|
629
416
|
|
|
630
417
|
if (response.status === 404 || response.status === 503) {
|
|
631
|
-
|
|
632
|
-
if (filterSection) filterSection.style.display = 'none';
|
|
418
|
+
store.filterConfigVisible = false;
|
|
633
419
|
return;
|
|
634
420
|
}
|
|
635
421
|
|
|
636
422
|
const data = await response.json();
|
|
637
423
|
|
|
638
424
|
if (response.ok && data.filters) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (filterIds.length === 0) {
|
|
655
|
-
if (noFiltersMsg) noFiltersMsg.style.display = 'block';
|
|
656
|
-
} else {
|
|
657
|
-
if (noFiltersMsg) noFiltersMsg.style.display = 'none';
|
|
658
|
-
|
|
659
|
-
filterIds.forEach(filterId => {
|
|
660
|
-
const filter = filters[filterId];
|
|
661
|
-
const isEnabled = filter.enabled !== undefined ? filter.enabled : true;
|
|
662
|
-
const filterColor = getFilterColor(filter.name);
|
|
663
|
-
const row = document.createElement('tr');
|
|
664
|
-
row.innerHTML = `
|
|
665
|
-
<td>
|
|
666
|
-
<input type="checkbox"
|
|
667
|
-
class="form-check-input filter-enabled-checkbox"
|
|
668
|
-
data-filter-id="${filterId}"
|
|
669
|
-
${isEnabled ? 'checked' : ''}>
|
|
670
|
-
</td>
|
|
671
|
-
<td>
|
|
672
|
-
<span class="badge" style="background-color: ${filterColor}; color: white;">${filter.name}</span>
|
|
673
|
-
</td>
|
|
674
|
-
<td>
|
|
675
|
-
<input type="number"
|
|
676
|
-
class="form-control form-control-sm filter-focus-input"
|
|
677
|
-
data-filter-id="${filterId}"
|
|
678
|
-
value="${filter.focus_position}"
|
|
679
|
-
min="0"
|
|
680
|
-
step="1">
|
|
681
|
-
</td>
|
|
682
|
-
`;
|
|
683
|
-
tbody.appendChild(row);
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
}
|
|
425
|
+
store.filterConfigVisible = true;
|
|
426
|
+
|
|
427
|
+
// Update enabled filters display on dashboard (Alpine store)
|
|
428
|
+
updateStoreEnabledFilters(data.filters);
|
|
429
|
+
|
|
430
|
+
// Add color to each filter and update store
|
|
431
|
+
const filtersWithColor = {};
|
|
432
|
+
Object.entries(data.filters).forEach(([id, filter]) => {
|
|
433
|
+
filtersWithColor[id] = {
|
|
434
|
+
...filter,
|
|
435
|
+
color: getFilterColor(filter.name),
|
|
436
|
+
enabled: filter.enabled !== undefined ? filter.enabled : true
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
store.filters = filtersWithColor;
|
|
687
440
|
} else {
|
|
688
|
-
|
|
441
|
+
store.filterConfigVisible = false;
|
|
689
442
|
}
|
|
690
443
|
} catch (error) {
|
|
691
444
|
console.error('Error loading filter config:', error);
|
|
692
|
-
|
|
445
|
+
store.filterConfigVisible = false;
|
|
693
446
|
}
|
|
694
447
|
}
|
|
695
448
|
|
|
@@ -698,41 +451,33 @@ async function loadFilterConfig() {
|
|
|
698
451
|
* Returns: Object with { success: number, failed: number }
|
|
699
452
|
*/
|
|
700
453
|
async function saveModifiedFilters() {
|
|
701
|
-
const
|
|
702
|
-
|
|
454
|
+
const store = Alpine.store('citrascope');
|
|
455
|
+
const filters = store.filters || {};
|
|
456
|
+
const filterIds = Object.keys(filters);
|
|
703
457
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
458
|
+
if (filterIds.length === 0) return { success: 0, failed: 0 };
|
|
459
|
+
|
|
460
|
+
// Validate at least one filter is enabled
|
|
461
|
+
const enabledCount = Object.values(filters).filter(f => f.enabled).length;
|
|
707
462
|
if (enabledCount === 0) {
|
|
708
463
|
showConfigMessage('At least one filter must be enabled', 'danger');
|
|
709
|
-
return { success: 0, failed:
|
|
464
|
+
return { success: 0, failed: filterIds.length };
|
|
710
465
|
}
|
|
711
466
|
|
|
712
|
-
// Collect all filter updates
|
|
467
|
+
// Collect all filter updates from store
|
|
713
468
|
const filterUpdates = [];
|
|
714
|
-
for (const
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (Number.isNaN(focusPosition) || focusPosition < 0) {
|
|
719
|
-
continue; // Skip invalid entries
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Get enabled state from corresponding checkbox
|
|
723
|
-
const checkbox = document.querySelector(`.filter-enabled-checkbox[data-filter-id="${filterId}"]`);
|
|
724
|
-
const enabled = checkbox ? checkbox.checked : true;
|
|
469
|
+
for (const [filterId, filter] of Object.entries(filters)) {
|
|
470
|
+
const focusPosition = parseInt(filter.focus_position);
|
|
471
|
+
if (Number.isNaN(focusPosition) || focusPosition < 0) continue;
|
|
725
472
|
|
|
726
473
|
filterUpdates.push({
|
|
727
474
|
filter_id: filterId,
|
|
728
475
|
focus_position: focusPosition,
|
|
729
|
-
enabled: enabled
|
|
476
|
+
enabled: filter.enabled !== undefined ? filter.enabled : true
|
|
730
477
|
});
|
|
731
478
|
}
|
|
732
479
|
|
|
733
|
-
if (filterUpdates.length === 0) {
|
|
734
|
-
return { success: 0, failed: 0 };
|
|
735
|
-
}
|
|
480
|
+
if (filterUpdates.length === 0) return { success: 0, failed: 0 };
|
|
736
481
|
|
|
737
482
|
// Send single batch update
|
|
738
483
|
try {
|
|
@@ -783,14 +528,8 @@ async function saveModifiedFilters() {
|
|
|
783
528
|
* Trigger or cancel autofocus routine
|
|
784
529
|
*/
|
|
785
530
|
async function triggerAutofocus() {
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
const buttonSpinner = document.getElementById('autofocusButtonSpinner');
|
|
789
|
-
|
|
790
|
-
if (!button || !buttonText || !buttonSpinner) return;
|
|
791
|
-
|
|
792
|
-
// Check if this is a cancel action
|
|
793
|
-
const isCancel = button.dataset.action === 'cancel';
|
|
531
|
+
const store = Alpine.store('citrascope');
|
|
532
|
+
const isCancel = store.status?.autofocus_requested;
|
|
794
533
|
|
|
795
534
|
if (isCancel) {
|
|
796
535
|
// Cancel autofocus
|
|
@@ -802,7 +541,6 @@ async function triggerAutofocus() {
|
|
|
802
541
|
|
|
803
542
|
if (response.ok && data.success) {
|
|
804
543
|
showToast('Autofocus cancelled', 'info');
|
|
805
|
-
updateAutofocusButton(false);
|
|
806
544
|
} else {
|
|
807
545
|
showToast('Nothing to cancel', 'warning');
|
|
808
546
|
}
|
|
@@ -814,8 +552,7 @@ async function triggerAutofocus() {
|
|
|
814
552
|
}
|
|
815
553
|
|
|
816
554
|
// Request autofocus
|
|
817
|
-
|
|
818
|
-
buttonSpinner.style.display = 'inline-block';
|
|
555
|
+
store.isAutofocusing = true;
|
|
819
556
|
|
|
820
557
|
try {
|
|
821
558
|
const response = await fetch('/api/adapter/autofocus', {
|
|
@@ -826,7 +563,6 @@ async function triggerAutofocus() {
|
|
|
826
563
|
|
|
827
564
|
if (response.ok) {
|
|
828
565
|
showToast('Autofocus queued', 'success');
|
|
829
|
-
updateAutofocusButton(true);
|
|
830
566
|
} else {
|
|
831
567
|
showToast(data.error || 'Autofocus request failed', 'error');
|
|
832
568
|
}
|
|
@@ -834,30 +570,7 @@ async function triggerAutofocus() {
|
|
|
834
570
|
console.error('Error triggering autofocus:', error);
|
|
835
571
|
showToast('Failed to trigger autofocus', 'error');
|
|
836
572
|
} finally {
|
|
837
|
-
|
|
838
|
-
buttonSpinner.style.display = 'none';
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* Update autofocus button state based on whether autofocus is queued
|
|
844
|
-
*/
|
|
845
|
-
function updateAutofocusButton(isQueued) {
|
|
846
|
-
const button = document.getElementById('runAutofocusButton');
|
|
847
|
-
const buttonText = document.getElementById('autofocusButtonText');
|
|
848
|
-
|
|
849
|
-
if (!button || !buttonText) return;
|
|
850
|
-
|
|
851
|
-
if (isQueued) {
|
|
852
|
-
buttonText.textContent = 'Cancel Autofocus';
|
|
853
|
-
button.dataset.action = 'cancel';
|
|
854
|
-
button.classList.remove('btn-outline-primary');
|
|
855
|
-
button.classList.add('btn-outline-warning');
|
|
856
|
-
} else {
|
|
857
|
-
buttonText.textContent = 'Run Autofocus';
|
|
858
|
-
button.dataset.action = 'request';
|
|
859
|
-
button.classList.remove('btn-outline-warning');
|
|
860
|
-
button.classList.add('btn-outline-primary');
|
|
573
|
+
store.isAutofocusing = false;
|
|
861
574
|
}
|
|
862
575
|
}
|
|
863
576
|
|
|
@@ -906,36 +619,27 @@ export async function initFilterConfig() {
|
|
|
906
619
|
}
|
|
907
620
|
|
|
908
621
|
/**
|
|
909
|
-
*
|
|
910
|
-
* @param {Object} filters - Filter configuration object
|
|
622
|
+
* Setup autofocus button event listener (call once during init)
|
|
911
623
|
*/
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const enabledFilters = Object.values(filters)
|
|
917
|
-
.filter(filter => filter.enabled !== false)
|
|
918
|
-
.map(filter => filter.name);
|
|
919
|
-
|
|
920
|
-
if (enabledFilters.length > 0) {
|
|
921
|
-
filtersEl.innerHTML = enabledFilters.map(filterName => {
|
|
922
|
-
const color = getFilterColor(filterName);
|
|
923
|
-
return `<span class="badge me-1" style="background-color: ${color}; color: white;">${filterName}</span>`;
|
|
924
|
-
}).join('');
|
|
925
|
-
} else {
|
|
926
|
-
filtersEl.textContent = '-';
|
|
927
|
-
}
|
|
624
|
+
// Autofocus button now uses Alpine @click directive in template
|
|
625
|
+
// Expose triggerAutofocus for template access
|
|
626
|
+
export function setupAutofocusButton() {
|
|
627
|
+
window.triggerAutofocus = triggerAutofocus;
|
|
928
628
|
}
|
|
929
629
|
|
|
930
630
|
/**
|
|
931
|
-
*
|
|
631
|
+
* Reload adapter schema (called from Alpine components when device type changes)
|
|
932
632
|
*/
|
|
933
|
-
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
633
|
+
async function reloadAdapterSchema() {
|
|
634
|
+
const store = Alpine.store('citrascope');
|
|
635
|
+
const adapter = store.config.hardware_adapter;
|
|
636
|
+
if (!adapter) return;
|
|
637
|
+
|
|
638
|
+
// Convert current adapterFields back to flat settings object
|
|
639
|
+
const currentSettings = {};
|
|
640
|
+
(store.adapterFields || []).forEach(field => {
|
|
641
|
+
currentSettings[field.name] = field.value;
|
|
642
|
+
});
|
|
939
643
|
|
|
940
|
-
|
|
941
|
-
|
|
644
|
+
await loadAdapterSchema(adapter, currentSettings);
|
|
645
|
+
}
|