citrascope 0.7.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/api/abstract_api_client.py +14 -0
- citrascope/api/citra_api_client.py +41 -0
- citrascope/citra_scope_daemon.py +75 -0
- citrascope/hardware/abstract_astro_hardware_adapter.py +97 -2
- citrascope/hardware/adapter_registry.py +15 -3
- citrascope/hardware/devices/__init__.py +17 -0
- citrascope/hardware/devices/abstract_hardware_device.py +79 -0
- citrascope/hardware/devices/camera/__init__.py +13 -0
- citrascope/hardware/devices/camera/abstract_camera.py +114 -0
- citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
- citrascope/hardware/devices/camera/usb_camera.py +407 -0
- citrascope/hardware/devices/camera/ximea_camera.py +756 -0
- citrascope/hardware/devices/device_registry.py +273 -0
- citrascope/hardware/devices/filter_wheel/__init__.py +7 -0
- citrascope/hardware/devices/filter_wheel/abstract_filter_wheel.py +73 -0
- citrascope/hardware/devices/focuser/__init__.py +7 -0
- citrascope/hardware/devices/focuser/abstract_focuser.py +78 -0
- citrascope/hardware/devices/mount/__init__.py +7 -0
- citrascope/hardware/devices/mount/abstract_mount.py +115 -0
- citrascope/hardware/direct_hardware_adapter.py +805 -0
- citrascope/hardware/dummy_adapter.py +202 -0
- citrascope/hardware/filter_sync.py +94 -0
- citrascope/hardware/indi_adapter.py +6 -2
- citrascope/hardware/kstars_dbus_adapter.py +46 -37
- citrascope/hardware/nina_adv_http_adapter.py +13 -11
- citrascope/settings/citrascope_settings.py +6 -0
- citrascope/tasks/runner.py +2 -0
- citrascope/tasks/scope/static_telescope_task.py +17 -12
- citrascope/tasks/task.py +3 -0
- citrascope/time/__init__.py +14 -0
- citrascope/time/time_health.py +103 -0
- citrascope/time/time_monitor.py +186 -0
- citrascope/time/time_sources.py +261 -0
- citrascope/web/app.py +260 -60
- citrascope/web/static/app.js +121 -731
- citrascope/web/static/components.js +136 -0
- citrascope/web/static/config.js +259 -420
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/formatters.js +129 -0
- citrascope/web/static/store-init.js +204 -0
- citrascope/web/static/style.css +44 -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 +109 -377
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/METADATA +18 -1
- citrascope-0.9.0.dist-info/RECORD +69 -0
- citrascope-0.7.0.dist-info/RECORD +0 -41
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/WHEEL +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/licenses/LICENSE +0 -0
citrascope/web/static/config.js
CHANGED
|
@@ -1,54 +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
|
+
import { getFilterColor } from './filters.js';
|
|
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
|
+
}
|
|
4
14
|
|
|
5
15
|
// API Host constants - must match backend constants in app.py
|
|
6
16
|
const PROD_API_HOST = 'api.citra.space';
|
|
7
17
|
const DEV_API_HOST = 'dev.api.citra.space';
|
|
8
18
|
const DEFAULT_API_PORT = 443;
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
13
40
|
|
|
14
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
|
+
|
|
15
47
|
// Populate hardware adapter dropdown
|
|
16
48
|
await loadAdapterOptions();
|
|
17
49
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
adapterSelect.addEventListener('change', async function(e) {
|
|
22
|
-
const adapter = e.target.value;
|
|
23
|
-
if (adapter) {
|
|
24
|
-
await loadAdapterSchema(adapter);
|
|
25
|
-
await loadFilterConfig();
|
|
26
|
-
} else {
|
|
27
|
-
document.getElementById('adapter-settings-container').innerHTML = '';
|
|
28
|
-
const filterSection = document.getElementById('filterConfigSection');
|
|
29
|
-
if (filterSection) filterSection.style.display = 'none';
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// API endpoint selection change
|
|
35
|
-
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
36
|
-
if (apiEndpointSelect) {
|
|
37
|
-
apiEndpointSelect.addEventListener('change', function(e) {
|
|
38
|
-
const customContainer = document.getElementById('customHostContainer');
|
|
39
|
-
if (e.target.value === 'custom') {
|
|
40
|
-
customContainer.style.display = 'block';
|
|
41
|
-
} else {
|
|
42
|
-
customContainer.style.display = 'none';
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Config form submission
|
|
48
|
-
const configForm = document.getElementById('configForm');
|
|
49
|
-
if (configForm) {
|
|
50
|
-
configForm.addEventListener('submit', saveConfiguration);
|
|
51
|
-
}
|
|
50
|
+
// Config form submission handled by Alpine @submit.prevent in template
|
|
51
|
+
// Expose saveConfiguration for template access
|
|
52
|
+
window.saveConfiguration = saveConfiguration;
|
|
52
53
|
|
|
53
54
|
// Load initial config
|
|
54
55
|
await loadConfiguration();
|
|
@@ -82,21 +83,13 @@ async function checkConfigStatus() {
|
|
|
82
83
|
async function loadAdapterOptions() {
|
|
83
84
|
try {
|
|
84
85
|
const data = await getHardwareAdapters();
|
|
85
|
-
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
86
|
-
|
|
87
|
-
if (adapterSelect && data.adapters) {
|
|
88
|
-
// Clear existing options except the first placeholder
|
|
89
|
-
while (adapterSelect.options.length > 1) {
|
|
90
|
-
adapterSelect.remove(1);
|
|
91
|
-
}
|
|
92
86
|
|
|
93
|
-
|
|
94
|
-
data.adapters.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
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;
|
|
100
93
|
}
|
|
101
94
|
} catch (error) {
|
|
102
95
|
console.error('Failed to load hardware adapters:', error);
|
|
@@ -109,74 +102,34 @@ async function loadAdapterOptions() {
|
|
|
109
102
|
async function loadConfiguration() {
|
|
110
103
|
try {
|
|
111
104
|
const config = await getConfig();
|
|
112
|
-
currentConfig = config; // Save for reuse when saving
|
|
113
|
-
savedAdapter = config.hardware_adapter; // Track saved adapter
|
|
114
105
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
configPathElement.textContent = config.config_file_path;
|
|
119
|
-
}
|
|
106
|
+
// Sync to Alpine store - x-model handles form population
|
|
107
|
+
if (typeof Alpine !== 'undefined' && Alpine.store) {
|
|
108
|
+
const store = Alpine.store('citrascope');
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
128
119
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Display images directory path
|
|
132
|
-
const imagesDirElement = document.getElementById('imagesDirPath');
|
|
133
|
-
if (imagesDirElement && config.images_dir_path) {
|
|
134
|
-
imagesDirElement.textContent = config.images_dir_path;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// API endpoint selector
|
|
138
|
-
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
139
|
-
const customHostContainer = document.getElementById('customHostContainer');
|
|
140
|
-
const customHost = document.getElementById('customHost');
|
|
141
|
-
const customPort = document.getElementById('customPort');
|
|
142
|
-
const customUseSsl = document.getElementById('customUseSsl');
|
|
143
|
-
|
|
144
|
-
if (config.host === PROD_API_HOST) {
|
|
145
|
-
apiEndpointSelect.value = 'production';
|
|
146
|
-
customHostContainer.style.display = 'none';
|
|
147
|
-
} else if (config.host === DEV_API_HOST) {
|
|
148
|
-
apiEndpointSelect.value = 'development';
|
|
149
|
-
customHostContainer.style.display = 'none';
|
|
150
|
-
} else {
|
|
151
|
-
apiEndpointSelect.value = 'custom';
|
|
152
|
-
customHostContainer.style.display = 'block';
|
|
153
|
-
customHost.value = config.host || '';
|
|
154
|
-
customPort.value = config.port || DEFAULT_API_PORT;
|
|
155
|
-
customUseSsl.checked = config.use_ssl !== undefined ? config.use_ssl : true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Core fields
|
|
159
|
-
document.getElementById('personal_access_token').value = config.personal_access_token || '';
|
|
160
|
-
document.getElementById('telescopeId').value = config.telescope_id || '';
|
|
161
|
-
document.getElementById('hardwareAdapterSelect').value = config.hardware_adapter || '';
|
|
162
|
-
document.getElementById('logLevel').value = config.log_level || 'INFO';
|
|
163
|
-
document.getElementById('keep_images').checked = config.keep_images || false;
|
|
164
|
-
document.getElementById('file_logging_enabled').checked = config.file_logging_enabled !== undefined ? config.file_logging_enabled : true;
|
|
165
|
-
|
|
166
|
-
// Load autofocus settings (top-level)
|
|
167
|
-
const scheduledAutofocusEnabled = document.getElementById('scheduled_autofocus_enabled');
|
|
168
|
-
const autofocusInterval = document.getElementById('autofocus_interval_minutes');
|
|
169
|
-
if (scheduledAutofocusEnabled) {
|
|
170
|
-
scheduledAutofocusEnabled.checked = config.scheduled_autofocus_enabled || false;
|
|
171
|
-
}
|
|
172
|
-
if (autofocusInterval) {
|
|
173
|
-
autofocusInterval.value = config.autofocus_interval_minutes || 60;
|
|
174
|
-
}
|
|
175
120
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
133
|
}
|
|
181
134
|
} catch (error) {
|
|
182
135
|
console.error('Failed to load config:', error);
|
|
@@ -184,134 +137,53 @@ async function loadConfiguration() {
|
|
|
184
137
|
}
|
|
185
138
|
|
|
186
139
|
/**
|
|
187
|
-
*
|
|
140
|
+
* Get default value for a field type
|
|
188
141
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
renderAdapterSettings(currentAdapterSchema);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
console.error('Failed to load adapter schema:', error);
|
|
196
|
-
showConfigError(`Failed to load settings for ${adapterName}`);
|
|
197
|
-
}
|
|
142
|
+
function getDefaultForType(type) {
|
|
143
|
+
if (type === 'bool') return false;
|
|
144
|
+
if (type === 'int' || type === 'float') return 0;
|
|
145
|
+
return '';
|
|
198
146
|
}
|
|
199
147
|
|
|
200
148
|
/**
|
|
201
|
-
*
|
|
149
|
+
* Load adapter schema and merge with current values
|
|
202
150
|
*/
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
let html = '<h5 class="mb-3">Adapter Settings</h5><div class="row g-3 mb-4">';
|
|
212
|
-
|
|
213
|
-
schema.forEach(field => {
|
|
214
|
-
// Skip readonly fields (handled elsewhere in UI)
|
|
215
|
-
if (field.readonly) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const isRequired = field.required ? '<span class="text-danger">*</span>' : '';
|
|
220
|
-
const placeholder = field.placeholder || '';
|
|
221
|
-
const description = field.description || '';
|
|
222
|
-
const displayName = field.friendly_name || field.name;
|
|
223
|
-
|
|
224
|
-
html += '<div class="col-12 col-md-6">';
|
|
225
|
-
html += `<label for="adapter_${field.name}" class="form-label">${displayName} ${isRequired}</label>`;
|
|
226
|
-
|
|
227
|
-
if (field.type === 'bool') {
|
|
228
|
-
html += `<div class="form-check mt-2">`;
|
|
229
|
-
html += `<input class="form-check-input adapter-setting" type="checkbox" id="adapter_${field.name}" data-field="${field.name}" data-type="${field.type}">`;
|
|
230
|
-
html += `<label class="form-check-label" for="adapter_${field.name}">${description}</label>`;
|
|
231
|
-
html += `</div>`;
|
|
232
|
-
} else if (field.options && field.options.length > 0) {
|
|
233
|
-
const displayName = field.friendly_name || field.name;
|
|
234
|
-
html += `<select id="adapter_${field.name}" class="form-select adapter-setting" data-field="${field.name}" data-type="${field.type}" ${field.required ? 'required' : ''}>`;
|
|
235
|
-
html += `<option value="">-- Select ${displayName} --</option>`;
|
|
236
|
-
field.options.forEach(opt => {
|
|
237
|
-
html += `<option value="${opt}">${opt}</option>`;
|
|
238
|
-
});
|
|
239
|
-
html += `</select>`;
|
|
240
|
-
} else if (field.type === 'int' || field.type === 'float') {
|
|
241
|
-
const min = field.min !== undefined ? `min="${field.min}"` : '';
|
|
242
|
-
const max = field.max !== undefined ? `max="${field.max}"` : '';
|
|
243
|
-
html += `<input type="number" id="adapter_${field.name}" class="form-control adapter-setting" `;
|
|
244
|
-
html += `data-field="${field.name}" data-type="${field.type}" `;
|
|
245
|
-
html += `placeholder="${placeholder}" ${min} ${max} ${field.required ? 'required' : ''}>`;
|
|
246
|
-
} else {
|
|
247
|
-
// Default to text input
|
|
248
|
-
const pattern = field.pattern ? `pattern="${field.pattern}"` : '';
|
|
249
|
-
html += `<input type="text" id="adapter_${field.name}" class="form-control adapter-setting" `;
|
|
250
|
-
html += `data-field="${field.name}" data-type="${field.type}" `;
|
|
251
|
-
html += `placeholder="${placeholder}" ${pattern} ${field.required ? 'required' : ''}>`;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (description && field.type !== 'bool') {
|
|
255
|
-
html += `<small class="text-muted">${description}</small>`;
|
|
256
|
-
}
|
|
257
|
-
html += '</div>';
|
|
258
|
-
});
|
|
151
|
+
async function loadAdapterSchema(adapterName, currentSettings = {}) {
|
|
152
|
+
try {
|
|
153
|
+
// Pass current adapter settings for dynamic schema generation
|
|
154
|
+
const settingsParam = Object.keys(currentSettings).length > 0
|
|
155
|
+
? `?current_settings=${encodeURIComponent(JSON.stringify(currentSettings))}`
|
|
156
|
+
: '';
|
|
259
157
|
|
|
260
|
-
|
|
261
|
-
container.innerHTML = html;
|
|
262
|
-
}
|
|
158
|
+
console.log(`Loading schema for ${adapterName} with settings:`, currentSettings);
|
|
263
159
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*/
|
|
267
|
-
function populateAdapterSettings(adapterSettings) {
|
|
268
|
-
Object.entries(adapterSettings).forEach(([key, value]) => {
|
|
269
|
-
const input = document.getElementById(`adapter_${key}`);
|
|
270
|
-
if (input) {
|
|
271
|
-
if (input.type === 'checkbox') {
|
|
272
|
-
input.checked = value;
|
|
273
|
-
} else {
|
|
274
|
-
input.value = value;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
}
|
|
160
|
+
const response = await fetch(`/api/hardware-adapters/${adapterName}/schema${settingsParam}`);
|
|
161
|
+
const data = await response.json();
|
|
279
162
|
|
|
280
|
-
|
|
281
|
-
* Collect adapter settings from form
|
|
282
|
-
*/
|
|
283
|
-
function collectAdapterSettings() {
|
|
284
|
-
const settings = {};
|
|
285
|
-
const inputs = document.querySelectorAll('.adapter-setting');
|
|
163
|
+
console.log(`Schema API response:`, data);
|
|
286
164
|
|
|
287
|
-
|
|
288
|
-
const fieldName = input.dataset.field;
|
|
289
|
-
const fieldType = input.dataset.type;
|
|
290
|
-
let value;
|
|
165
|
+
const schema = data.schema || [];
|
|
291
166
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}));
|
|
297
175
|
|
|
298
|
-
|
|
299
|
-
if (input.type !== 'checkbox' && (value === '' || value === null || value === undefined)) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
176
|
+
console.log('Loaded adapter fields:', enrichedFields.map(f => `${f.name}=${f.value} (${f.type})`));
|
|
302
177
|
|
|
303
|
-
//
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
value = parseFloat(value);
|
|
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;
|
|
308
182
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
settings
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return settings;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Failed to load adapter schema:', error);
|
|
185
|
+
showConfigError(`Failed to load settings for ${adapterName}`);
|
|
186
|
+
}
|
|
315
187
|
}
|
|
316
188
|
|
|
317
189
|
/**
|
|
@@ -320,68 +192,71 @@ function collectAdapterSettings() {
|
|
|
320
192
|
async function saveConfiguration(event) {
|
|
321
193
|
event.preventDefault();
|
|
322
194
|
|
|
323
|
-
const saveButton = document.getElementById('saveConfigButton');
|
|
324
|
-
const buttonText = document.getElementById('saveButtonText');
|
|
325
|
-
const spinner = document.getElementById('saveButtonSpinner');
|
|
326
|
-
|
|
327
|
-
// Show loading state
|
|
328
|
-
saveButton.disabled = true;
|
|
329
|
-
spinner.style.display = 'inline-block';
|
|
330
|
-
buttonText.textContent = 'Saving...';
|
|
331
|
-
|
|
332
195
|
// Hide previous messages
|
|
333
196
|
hideConfigMessages();
|
|
334
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
|
+
|
|
335
203
|
// Determine API host settings based on endpoint selection
|
|
336
|
-
const apiEndpoint = document.getElementById('apiEndpoint').value;
|
|
337
204
|
let host, port, use_ssl;
|
|
338
|
-
|
|
339
|
-
if (apiEndpoint === 'production') {
|
|
205
|
+
if (store.apiEndpoint === 'production') {
|
|
340
206
|
host = PROD_API_HOST;
|
|
341
207
|
port = DEFAULT_API_PORT;
|
|
342
208
|
use_ssl = true;
|
|
343
|
-
} else if (apiEndpoint === 'development') {
|
|
209
|
+
} else if (store.apiEndpoint === 'development') {
|
|
344
210
|
host = DEV_API_HOST;
|
|
345
211
|
port = DEFAULT_API_PORT;
|
|
346
212
|
use_ssl = true;
|
|
347
|
-
} else {
|
|
348
|
-
host =
|
|
349
|
-
port =
|
|
350
|
-
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;
|
|
351
217
|
}
|
|
352
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
|
+
|
|
353
228
|
const config = {
|
|
354
|
-
personal_access_token:
|
|
355
|
-
telescope_id:
|
|
356
|
-
hardware_adapter:
|
|
357
|
-
adapter_settings:
|
|
358
|
-
log_level:
|
|
359
|
-
keep_images:
|
|
360
|
-
file_logging_enabled:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
host
|
|
366
|
-
port
|
|
367
|
-
use_ssl
|
|
368
|
-
// Preserve other settings from
|
|
369
|
-
max_task_retries:
|
|
370
|
-
initial_retry_delay_seconds:
|
|
371
|
-
max_retry_delay_seconds:
|
|
372
|
-
log_retention_days:
|
|
373
|
-
last_autofocus_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,
|
|
374
249
|
};
|
|
375
250
|
|
|
376
251
|
try {
|
|
377
|
-
// Validate filters BEFORE saving main config
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
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;
|
|
382
257
|
if (enabledCount === 0) {
|
|
383
258
|
showConfigMessage('At least one filter must be enabled', 'danger');
|
|
384
|
-
return;
|
|
259
|
+
return;
|
|
385
260
|
}
|
|
386
261
|
}
|
|
387
262
|
|
|
@@ -389,7 +264,9 @@ async function saveConfiguration(event) {
|
|
|
389
264
|
|
|
390
265
|
if (result.ok) {
|
|
391
266
|
// Update saved adapter to match newly saved config
|
|
392
|
-
|
|
267
|
+
if (typeof Alpine !== 'undefined' && Alpine.store) {
|
|
268
|
+
Alpine.store('citrascope').savedAdapter = config.hardware_adapter;
|
|
269
|
+
}
|
|
393
270
|
|
|
394
271
|
// After config saved successfully, save any modified filter focus positions
|
|
395
272
|
const filterResults = await saveModifiedFilters();
|
|
@@ -416,9 +293,7 @@ async function saveConfiguration(event) {
|
|
|
416
293
|
showConfigError('Failed to save configuration: ' + error.message);
|
|
417
294
|
} finally {
|
|
418
295
|
// Reset button state
|
|
419
|
-
|
|
420
|
-
spinner.style.display = 'none';
|
|
421
|
-
buttonText.textContent = 'Save Configuration';
|
|
296
|
+
store.isSavingConfig = false;
|
|
422
297
|
}
|
|
423
298
|
}
|
|
424
299
|
|
|
@@ -428,7 +303,7 @@ async function saveConfiguration(event) {
|
|
|
428
303
|
* @param {string} type - 'danger' for errors, 'success' for success messages
|
|
429
304
|
* @param {boolean} autohide - Whether to auto-hide the toast
|
|
430
305
|
*/
|
|
431
|
-
function createToast(message, type = 'danger', autohide = false) {
|
|
306
|
+
export function createToast(message, type = 'danger', autohide = false) {
|
|
432
307
|
const toastContainer = document.getElementById('toastContainer');
|
|
433
308
|
if (!toastContainer) {
|
|
434
309
|
console.error('Toast container not found');
|
|
@@ -521,86 +396,53 @@ export function showConfigSection() {
|
|
|
521
396
|
* Load and display filter configuration
|
|
522
397
|
*/
|
|
523
398
|
async function loadFilterConfig() {
|
|
524
|
-
const
|
|
525
|
-
const changeMessage = document.getElementById('filterAdapterChangeMessage');
|
|
526
|
-
const tableContainer = document.getElementById('filterTableContainer');
|
|
399
|
+
const store = Alpine.store('citrascope');
|
|
527
400
|
|
|
528
401
|
// Check if selected adapter matches saved adapter
|
|
529
|
-
const
|
|
530
|
-
const selectedAdapter = adapterSelect ? adapterSelect.value : null;
|
|
402
|
+
const selectedAdapter = store.config.hardware_adapter;
|
|
531
403
|
|
|
532
|
-
if (selectedAdapter && savedAdapter && selectedAdapter !== savedAdapter) {
|
|
404
|
+
if (selectedAdapter && store.savedAdapter && selectedAdapter !== store.savedAdapter) {
|
|
533
405
|
// Adapter has changed but not saved yet - show message and hide table
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (tableContainer) tableContainer.style.display = 'none';
|
|
406
|
+
store.filterConfigVisible = true;
|
|
407
|
+
store.filterAdapterChangeMessageVisible = true;
|
|
537
408
|
return;
|
|
538
409
|
}
|
|
539
410
|
|
|
540
411
|
// Hide message and show table when adapters match
|
|
541
|
-
|
|
542
|
-
if (tableContainer) tableContainer.style.display = 'block';
|
|
412
|
+
store.filterAdapterChangeMessageVisible = false;
|
|
543
413
|
|
|
544
414
|
try {
|
|
545
415
|
const response = await fetch('/api/adapter/filters');
|
|
546
416
|
|
|
547
417
|
if (response.status === 404 || response.status === 503) {
|
|
548
|
-
|
|
549
|
-
if (filterSection) filterSection.style.display = 'none';
|
|
418
|
+
store.filterConfigVisible = false;
|
|
550
419
|
return;
|
|
551
420
|
}
|
|
552
421
|
|
|
553
422
|
const data = await response.json();
|
|
554
423
|
|
|
555
424
|
if (response.ok && data.filters) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (noFiltersMsg) noFiltersMsg.style.display = 'none';
|
|
572
|
-
|
|
573
|
-
filterIds.forEach(filterId => {
|
|
574
|
-
const filter = filters[filterId];
|
|
575
|
-
const isEnabled = filter.enabled !== undefined ? filter.enabled : true;
|
|
576
|
-
const row = document.createElement('tr');
|
|
577
|
-
row.innerHTML = `
|
|
578
|
-
<td>
|
|
579
|
-
<input type="checkbox"
|
|
580
|
-
class="form-check-input filter-enabled-checkbox"
|
|
581
|
-
data-filter-id="${filterId}"
|
|
582
|
-
${isEnabled ? 'checked' : ''}>
|
|
583
|
-
</td>
|
|
584
|
-
<td>${filter.name}</td>
|
|
585
|
-
<td>
|
|
586
|
-
<input type="number"
|
|
587
|
-
class="form-control form-control-sm filter-focus-input"
|
|
588
|
-
data-filter-id="${filterId}"
|
|
589
|
-
value="${filter.focus_position}"
|
|
590
|
-
min="0"
|
|
591
|
-
step="1">
|
|
592
|
-
</td>
|
|
593
|
-
`;
|
|
594
|
-
tbody.appendChild(row);
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
}
|
|
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;
|
|
598
440
|
} else {
|
|
599
|
-
|
|
441
|
+
store.filterConfigVisible = false;
|
|
600
442
|
}
|
|
601
443
|
} catch (error) {
|
|
602
444
|
console.error('Error loading filter config:', error);
|
|
603
|
-
|
|
445
|
+
store.filterConfigVisible = false;
|
|
604
446
|
}
|
|
605
447
|
}
|
|
606
448
|
|
|
@@ -609,75 +451,85 @@ async function loadFilterConfig() {
|
|
|
609
451
|
* Returns: Object with { success: number, failed: number }
|
|
610
452
|
*/
|
|
611
453
|
async function saveModifiedFilters() {
|
|
612
|
-
const
|
|
613
|
-
|
|
454
|
+
const store = Alpine.store('citrascope');
|
|
455
|
+
const filters = store.filters || {};
|
|
456
|
+
const filterIds = Object.keys(filters);
|
|
614
457
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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;
|
|
618
462
|
if (enabledCount === 0) {
|
|
619
463
|
showConfigMessage('At least one filter must be enabled', 'danger');
|
|
620
|
-
return { success: 0, failed:
|
|
464
|
+
return { success: 0, failed: filterIds.length };
|
|
621
465
|
}
|
|
622
466
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const filterId = input.dataset.filterId;
|
|
629
|
-
const focusPosition = parseInt(input.value);
|
|
467
|
+
// Collect all filter updates from store
|
|
468
|
+
const filterUpdates = [];
|
|
469
|
+
for (const [filterId, filter] of Object.entries(filters)) {
|
|
470
|
+
const focusPosition = parseInt(filter.focus_position);
|
|
471
|
+
if (Number.isNaN(focusPosition) || focusPosition < 0) continue;
|
|
630
472
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
473
|
+
filterUpdates.push({
|
|
474
|
+
filter_id: filterId,
|
|
475
|
+
focus_position: focusPosition,
|
|
476
|
+
enabled: filter.enabled !== undefined ? filter.enabled : true
|
|
477
|
+
});
|
|
478
|
+
}
|
|
635
479
|
|
|
636
|
-
|
|
637
|
-
const checkbox = document.querySelector(`.filter-enabled-checkbox[data-filter-id="${filterId}"]`);
|
|
638
|
-
const enabled = checkbox ? checkbox.checked : true;
|
|
480
|
+
if (filterUpdates.length === 0) return { success: 0, failed: 0 };
|
|
639
481
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
482
|
+
// Send single batch update
|
|
483
|
+
try {
|
|
484
|
+
const response = await fetch('/api/adapter/filters/batch', {
|
|
485
|
+
method: 'POST',
|
|
486
|
+
headers: {
|
|
487
|
+
'Content-Type': 'application/json'
|
|
488
|
+
},
|
|
489
|
+
body: JSON.stringify(filterUpdates)
|
|
490
|
+
});
|
|
648
491
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
492
|
+
if (response.ok) {
|
|
493
|
+
const data = await response.json();
|
|
494
|
+
const successCount = data.updated_count || 0;
|
|
495
|
+
|
|
496
|
+
// After batch update, sync to backend
|
|
497
|
+
try {
|
|
498
|
+
const syncResponse = await fetch('/api/adapter/filters/sync', {
|
|
499
|
+
method: 'POST'
|
|
500
|
+
});
|
|
501
|
+
if (!syncResponse.ok) {
|
|
502
|
+
console.error('Failed to sync filters to backend');
|
|
658
503
|
}
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.error('Error syncing filters to backend:', error);
|
|
659
506
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
507
|
+
|
|
508
|
+
return { success: successCount, failed: 0 };
|
|
509
|
+
} else {
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
const errorMsg = data.error || 'Unknown error';
|
|
512
|
+
console.error(`Failed to save filters: ${errorMsg}`);
|
|
513
|
+
|
|
514
|
+
// Show error to user
|
|
515
|
+
if (response.status === 400 && errorMsg.includes('last enabled filter')) {
|
|
516
|
+
showConfigMessage(errorMsg, 'danger');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { success: 0, failed: filterUpdates.length };
|
|
663
520
|
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.error('Error saving filters:', error);
|
|
523
|
+
return { success: 0, failed: filterUpdates.length };
|
|
664
524
|
}
|
|
665
|
-
|
|
666
|
-
return { success: successCount, failed: failedCount };
|
|
667
525
|
}
|
|
668
526
|
|
|
669
527
|
/**
|
|
670
528
|
* Trigger or cancel autofocus routine
|
|
671
529
|
*/
|
|
672
530
|
async function triggerAutofocus() {
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
const buttonSpinner = document.getElementById('autofocusButtonSpinner');
|
|
676
|
-
|
|
677
|
-
if (!button || !buttonText || !buttonSpinner) return;
|
|
678
|
-
|
|
679
|
-
// Check if this is a cancel action
|
|
680
|
-
const isCancel = button.dataset.action === 'cancel';
|
|
531
|
+
const store = Alpine.store('citrascope');
|
|
532
|
+
const isCancel = store.status?.autofocus_requested;
|
|
681
533
|
|
|
682
534
|
if (isCancel) {
|
|
683
535
|
// Cancel autofocus
|
|
@@ -689,7 +541,6 @@ async function triggerAutofocus() {
|
|
|
689
541
|
|
|
690
542
|
if (response.ok && data.success) {
|
|
691
543
|
showToast('Autofocus cancelled', 'info');
|
|
692
|
-
updateAutofocusButton(false);
|
|
693
544
|
} else {
|
|
694
545
|
showToast('Nothing to cancel', 'warning');
|
|
695
546
|
}
|
|
@@ -701,8 +552,7 @@ async function triggerAutofocus() {
|
|
|
701
552
|
}
|
|
702
553
|
|
|
703
554
|
// Request autofocus
|
|
704
|
-
|
|
705
|
-
buttonSpinner.style.display = 'inline-block';
|
|
555
|
+
store.isAutofocusing = true;
|
|
706
556
|
|
|
707
557
|
try {
|
|
708
558
|
const response = await fetch('/api/adapter/autofocus', {
|
|
@@ -713,7 +563,6 @@ async function triggerAutofocus() {
|
|
|
713
563
|
|
|
714
564
|
if (response.ok) {
|
|
715
565
|
showToast('Autofocus queued', 'success');
|
|
716
|
-
updateAutofocusButton(true);
|
|
717
566
|
} else {
|
|
718
567
|
showToast(data.error || 'Autofocus request failed', 'error');
|
|
719
568
|
}
|
|
@@ -721,30 +570,7 @@ async function triggerAutofocus() {
|
|
|
721
570
|
console.error('Error triggering autofocus:', error);
|
|
722
571
|
showToast('Failed to trigger autofocus', 'error');
|
|
723
572
|
} finally {
|
|
724
|
-
|
|
725
|
-
buttonSpinner.style.display = 'none';
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
/**
|
|
730
|
-
* Update autofocus button state based on whether autofocus is queued
|
|
731
|
-
*/
|
|
732
|
-
function updateAutofocusButton(isQueued) {
|
|
733
|
-
const button = document.getElementById('runAutofocusButton');
|
|
734
|
-
const buttonText = document.getElementById('autofocusButtonText');
|
|
735
|
-
|
|
736
|
-
if (!button || !buttonText) return;
|
|
737
|
-
|
|
738
|
-
if (isQueued) {
|
|
739
|
-
buttonText.textContent = 'Cancel Autofocus';
|
|
740
|
-
button.dataset.action = 'cancel';
|
|
741
|
-
button.classList.remove('btn-outline-primary');
|
|
742
|
-
button.classList.add('btn-outline-warning');
|
|
743
|
-
} else {
|
|
744
|
-
buttonText.textContent = 'Run Autofocus';
|
|
745
|
-
button.dataset.action = 'request';
|
|
746
|
-
button.classList.remove('btn-outline-warning');
|
|
747
|
-
button.classList.add('btn-outline-primary');
|
|
573
|
+
store.isAutofocusing = false;
|
|
748
574
|
}
|
|
749
575
|
}
|
|
750
576
|
|
|
@@ -795,12 +621,25 @@ export async function initFilterConfig() {
|
|
|
795
621
|
/**
|
|
796
622
|
* Setup autofocus button event listener (call once during init)
|
|
797
623
|
*/
|
|
624
|
+
// Autofocus button now uses Alpine @click directive in template
|
|
625
|
+
// Expose triggerAutofocus for template access
|
|
798
626
|
export function setupAutofocusButton() {
|
|
799
|
-
|
|
800
|
-
if (autofocusBtn) {
|
|
801
|
-
autofocusBtn.addEventListener('click', triggerAutofocus);
|
|
802
|
-
}
|
|
627
|
+
window.triggerAutofocus = triggerAutofocus;
|
|
803
628
|
}
|
|
804
629
|
|
|
805
|
-
|
|
806
|
-
|
|
630
|
+
/**
|
|
631
|
+
* Reload adapter schema (called from Alpine components when device type changes)
|
|
632
|
+
*/
|
|
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
|
+
});
|
|
643
|
+
|
|
644
|
+
await loadAdapterSchema(adapter, currentSettings);
|
|
645
|
+
}
|