citrascope 0.7.0__py3-none-any.whl → 0.8.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 +80 -2
- citrascope/hardware/adapter_registry.py +10 -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 +102 -0
- citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
- citrascope/hardware/devices/camera/usb_camera.py +402 -0
- citrascope/hardware/devices/camera/ximea_camera.py +744 -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 +787 -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 +13 -0
- citrascope/time/time_health.py +96 -0
- citrascope/time/time_monitor.py +164 -0
- citrascope/time/time_sources.py +62 -0
- citrascope/web/app.py +229 -51
- citrascope/web/static/app.js +296 -36
- citrascope/web/static/config.js +216 -81
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/style.css +39 -0
- citrascope/web/templates/dashboard.html +114 -9
- {citrascope-0.7.0.dist-info → citrascope-0.8.0.dist-info}/METADATA +17 -1
- citrascope-0.8.0.dist-info/RECORD +62 -0
- citrascope-0.7.0.dist-info/RECORD +0 -41
- {citrascope-0.7.0.dist-info → citrascope-0.8.0.dist-info}/WHEEL +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.8.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.8.0.dist-info}/licenses/LICENSE +0 -0
citrascope/web/static/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Configuration management for CitraScope
|
|
2
2
|
|
|
3
3
|
import { getConfig, saveConfig, getConfigStatus, getHardwareAdapters, getAdapterSchema } from './api.js';
|
|
4
|
+
import { getFilterColor } from './filters.js';
|
|
4
5
|
|
|
5
6
|
// API Host constants - must match backend constants in app.py
|
|
6
7
|
const PROD_API_HOST = 'api.citra.space';
|
|
@@ -18,10 +19,14 @@ export async function initConfig() {
|
|
|
18
19
|
// Hardware adapter selection change
|
|
19
20
|
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
20
21
|
if (adapterSelect) {
|
|
21
|
-
adapterSelect.addEventListener('change', async
|
|
22
|
+
adapterSelect.addEventListener('change', async (e) => {
|
|
22
23
|
const adapter = e.target.value;
|
|
23
24
|
if (adapter) {
|
|
24
|
-
|
|
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);
|
|
25
30
|
await loadFilterConfig();
|
|
26
31
|
} else {
|
|
27
32
|
document.getElementById('adapter-settings-container').innerHTML = '';
|
|
@@ -34,7 +39,7 @@ export async function initConfig() {
|
|
|
34
39
|
// API endpoint selection change
|
|
35
40
|
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
36
41
|
if (apiEndpointSelect) {
|
|
37
|
-
apiEndpointSelect.addEventListener('change',
|
|
42
|
+
apiEndpointSelect.addEventListener('change', (e) => {
|
|
38
43
|
const customContainer = document.getElementById('customHostContainer');
|
|
39
44
|
if (e.target.value === 'custom') {
|
|
40
45
|
customContainer.style.display = 'block';
|
|
@@ -173,10 +178,21 @@ async function loadConfiguration() {
|
|
|
173
178
|
autofocusInterval.value = config.autofocus_interval_minutes || 60;
|
|
174
179
|
}
|
|
175
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
|
+
|
|
176
188
|
// Load adapter-specific settings if adapter is selected
|
|
177
189
|
if (config.hardware_adapter) {
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
// adapter_settings is nested: {"nina": {...}, "kstars": {...}, "direct": {...}}
|
|
191
|
+
// Extract the current adapter's settings
|
|
192
|
+
const allAdapterSettings = config.adapter_settings || {};
|
|
193
|
+
const currentAdapterSettings = allAdapterSettings[config.hardware_adapter] || {};
|
|
194
|
+
await loadAdapterSchema(config.hardware_adapter, currentAdapterSettings);
|
|
195
|
+
populateAdapterSettings(currentAdapterSettings);
|
|
180
196
|
}
|
|
181
197
|
} catch (error) {
|
|
182
198
|
console.error('Failed to load config:', error);
|
|
@@ -186,9 +202,16 @@ async function loadConfiguration() {
|
|
|
186
202
|
/**
|
|
187
203
|
* Load adapter schema and render settings form
|
|
188
204
|
*/
|
|
189
|
-
async function loadAdapterSchema(adapterName) {
|
|
205
|
+
async function loadAdapterSchema(adapterName, currentSettings = {}) {
|
|
190
206
|
try {
|
|
191
|
-
|
|
207
|
+
// Pass current adapter settings for dynamic schema generation
|
|
208
|
+
const settingsParam = Object.keys(currentSettings).length > 0
|
|
209
|
+
? `?current_settings=${encodeURIComponent(JSON.stringify(currentSettings))}`
|
|
210
|
+
: '';
|
|
211
|
+
|
|
212
|
+
const response = await fetch(`/api/hardware-adapters/${adapterName}/schema${settingsParam}`);
|
|
213
|
+
const data = await response.json();
|
|
214
|
+
|
|
192
215
|
currentAdapterSchema = data.schema || [];
|
|
193
216
|
renderAdapterSettings(currentAdapterSchema);
|
|
194
217
|
} catch (error) {
|
|
@@ -208,57 +231,115 @@ function renderAdapterSettings(schema) {
|
|
|
208
231
|
return;
|
|
209
232
|
}
|
|
210
233
|
|
|
211
|
-
|
|
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;
|
|
212
259
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
292
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
293
|
+
if (description && field.type !== 'bool') {
|
|
294
|
+
html += `<small class="text-muted">${description}</small>`;
|
|
295
|
+
}
|
|
296
|
+
html += '</div>';
|
|
297
|
+
});
|
|
223
298
|
|
|
224
|
-
html +=
|
|
225
|
-
|
|
299
|
+
html += `</div></div></div>`; // Close row, card-body, card
|
|
300
|
+
});
|
|
226
301
|
|
|
227
|
-
|
|
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
|
-
}
|
|
302
|
+
container.innerHTML = html;
|
|
253
303
|
|
|
254
|
-
|
|
255
|
-
|
|
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);
|
|
319
|
+
|
|
320
|
+
// Repopulate settings (preserves user's selections)
|
|
321
|
+
populateAdapterSettings(currentSettings);
|
|
322
|
+
});
|
|
256
323
|
}
|
|
257
|
-
html += '</div>';
|
|
258
324
|
});
|
|
325
|
+
}
|
|
259
326
|
|
|
260
|
-
|
|
261
|
-
|
|
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';
|
|
262
343
|
}
|
|
263
344
|
|
|
264
345
|
/**
|
|
@@ -361,6 +442,8 @@ async function saveConfiguration(event) {
|
|
|
361
442
|
// Autofocus settings (top-level)
|
|
362
443
|
scheduled_autofocus_enabled: document.getElementById('scheduled_autofocus_enabled')?.checked || false,
|
|
363
444
|
autofocus_interval_minutes: parseInt(document.getElementById('autofocus_interval_minutes')?.value || 60, 10),
|
|
445
|
+
// Time sync settings (monitoring always enabled)
|
|
446
|
+
time_offset_pause_ms: parseFloat(document.getElementById('time_offset_pause_ms')?.value || 500),
|
|
364
447
|
// API settings from endpoint selector
|
|
365
448
|
host: host,
|
|
366
449
|
port: port,
|
|
@@ -428,7 +511,7 @@ async function saveConfiguration(event) {
|
|
|
428
511
|
* @param {string} type - 'danger' for errors, 'success' for success messages
|
|
429
512
|
* @param {boolean} autohide - Whether to auto-hide the toast
|
|
430
513
|
*/
|
|
431
|
-
function createToast(message, type = 'danger', autohide = false) {
|
|
514
|
+
export function createToast(message, type = 'danger', autohide = false) {
|
|
432
515
|
const toastContainer = document.getElementById('toastContainer');
|
|
433
516
|
if (!toastContainer) {
|
|
434
517
|
console.error('Toast container not found');
|
|
@@ -556,6 +639,9 @@ async function loadFilterConfig() {
|
|
|
556
639
|
// Show the filter section
|
|
557
640
|
if (filterSection) filterSection.style.display = 'block';
|
|
558
641
|
|
|
642
|
+
// Update enabled filters display on dashboard
|
|
643
|
+
updateEnabledFiltersDisplay(data.filters);
|
|
644
|
+
|
|
559
645
|
// Populate filter table
|
|
560
646
|
const tbody = document.getElementById('filterTableBody');
|
|
561
647
|
const noFiltersMsg = document.getElementById('noFiltersMessage');
|
|
@@ -573,6 +659,7 @@ async function loadFilterConfig() {
|
|
|
573
659
|
filterIds.forEach(filterId => {
|
|
574
660
|
const filter = filters[filterId];
|
|
575
661
|
const isEnabled = filter.enabled !== undefined ? filter.enabled : true;
|
|
662
|
+
const filterColor = getFilterColor(filter.name);
|
|
576
663
|
const row = document.createElement('tr');
|
|
577
664
|
row.innerHTML = `
|
|
578
665
|
<td>
|
|
@@ -581,7 +668,9 @@ async function loadFilterConfig() {
|
|
|
581
668
|
data-filter-id="${filterId}"
|
|
582
669
|
${isEnabled ? 'checked' : ''}>
|
|
583
670
|
</td>
|
|
584
|
-
<td
|
|
671
|
+
<td>
|
|
672
|
+
<span class="badge" style="background-color: ${filterColor}; color: white;">${filter.name}</span>
|
|
673
|
+
</td>
|
|
585
674
|
<td>
|
|
586
675
|
<input type="number"
|
|
587
676
|
class="form-control form-control-sm filter-focus-input"
|
|
@@ -620,50 +709,74 @@ async function saveModifiedFilters() {
|
|
|
620
709
|
return { success: 0, failed: inputs.length };
|
|
621
710
|
}
|
|
622
711
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
// Save all filter values
|
|
712
|
+
// Collect all filter updates into array
|
|
713
|
+
const filterUpdates = [];
|
|
627
714
|
for (const input of inputs) {
|
|
628
715
|
const filterId = input.dataset.filterId;
|
|
629
716
|
const focusPosition = parseInt(input.value);
|
|
630
717
|
|
|
631
|
-
if (isNaN(focusPosition) || focusPosition < 0) {
|
|
632
|
-
|
|
633
|
-
continue;
|
|
718
|
+
if (Number.isNaN(focusPosition) || focusPosition < 0) {
|
|
719
|
+
continue; // Skip invalid entries
|
|
634
720
|
}
|
|
635
721
|
|
|
636
722
|
// Get enabled state from corresponding checkbox
|
|
637
723
|
const checkbox = document.querySelector(`.filter-enabled-checkbox[data-filter-id="${filterId}"]`);
|
|
638
724
|
const enabled = checkbox ? checkbox.checked : true;
|
|
639
725
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
body: JSON.stringify({ focus_position: focusPosition, enabled: enabled })
|
|
647
|
-
});
|
|
726
|
+
filterUpdates.push({
|
|
727
|
+
filter_id: filterId,
|
|
728
|
+
focus_position: focusPosition,
|
|
729
|
+
enabled: enabled
|
|
730
|
+
});
|
|
731
|
+
}
|
|
648
732
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
733
|
+
if (filterUpdates.length === 0) {
|
|
734
|
+
return { success: 0, failed: 0 };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Send single batch update
|
|
738
|
+
try {
|
|
739
|
+
const response = await fetch('/api/adapter/filters/batch', {
|
|
740
|
+
method: 'POST',
|
|
741
|
+
headers: {
|
|
742
|
+
'Content-Type': 'application/json'
|
|
743
|
+
},
|
|
744
|
+
body: JSON.stringify(filterUpdates)
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
if (response.ok) {
|
|
748
|
+
const data = await response.json();
|
|
749
|
+
const successCount = data.updated_count || 0;
|
|
750
|
+
|
|
751
|
+
// After batch update, sync to backend
|
|
752
|
+
try {
|
|
753
|
+
const syncResponse = await fetch('/api/adapter/filters/sync', {
|
|
754
|
+
method: 'POST'
|
|
755
|
+
});
|
|
756
|
+
if (!syncResponse.ok) {
|
|
757
|
+
console.error('Failed to sync filters to backend');
|
|
658
758
|
}
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.error('Error syncing filters to backend:', error);
|
|
659
761
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
762
|
+
|
|
763
|
+
return { success: successCount, failed: 0 };
|
|
764
|
+
} else {
|
|
765
|
+
const data = await response.json();
|
|
766
|
+
const errorMsg = data.error || 'Unknown error';
|
|
767
|
+
console.error(`Failed to save filters: ${errorMsg}`);
|
|
768
|
+
|
|
769
|
+
// Show error to user
|
|
770
|
+
if (response.status === 400 && errorMsg.includes('last enabled filter')) {
|
|
771
|
+
showConfigMessage(errorMsg, 'danger');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return { success: 0, failed: filterUpdates.length };
|
|
663
775
|
}
|
|
776
|
+
} catch (error) {
|
|
777
|
+
console.error('Error saving filters:', error);
|
|
778
|
+
return { success: 0, failed: filterUpdates.length };
|
|
664
779
|
}
|
|
665
|
-
|
|
666
|
-
return { success: successCount, failed: failedCount };
|
|
667
780
|
}
|
|
668
781
|
|
|
669
782
|
/**
|
|
@@ -792,6 +905,28 @@ export async function initFilterConfig() {
|
|
|
792
905
|
await loadFilterConfig();
|
|
793
906
|
}
|
|
794
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Update the enabled filters display on the dashboard.
|
|
910
|
+
* @param {Object} filters - Filter configuration object
|
|
911
|
+
*/
|
|
912
|
+
function updateEnabledFiltersDisplay(filters) {
|
|
913
|
+
const filtersEl = document.getElementById('enabledFilters');
|
|
914
|
+
if (!filtersEl) return;
|
|
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
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
795
930
|
/**
|
|
796
931
|
* Setup autofocus button event listener (call once during init)
|
|
797
932
|
*/
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter color utilities for visual wavelength representation.
|
|
3
|
+
*
|
|
4
|
+
* Maps standard astronomical filters to their representative colors
|
|
5
|
+
* based on spectral wavelength. Colors are darker variants suitable
|
|
6
|
+
* for badge backgrounds with white text.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Standard filter name to color mappings.
|
|
11
|
+
* Colors chosen to be dark enough for white text readability.
|
|
12
|
+
*/
|
|
13
|
+
export const FILTER_COLORS = {
|
|
14
|
+
// Johnson-Cousins UBVRI
|
|
15
|
+
'U': '#33005D', // Ultraviolet (365nm)
|
|
16
|
+
'B': '#002A6E', // Blue (445nm)
|
|
17
|
+
'V': '#005500', // Visual/Green (551nm)
|
|
18
|
+
'R': '#6E0000', // Red (658nm)
|
|
19
|
+
'I': '#550000', // Near-infrared (806nm)
|
|
20
|
+
|
|
21
|
+
// Sloan ugriz
|
|
22
|
+
'u': '#3B0066', // Ultraviolet (354nm)
|
|
23
|
+
'g': '#00442A', // Green (477nm)
|
|
24
|
+
'r': '#6E2200', // Orange-Red (623nm)
|
|
25
|
+
'i': '#5D0000', // Near-infrared (763nm)
|
|
26
|
+
'z': '#440000', // Infrared (913nm)
|
|
27
|
+
|
|
28
|
+
// RGB filters
|
|
29
|
+
'Red': '#770000',
|
|
30
|
+
'Green': '#006600',
|
|
31
|
+
'Blue': '#003380',
|
|
32
|
+
'Clear': '#4C4C4C', // Gray for broadband clear
|
|
33
|
+
'Luminance': '#4C4C4C', // Gray for luminance
|
|
34
|
+
|
|
35
|
+
// Narrowband emission line filters
|
|
36
|
+
'Ha': '#6E002A', // H-alpha (656.3nm) - characteristic deep red
|
|
37
|
+
'Hb': '#004C66', // H-beta (486.1nm) - cyan
|
|
38
|
+
'OIII': '#005D44', // OIII (500.7nm) - teal
|
|
39
|
+
'SII': '#6E112A', // SII (672.4nm) - pink-red
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get color for a filter based on name.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} name - Filter name (e.g., "Ha", "Red", "V")
|
|
46
|
+
* @returns {string} Hex color string suitable for dark badge backgrounds
|
|
47
|
+
*/
|
|
48
|
+
export function getFilterColor(name) {
|
|
49
|
+
// Check known filters first
|
|
50
|
+
if (FILTER_COLORS[name]) {
|
|
51
|
+
return FILTER_COLORS[name];
|
|
52
|
+
}
|
|
53
|
+
// Default to gray if no match
|
|
54
|
+
return '#777777';
|
|
55
|
+
}
|
citrascope/web/static/style.css
CHANGED
|
@@ -49,6 +49,45 @@ body {
|
|
|
49
49
|
gap: 0.5em;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
#automatedSchedulingStatus {
|
|
53
|
+
display: inline-block;
|
|
54
|
+
min-width: 70px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#toggleAutomatedSchedulingSwitch {
|
|
58
|
+
margin-top: 6px;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#toggleAutomatedSchedulingSwitch:checked {
|
|
63
|
+
background-color: #198754;
|
|
64
|
+
border-color: #198754;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#toggleAutomatedSchedulingSwitch:focus {
|
|
68
|
+
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#processingStatus {
|
|
72
|
+
display: inline-block;
|
|
73
|
+
min-width: 70px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Task processing switch alignment */
|
|
77
|
+
#toggleProcessingSwitch {
|
|
78
|
+
margin-top: 6px;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#toggleProcessingSwitch:checked {
|
|
83
|
+
background-color: #198754;
|
|
84
|
+
border-color: #198754;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#toggleProcessingSwitch:focus {
|
|
88
|
+
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
/* Clickable elements */
|
|
53
92
|
#headerVersion, #updateIndicator {
|
|
54
93
|
cursor: pointer;
|