citrascope 0.5.2__py3-none-any.whl → 0.7.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/citra_scope_daemon.py +22 -38
- citrascope/hardware/abstract_astro_hardware_adapter.py +64 -6
- citrascope/hardware/kstars_dbus_adapter.py +875 -30
- citrascope/hardware/kstars_scheduler_template.esl +30 -0
- citrascope/hardware/kstars_sequence_template.esq +16 -0
- citrascope/hardware/nina_adv_http_adapter.py +74 -59
- citrascope/hardware/nina_adv_http_survey_template.json +4 -4
- citrascope/settings/citrascope_settings.py +25 -4
- citrascope/tasks/runner.py +103 -0
- citrascope/tasks/scope/static_telescope_task.py +6 -1
- citrascope/web/app.py +82 -37
- citrascope/web/static/app.js +83 -0
- citrascope/web/static/config.js +244 -39
- citrascope/web/templates/dashboard.html +62 -27
- {citrascope-0.5.2.dist-info → citrascope-0.7.0.dist-info}/METADATA +19 -1
- {citrascope-0.5.2.dist-info → citrascope-0.7.0.dist-info}/RECORD +19 -17
- {citrascope-0.5.2.dist-info → citrascope-0.7.0.dist-info}/WHEEL +0 -0
- {citrascope-0.5.2.dist-info → citrascope-0.7.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.5.2.dist-info → citrascope-0.7.0.dist-info}/licenses/LICENSE +0 -0
citrascope/web/static/config.js
CHANGED
|
@@ -9,6 +9,7 @@ const DEFAULT_API_PORT = 443;
|
|
|
9
9
|
|
|
10
10
|
let currentAdapterSchema = [];
|
|
11
11
|
export let currentConfig = {};
|
|
12
|
+
let savedAdapter = null; // Track the currently saved adapter
|
|
12
13
|
|
|
13
14
|
export async function initConfig() {
|
|
14
15
|
// Populate hardware adapter dropdown
|
|
@@ -21,8 +22,11 @@ export async function initConfig() {
|
|
|
21
22
|
const adapter = e.target.value;
|
|
22
23
|
if (adapter) {
|
|
23
24
|
await loadAdapterSchema(adapter);
|
|
25
|
+
await loadFilterConfig();
|
|
24
26
|
} else {
|
|
25
27
|
document.getElementById('adapter-settings-container').innerHTML = '';
|
|
28
|
+
const filterSection = document.getElementById('filterConfigSection');
|
|
29
|
+
if (filterSection) filterSection.style.display = 'none';
|
|
26
30
|
}
|
|
27
31
|
});
|
|
28
32
|
}
|
|
@@ -106,6 +110,7 @@ async function loadConfiguration() {
|
|
|
106
110
|
try {
|
|
107
111
|
const config = await getConfig();
|
|
108
112
|
currentConfig = config; // Save for reuse when saving
|
|
113
|
+
savedAdapter = config.hardware_adapter; // Track saved adapter
|
|
109
114
|
|
|
110
115
|
// Display config file path
|
|
111
116
|
const configPathElement = document.getElementById('configFilePath');
|
|
@@ -158,6 +163,16 @@ async function loadConfiguration() {
|
|
|
158
163
|
document.getElementById('keep_images').checked = config.keep_images || false;
|
|
159
164
|
document.getElementById('file_logging_enabled').checked = config.file_logging_enabled !== undefined ? config.file_logging_enabled : true;
|
|
160
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
|
+
|
|
161
176
|
// Load adapter-specific settings if adapter is selected
|
|
162
177
|
if (config.hardware_adapter) {
|
|
163
178
|
await loadAdapterSchema(config.hardware_adapter);
|
|
@@ -196,6 +211,11 @@ function renderAdapterSettings(schema) {
|
|
|
196
211
|
let html = '<h5 class="mb-3">Adapter Settings</h5><div class="row g-3 mb-4">';
|
|
197
212
|
|
|
198
213
|
schema.forEach(field => {
|
|
214
|
+
// Skip readonly fields (handled elsewhere in UI)
|
|
215
|
+
if (field.readonly) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
const isRequired = field.required ? '<span class="text-danger">*</span>' : '';
|
|
200
220
|
const placeholder = field.placeholder || '';
|
|
201
221
|
const description = field.description || '';
|
|
@@ -275,16 +295,18 @@ function collectAdapterSettings() {
|
|
|
275
295
|
value = input.value;
|
|
276
296
|
}
|
|
277
297
|
|
|
298
|
+
// Skip empty values for non-checkbox fields (will use backend defaults)
|
|
299
|
+
if (input.type !== 'checkbox' && (value === '' || value === null || value === undefined)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
278
303
|
// Type conversion
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
value = parseFloat(value);
|
|
284
|
-
} else if (fieldType === 'bool') {
|
|
285
|
-
// Already handled above
|
|
286
|
-
}
|
|
304
|
+
if (fieldType === 'int') {
|
|
305
|
+
value = parseInt(value, 10);
|
|
306
|
+
} else if (fieldType === 'float') {
|
|
307
|
+
value = parseFloat(value);
|
|
287
308
|
}
|
|
309
|
+
// bool type already handled above
|
|
288
310
|
|
|
289
311
|
settings[fieldName] = value;
|
|
290
312
|
});
|
|
@@ -336,6 +358,9 @@ async function saveConfiguration(event) {
|
|
|
336
358
|
log_level: document.getElementById('logLevel').value,
|
|
337
359
|
keep_images: document.getElementById('keep_images').checked,
|
|
338
360
|
file_logging_enabled: document.getElementById('file_logging_enabled').checked,
|
|
361
|
+
// Autofocus settings (top-level)
|
|
362
|
+
scheduled_autofocus_enabled: document.getElementById('scheduled_autofocus_enabled')?.checked || false,
|
|
363
|
+
autofocus_interval_minutes: parseInt(document.getElementById('autofocus_interval_minutes')?.value || 60, 10),
|
|
339
364
|
// API settings from endpoint selector
|
|
340
365
|
host: host,
|
|
341
366
|
port: port,
|
|
@@ -345,12 +370,27 @@ async function saveConfiguration(event) {
|
|
|
345
370
|
initial_retry_delay_seconds: currentConfig.initial_retry_delay_seconds || 30,
|
|
346
371
|
max_retry_delay_seconds: currentConfig.max_retry_delay_seconds || 300,
|
|
347
372
|
log_retention_days: currentConfig.log_retention_days || 30,
|
|
373
|
+
last_autofocus_timestamp: currentConfig.last_autofocus_timestamp, // Preserve timestamp
|
|
348
374
|
};
|
|
349
375
|
|
|
350
376
|
try {
|
|
377
|
+
// Validate filters BEFORE saving main config (belt and suspenders)
|
|
378
|
+
const inputs = document.querySelectorAll('.filter-focus-input');
|
|
379
|
+
if (inputs.length > 0) {
|
|
380
|
+
const checkboxes = document.querySelectorAll('.filter-enabled-checkbox');
|
|
381
|
+
const enabledCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
|
382
|
+
if (enabledCount === 0) {
|
|
383
|
+
showConfigMessage('At least one filter must be enabled', 'danger');
|
|
384
|
+
return; // Exit early without saving anything
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
351
388
|
const result = await saveConfig(config);
|
|
352
389
|
|
|
353
390
|
if (result.ok) {
|
|
391
|
+
// Update saved adapter to match newly saved config
|
|
392
|
+
savedAdapter = config.hardware_adapter;
|
|
393
|
+
|
|
354
394
|
// After config saved successfully, save any modified filter focus positions
|
|
355
395
|
const filterResults = await saveModifiedFilters();
|
|
356
396
|
|
|
@@ -364,6 +404,9 @@ async function saveConfiguration(event) {
|
|
|
364
404
|
}
|
|
365
405
|
|
|
366
406
|
showConfigSuccess(message);
|
|
407
|
+
|
|
408
|
+
// Reload filters to re-enable editing for the new adapter
|
|
409
|
+
await loadFilterConfig();
|
|
367
410
|
} else {
|
|
368
411
|
// Check for specific error codes
|
|
369
412
|
const errorMsg = result.data.error || result.data.message || 'Failed to save configuration';
|
|
@@ -379,35 +422,82 @@ async function saveConfiguration(event) {
|
|
|
379
422
|
}
|
|
380
423
|
}
|
|
381
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Create and show a Bootstrap toast notification
|
|
427
|
+
* @param {string} message - The message to display
|
|
428
|
+
* @param {string} type - 'danger' for errors, 'success' for success messages
|
|
429
|
+
* @param {boolean} autohide - Whether to auto-hide the toast
|
|
430
|
+
*/
|
|
431
|
+
function createToast(message, type = 'danger', autohide = false) {
|
|
432
|
+
const toastContainer = document.getElementById('toastContainer');
|
|
433
|
+
if (!toastContainer) {
|
|
434
|
+
console.error('Toast container not found');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Create toast element
|
|
439
|
+
const toastId = `toast-${Date.now()}`;
|
|
440
|
+
const toastHTML = `
|
|
441
|
+
<div id="${toastId}" class="toast text-bg-${type}" role="alert" aria-live="assertive" aria-atomic="true">
|
|
442
|
+
<div class="toast-header text-bg-${type}">
|
|
443
|
+
<strong class="me-auto">CitraScope</strong>
|
|
444
|
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
445
|
+
</div>
|
|
446
|
+
<div class="toast-body">
|
|
447
|
+
${message}
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
`;
|
|
451
|
+
|
|
452
|
+
// Insert toast into container
|
|
453
|
+
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
|
454
|
+
|
|
455
|
+
// Get the toast element and initialize Bootstrap toast
|
|
456
|
+
const toastElement = document.getElementById(toastId);
|
|
457
|
+
const toast = new bootstrap.Toast(toastElement, {
|
|
458
|
+
autohide: autohide,
|
|
459
|
+
delay: 5000
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Remove toast element from DOM after it's hidden
|
|
463
|
+
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
464
|
+
toastElement.remove();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Show the toast
|
|
468
|
+
toast.show();
|
|
469
|
+
}
|
|
470
|
+
|
|
382
471
|
/**
|
|
383
472
|
* Show configuration error message
|
|
384
473
|
*/
|
|
385
474
|
function showConfigError(message) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
475
|
+
createToast(message, 'danger', false);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Show configuration message (can be error or success)
|
|
480
|
+
*/
|
|
481
|
+
function showConfigMessage(message, type = 'danger') {
|
|
482
|
+
if (type === 'danger') {
|
|
483
|
+
showConfigError(message);
|
|
484
|
+
} else {
|
|
485
|
+
showConfigSuccess(message);
|
|
486
|
+
}
|
|
389
487
|
}
|
|
390
488
|
|
|
391
489
|
/**
|
|
392
490
|
* Show configuration success message
|
|
393
491
|
*/
|
|
394
492
|
function showConfigSuccess(message) {
|
|
395
|
-
|
|
396
|
-
successDiv.textContent = message;
|
|
397
|
-
successDiv.style.display = 'block';
|
|
398
|
-
|
|
399
|
-
// Auto-hide after 5 seconds
|
|
400
|
-
setTimeout(() => {
|
|
401
|
-
successDiv.style.display = 'none';
|
|
402
|
-
}, 5000);
|
|
493
|
+
createToast(message, 'success', true);
|
|
403
494
|
}
|
|
404
495
|
|
|
405
496
|
/**
|
|
406
|
-
* Hide all configuration messages
|
|
497
|
+
* Hide all configuration messages (no-op for toast compatibility)
|
|
407
498
|
*/
|
|
408
499
|
function hideConfigMessages() {
|
|
409
|
-
|
|
410
|
-
document.getElementById('configSuccess').style.display = 'none';
|
|
500
|
+
// No-op - toasts handle their own hiding
|
|
411
501
|
}
|
|
412
502
|
|
|
413
503
|
/**
|
|
@@ -432,6 +522,24 @@ export function showConfigSection() {
|
|
|
432
522
|
*/
|
|
433
523
|
async function loadFilterConfig() {
|
|
434
524
|
const filterSection = document.getElementById('filterConfigSection');
|
|
525
|
+
const changeMessage = document.getElementById('filterAdapterChangeMessage');
|
|
526
|
+
const tableContainer = document.getElementById('filterTableContainer');
|
|
527
|
+
|
|
528
|
+
// Check if selected adapter matches saved adapter
|
|
529
|
+
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
530
|
+
const selectedAdapter = adapterSelect ? adapterSelect.value : null;
|
|
531
|
+
|
|
532
|
+
if (selectedAdapter && savedAdapter && selectedAdapter !== savedAdapter) {
|
|
533
|
+
// Adapter has changed but not saved yet - show message and hide table
|
|
534
|
+
if (filterSection) filterSection.style.display = 'block';
|
|
535
|
+
if (changeMessage) changeMessage.style.display = 'block';
|
|
536
|
+
if (tableContainer) tableContainer.style.display = 'none';
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Hide message and show table when adapters match
|
|
541
|
+
if (changeMessage) changeMessage.style.display = 'none';
|
|
542
|
+
if (tableContainer) tableContainer.style.display = 'block';
|
|
435
543
|
|
|
436
544
|
try {
|
|
437
545
|
const response = await fetch('/api/adapter/filters');
|
|
@@ -464,9 +572,15 @@ async function loadFilterConfig() {
|
|
|
464
572
|
|
|
465
573
|
filterIds.forEach(filterId => {
|
|
466
574
|
const filter = filters[filterId];
|
|
575
|
+
const isEnabled = filter.enabled !== undefined ? filter.enabled : true;
|
|
467
576
|
const row = document.createElement('tr');
|
|
468
577
|
row.innerHTML = `
|
|
469
|
-
<td
|
|
578
|
+
<td>
|
|
579
|
+
<input type="checkbox"
|
|
580
|
+
class="form-check-input filter-enabled-checkbox"
|
|
581
|
+
data-filter-id="${filterId}"
|
|
582
|
+
${isEnabled ? 'checked' : ''}>
|
|
583
|
+
</td>
|
|
470
584
|
<td>${filter.name}</td>
|
|
471
585
|
<td>
|
|
472
586
|
<input type="number"
|
|
@@ -491,13 +605,21 @@ async function loadFilterConfig() {
|
|
|
491
605
|
}
|
|
492
606
|
|
|
493
607
|
/**
|
|
494
|
-
* Save all filter focus positions (called during main config save)
|
|
608
|
+
* Save all filter focus positions and enabled states (called during main config save)
|
|
495
609
|
* Returns: Object with { success: number, failed: number }
|
|
496
610
|
*/
|
|
497
611
|
async function saveModifiedFilters() {
|
|
498
612
|
const inputs = document.querySelectorAll('.filter-focus-input');
|
|
499
613
|
if (inputs.length === 0) return { success: 0, failed: 0 }; // No filters to save
|
|
500
614
|
|
|
615
|
+
// Belt and suspenders: Validate at least one filter is enabled before saving
|
|
616
|
+
const checkboxes = document.querySelectorAll('.filter-enabled-checkbox');
|
|
617
|
+
const enabledCount = Array.from(checkboxes).filter(cb => cb.checked).length;
|
|
618
|
+
if (enabledCount === 0) {
|
|
619
|
+
showConfigMessage('At least one filter must be enabled', 'danger');
|
|
620
|
+
return { success: 0, failed: inputs.length };
|
|
621
|
+
}
|
|
622
|
+
|
|
501
623
|
let successCount = 0;
|
|
502
624
|
let failedCount = 0;
|
|
503
625
|
|
|
@@ -511,20 +633,29 @@ async function saveModifiedFilters() {
|
|
|
511
633
|
continue;
|
|
512
634
|
}
|
|
513
635
|
|
|
636
|
+
// Get enabled state from corresponding checkbox
|
|
637
|
+
const checkbox = document.querySelector(`.filter-enabled-checkbox[data-filter-id="${filterId}"]`);
|
|
638
|
+
const enabled = checkbox ? checkbox.checked : true;
|
|
639
|
+
|
|
514
640
|
try {
|
|
515
641
|
const response = await fetch(`/api/adapter/filters/${filterId}`, {
|
|
516
642
|
method: 'PATCH',
|
|
517
643
|
headers: {
|
|
518
644
|
'Content-Type': 'application/json'
|
|
519
645
|
},
|
|
520
|
-
body: JSON.stringify({ focus_position: focusPosition })
|
|
646
|
+
body: JSON.stringify({ focus_position: focusPosition, enabled: enabled })
|
|
521
647
|
});
|
|
522
648
|
|
|
523
649
|
if (response.ok) {
|
|
524
650
|
successCount++;
|
|
525
651
|
} else {
|
|
526
652
|
failedCount++;
|
|
527
|
-
|
|
653
|
+
const data = await response.json();
|
|
654
|
+
console.error(`Failed to save filter ${filterId}: ${data.error || 'Unknown error'}`);
|
|
655
|
+
// If it's the "last enabled filter" error, show it to the user
|
|
656
|
+
if (response.status === 400 && data.error && data.error.includes('last enabled filter')) {
|
|
657
|
+
showConfigMessage(data.error, 'danger');
|
|
658
|
+
}
|
|
528
659
|
}
|
|
529
660
|
} catch (error) {
|
|
530
661
|
failedCount++;
|
|
@@ -536,7 +667,7 @@ async function saveModifiedFilters() {
|
|
|
536
667
|
}
|
|
537
668
|
|
|
538
669
|
/**
|
|
539
|
-
* Trigger autofocus routine
|
|
670
|
+
* Trigger or cancel autofocus routine
|
|
540
671
|
*/
|
|
541
672
|
async function triggerAutofocus() {
|
|
542
673
|
const button = document.getElementById('runAutofocusButton');
|
|
@@ -545,12 +676,32 @@ async function triggerAutofocus() {
|
|
|
545
676
|
|
|
546
677
|
if (!button || !buttonText || !buttonSpinner) return;
|
|
547
678
|
|
|
548
|
-
//
|
|
549
|
-
|
|
679
|
+
// Check if this is a cancel action
|
|
680
|
+
const isCancel = button.dataset.action === 'cancel';
|
|
550
681
|
|
|
551
|
-
|
|
682
|
+
if (isCancel) {
|
|
683
|
+
// Cancel autofocus
|
|
684
|
+
try {
|
|
685
|
+
const response = await fetch('/api/adapter/autofocus/cancel', {
|
|
686
|
+
method: 'POST'
|
|
687
|
+
});
|
|
688
|
+
const data = await response.json();
|
|
689
|
+
|
|
690
|
+
if (response.ok && data.success) {
|
|
691
|
+
showToast('Autofocus cancelled', 'info');
|
|
692
|
+
updateAutofocusButton(false);
|
|
693
|
+
} else {
|
|
694
|
+
showToast('Nothing to cancel', 'warning');
|
|
695
|
+
}
|
|
696
|
+
} catch (error) {
|
|
697
|
+
console.error('Error cancelling autofocus:', error);
|
|
698
|
+
showToast('Failed to cancel autofocus', 'error');
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Request autofocus
|
|
552
704
|
button.disabled = true;
|
|
553
|
-
buttonText.textContent = 'Running Autofocus...';
|
|
554
705
|
buttonSpinner.style.display = 'inline-block';
|
|
555
706
|
|
|
556
707
|
try {
|
|
@@ -561,24 +712,78 @@ async function triggerAutofocus() {
|
|
|
561
712
|
const data = await response.json();
|
|
562
713
|
|
|
563
714
|
if (response.ok) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await loadFilterConfig();
|
|
715
|
+
showToast('Autofocus queued', 'success');
|
|
716
|
+
updateAutofocusButton(true);
|
|
567
717
|
} else {
|
|
568
|
-
|
|
569
|
-
showConfigError(data.error || 'Autofocus failed');
|
|
718
|
+
showToast(data.error || 'Autofocus request failed', 'error');
|
|
570
719
|
}
|
|
571
720
|
} catch (error) {
|
|
572
721
|
console.error('Error triggering autofocus:', error);
|
|
573
|
-
|
|
722
|
+
showToast('Failed to trigger autofocus', 'error');
|
|
574
723
|
} finally {
|
|
575
|
-
// Re-enable button
|
|
576
724
|
button.disabled = false;
|
|
577
|
-
buttonText.textContent = 'Run Autofocus';
|
|
578
725
|
buttonSpinner.style.display = 'none';
|
|
579
726
|
}
|
|
580
727
|
}
|
|
581
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');
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Show toast notification
|
|
753
|
+
*/
|
|
754
|
+
function showToast(message, type = 'info') {
|
|
755
|
+
// Use Bootstrap toast if available, otherwise fallback to alert
|
|
756
|
+
const toastContainer = document.getElementById('toastContainer');
|
|
757
|
+
if (!toastContainer) {
|
|
758
|
+
console.log(`Toast (${type}): ${message}`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const toastId = `toast-${Date.now()}`;
|
|
763
|
+
const bgClass = type === 'success' ? 'bg-success' :
|
|
764
|
+
type === 'error' ? 'bg-danger' :
|
|
765
|
+
type === 'warning' ? 'bg-warning' : 'bg-info';
|
|
766
|
+
|
|
767
|
+
const toastHtml = `
|
|
768
|
+
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
769
|
+
<div class="d-flex">
|
|
770
|
+
<div class="toast-body">${message}</div>
|
|
771
|
+
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
`;
|
|
775
|
+
|
|
776
|
+
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
|
|
777
|
+
const toastElement = document.getElementById(toastId);
|
|
778
|
+
const toast = new bootstrap.Toast(toastElement, { delay: 3000 });
|
|
779
|
+
toast.show();
|
|
780
|
+
|
|
781
|
+
// Remove from DOM after hidden
|
|
782
|
+
toastElement.addEventListener('hidden.bs.toast', () => {
|
|
783
|
+
toastElement.remove();
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
582
787
|
/**
|
|
583
788
|
* Initialize filter configuration on page load
|
|
584
789
|
*/
|
|
@@ -166,9 +166,6 @@
|
|
|
166
166
|
</div>
|
|
167
167
|
|
|
168
168
|
<div class="container my-4" id="configSection" style="display: none;">
|
|
169
|
-
<div id="configError" class="alert alert-danger" style="display: none;" role="alert"></div>
|
|
170
|
-
<div id="configSuccess" class="alert alert-success" style="display: none;" role="alert"></div>
|
|
171
|
-
|
|
172
169
|
<form id="configForm">
|
|
173
170
|
<div class="row g-3 mb-3">
|
|
174
171
|
<!-- API Configuration Card -->
|
|
@@ -242,31 +239,66 @@
|
|
|
242
239
|
<!-- Filter Configuration Section (shown when adapter supports filters) -->
|
|
243
240
|
<div id="filterConfigSection" style="display: none; margin-top: 1.5rem;">
|
|
244
241
|
<hr class="border-secondary">
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<
|
|
242
|
+
<h5 class="mb-3">Filter Configuration</h5>
|
|
243
|
+
<p id="filterAdapterChangeMessage" class="text-muted mb-3" style="display: none;">
|
|
244
|
+
Save configuration to edit filter settings for this adapter
|
|
245
|
+
</p>
|
|
246
|
+
<div class="row">
|
|
247
|
+
<!-- Filter List Column -->
|
|
248
|
+
<div class="col-12 col-md-6">
|
|
249
|
+
<h6 class="mb-2">Filters</h6>
|
|
250
|
+
<div id="filterTableContainer">
|
|
251
|
+
<table class="table table-dark table-sm">
|
|
252
|
+
<thead>
|
|
253
|
+
<tr>
|
|
254
|
+
<th>Enabled</th>
|
|
255
|
+
<th>Name</th>
|
|
256
|
+
<th>Focus Position</th>
|
|
257
|
+
</tr>
|
|
258
|
+
</thead>
|
|
259
|
+
<tbody id="filterTableBody">
|
|
260
|
+
<!-- Filter rows will be populated by JavaScript -->
|
|
261
|
+
</tbody>
|
|
262
|
+
</table>
|
|
263
|
+
<div id="noFiltersMessage" class="text-muted small" style="display: none;">
|
|
264
|
+
No filters configured. Connect to hardware to discover filters.
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
253
267
|
</div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
</
|
|
263
|
-
</
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
|
|
269
|
+
<!-- Autofocus Column -->
|
|
270
|
+
<div class="col-12 col-md-6">
|
|
271
|
+
<h6 class="mb-2">Autofocus</h6>
|
|
272
|
+
<div class="mb-3">
|
|
273
|
+
<button type="button" class="btn btn-sm btn-outline-primary w-100" id="runAutofocusButton">
|
|
274
|
+
<span id="autofocusButtonText">Run Autofocus</span>
|
|
275
|
+
<span id="autofocusButtonSpinner" class="spinner-border spinner-border-sm ms-2" style="display: none;" role="status"></span>
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
<div class="mb-3">
|
|
279
|
+
<label class="form-label small text-muted">Last Autofocus</label>
|
|
280
|
+
<div id="lastAutofocusDisplay" class="text-light">Never</div>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="mb-3">
|
|
283
|
+
<div class="form-check">
|
|
284
|
+
<input class="form-check-input" type="checkbox" id="scheduled_autofocus_enabled">
|
|
285
|
+
<label class="form-check-label" for="scheduled_autofocus_enabled">
|
|
286
|
+
Enable Scheduled Autofocus
|
|
287
|
+
</label>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="mb-3">
|
|
291
|
+
<label for="autofocus_interval_minutes" class="form-label small">Autofocus Interval</label>
|
|
292
|
+
<select id="autofocus_interval_minutes" class="form-select form-select-sm">
|
|
293
|
+
<option value="30">30 minutes</option>
|
|
294
|
+
<option value="60" selected>60 minutes</option>
|
|
295
|
+
<option value="120">120 minutes (2 hours)</option>
|
|
296
|
+
<option value="180">180 minutes (3 hours)</option>
|
|
297
|
+
</select>
|
|
298
|
+
</div>
|
|
299
|
+
<div id="nextAutofocusDisplay" class="small text-muted" style="display: none;">
|
|
300
|
+
Next autofocus in: <span id="nextAutofocusTime">--</span>
|
|
301
|
+
</div>
|
|
270
302
|
</div>
|
|
271
303
|
</div>
|
|
272
304
|
</div>
|
|
@@ -482,6 +514,9 @@
|
|
|
482
514
|
</div>
|
|
483
515
|
</div>
|
|
484
516
|
|
|
517
|
+
<!-- Toast Container for Notifications -->
|
|
518
|
+
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;" id="toastContainer"></div>
|
|
519
|
+
|
|
485
520
|
<script type="module" src="/static/app.js"></script>
|
|
486
521
|
|
|
487
522
|
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: citrascope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Remotely control a telescope while it polls for tasks, collects and edge processes data, and delivers results and data for further processing.
|
|
5
|
+
Project-URL: Homepage, https://citra.space
|
|
6
|
+
Project-URL: Documentation, https://docs.citra.space/citrascope/
|
|
7
|
+
Project-URL: Repository, https://github.com/citra-space/citrascope
|
|
8
|
+
Project-URL: Issues, https://github.com/citra-space/citrascope/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/citra-space/citrascope/releases
|
|
5
10
|
Author-email: Patrick McDavid <patrick@citra.space>
|
|
6
11
|
License: MIT
|
|
7
12
|
License-File: LICENSE
|
|
13
|
+
Keywords: INDI,KStars,NINA,astronomy,astrophotography,imaging,observatory,remote-telescope,telescope,telescope-control
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
23
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
8
24
|
Requires-Python: <3.13,>=3.10
|
|
9
25
|
Requires-Dist: click
|
|
10
26
|
Requires-Dist: fastapi>=0.104.0
|
|
@@ -51,6 +67,8 @@ Description-Content-Type: text/markdown
|
|
|
51
67
|
# CitraScope
|
|
52
68
|
[](https://github.com/citra-space/citrascope/actions/workflows/pytest.yml) [](https://github.com/citra-space/citrascope/actions/workflows/pypi-publish.yml) [](https://pypi.org/project/citrascope/) [](https://pypi.org/project/citrascope/) [](https://github.com/citra-space/citrascope/blob/main/LICENSE)
|
|
53
69
|
|
|
70
|
+
**[GitHub Repository](https://github.com/citra-space/citrascope)** | **[Documentation](https://docs.citra.space/citrascope/)** | **[Citra.space](https://citra.space)**
|
|
71
|
+
|
|
54
72
|
Remotely control a telescope while it polls for tasks, collects observations, and delivers data for further processing.
|
|
55
73
|
|
|
56
74
|
## Features
|