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/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// CitraScope Dashboard - Main Application
|
|
2
2
|
import { connectWebSocket } from './websocket.js';
|
|
3
|
-
import { initConfig, currentConfig, initFilterConfig, setupAutofocusButton } from './config.js';
|
|
3
|
+
import { initConfig, currentConfig, initFilterConfig, setupAutofocusButton, createToast } from './config.js';
|
|
4
4
|
import { getTasks, getLogs } from './api.js';
|
|
5
5
|
|
|
6
6
|
function updateAppUrlLinks() {
|
|
@@ -279,26 +279,53 @@ function initNavigation() {
|
|
|
279
279
|
}
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
const link =
|
|
282
|
+
function navigateToSection(section) {
|
|
283
|
+
const link = nav.querySelector(`a[data-section="${section}"]`);
|
|
284
284
|
if (link) {
|
|
285
|
-
e.preventDefault();
|
|
286
|
-
const section = link.getAttribute('data-section');
|
|
287
285
|
activateNav(link);
|
|
288
286
|
showSection(section);
|
|
287
|
+
window.location.hash = section;
|
|
289
288
|
|
|
290
289
|
// Reload filter config when config section is shown
|
|
291
290
|
if (section === 'config') {
|
|
292
291
|
initFilterConfig();
|
|
293
292
|
}
|
|
294
293
|
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
nav.addEventListener('click', function(e) {
|
|
297
|
+
const link = e.target.closest('a[data-section]');
|
|
298
|
+
if (link) {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
const section = link.getAttribute('data-section');
|
|
301
|
+
navigateToSection(section);
|
|
302
|
+
}
|
|
295
303
|
});
|
|
296
304
|
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
305
|
+
// Handle hash changes (back/forward navigation)
|
|
306
|
+
window.addEventListener('hashchange', function() {
|
|
307
|
+
const hash = window.location.hash.substring(1);
|
|
308
|
+
if (hash && sections[hash]) {
|
|
309
|
+
const link = nav.querySelector(`a[data-section="${hash}"]`);
|
|
310
|
+
if (link) {
|
|
311
|
+
activateNav(link);
|
|
312
|
+
showSection(hash);
|
|
313
|
+
if (hash === 'config') {
|
|
314
|
+
initFilterConfig();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Initialize from hash or default to first nav item
|
|
321
|
+
const hash = window.location.hash.substring(1);
|
|
322
|
+
if (hash && sections[hash]) {
|
|
323
|
+
navigateToSection(hash);
|
|
324
|
+
} else {
|
|
325
|
+
const first = nav.querySelector('a[data-section]');
|
|
326
|
+
if (first) {
|
|
327
|
+
navigateToSection(first.getAttribute('data-section'));
|
|
328
|
+
}
|
|
302
329
|
}
|
|
303
330
|
}
|
|
304
331
|
}
|
|
@@ -335,15 +362,91 @@ function updateWSStatus(connected, reconnectInfo = '') {
|
|
|
335
362
|
}
|
|
336
363
|
}
|
|
337
364
|
|
|
365
|
+
// --- Camera Control ---
|
|
366
|
+
window.showCameraControl = function() {
|
|
367
|
+
const modal = new bootstrap.Modal(document.getElementById('cameraControlModal'));
|
|
368
|
+
|
|
369
|
+
// Populate images directory link from config
|
|
370
|
+
const imagesDirLink = document.getElementById('imagesDirLink');
|
|
371
|
+
if (imagesDirLink && currentConfig.images_dir_path) {
|
|
372
|
+
imagesDirLink.textContent = currentConfig.images_dir_path;
|
|
373
|
+
imagesDirLink.href = `file://${currentConfig.images_dir_path}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Clear previous capture result
|
|
377
|
+
document.getElementById('captureResult').style.display = 'none';
|
|
378
|
+
|
|
379
|
+
modal.show();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
window.captureImage = async function() {
|
|
383
|
+
const captureButton = document.getElementById('captureButton');
|
|
384
|
+
const buttonText = document.getElementById('captureButtonText');
|
|
385
|
+
const spinner = document.getElementById('captureButtonSpinner');
|
|
386
|
+
const exposureDuration = parseFloat(document.getElementById('exposureDuration').value);
|
|
387
|
+
|
|
388
|
+
// Validate exposure duration
|
|
389
|
+
if (isNaN(exposureDuration) || exposureDuration <= 0) {
|
|
390
|
+
createToast('Invalid exposure duration', 'danger', false);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Show loading state
|
|
395
|
+
captureButton.disabled = true;
|
|
396
|
+
spinner.style.display = 'inline-block';
|
|
397
|
+
buttonText.textContent = 'Capturing...';
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const response = await fetch('/api/camera/capture', {
|
|
401
|
+
method: 'POST',
|
|
402
|
+
headers: {
|
|
403
|
+
'Content-Type': 'application/json'
|
|
404
|
+
},
|
|
405
|
+
body: JSON.stringify({ duration: exposureDuration })
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const data = await response.json();
|
|
409
|
+
|
|
410
|
+
if (response.ok && data.success) {
|
|
411
|
+
// Show capture result
|
|
412
|
+
document.getElementById('captureFilename').textContent = data.filename;
|
|
413
|
+
document.getElementById('captureFormat').textContent = data.format || 'Unknown';
|
|
414
|
+
document.getElementById('captureResult').style.display = 'block';
|
|
415
|
+
|
|
416
|
+
createToast('Image captured successfully', 'success', true);
|
|
417
|
+
} else {
|
|
418
|
+
const errorMsg = data.error || 'Failed to capture image';
|
|
419
|
+
createToast(errorMsg, 'danger', false);
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('Capture error:', error);
|
|
423
|
+
createToast('Failed to capture image: ' + error.message, 'danger', false);
|
|
424
|
+
} finally {
|
|
425
|
+
// Reset button state
|
|
426
|
+
captureButton.disabled = false;
|
|
427
|
+
spinner.style.display = 'none';
|
|
428
|
+
buttonText.textContent = 'Capture';
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
338
432
|
// --- Status Updates ---
|
|
339
433
|
function updateStatus(status) {
|
|
340
434
|
document.getElementById('hardwareAdapter').textContent = status.hardware_adapter || '-';
|
|
341
435
|
document.getElementById('telescopeConnected').innerHTML = status.telescope_connected
|
|
342
436
|
? '<span class="badge rounded-pill bg-success">Connected</span>'
|
|
343
437
|
: '<span class="badge rounded-pill bg-danger">Disconnected</span>';
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
438
|
+
// Update camera status with control button when connected
|
|
439
|
+
const cameraEl = document.getElementById('cameraConnected');
|
|
440
|
+
if (status.camera_connected) {
|
|
441
|
+
cameraEl.innerHTML = `
|
|
442
|
+
<span class="badge rounded-pill bg-success">Connected</span>
|
|
443
|
+
<button class="btn btn-sm btn-outline-light" onclick="showCameraControl()">
|
|
444
|
+
<i class="bi bi-camera"></i> Control
|
|
445
|
+
</button>
|
|
446
|
+
`;
|
|
447
|
+
} else {
|
|
448
|
+
cameraEl.innerHTML = '<span class="badge rounded-pill bg-danger">Disconnected</span>';
|
|
449
|
+
}
|
|
347
450
|
|
|
348
451
|
// Update current task display
|
|
349
452
|
if (status.current_task && status.current_task !== 'None') {
|
|
@@ -363,11 +466,9 @@ function updateStatus(status) {
|
|
|
363
466
|
document.getElementById('tasksPending').textContent = status.tasks_pending || '0';
|
|
364
467
|
}
|
|
365
468
|
|
|
366
|
-
if (status.telescope_ra !== null) {
|
|
367
|
-
document.getElementById('
|
|
368
|
-
|
|
369
|
-
if (status.telescope_dec !== null) {
|
|
370
|
-
document.getElementById('telescopeDEC').textContent = status.telescope_dec.toFixed(4) + '°';
|
|
469
|
+
if (status.telescope_ra !== null && status.telescope_dec !== null) {
|
|
470
|
+
document.getElementById('telescopeCoords').textContent =
|
|
471
|
+
status.telescope_ra.toFixed(3) + '° / ' + status.telescope_dec.toFixed(3) + '°';
|
|
371
472
|
}
|
|
372
473
|
|
|
373
474
|
// Update ground station information
|
|
@@ -394,8 +495,43 @@ function updateStatus(status) {
|
|
|
394
495
|
updateProcessingState(status.processing_active);
|
|
395
496
|
}
|
|
396
497
|
|
|
498
|
+
// Update automated scheduling state
|
|
499
|
+
if (status.automated_scheduling !== undefined) {
|
|
500
|
+
updateAutomatedSchedulingState(status.automated_scheduling);
|
|
501
|
+
}
|
|
502
|
+
|
|
397
503
|
// Update autofocus status
|
|
398
504
|
updateAutofocusStatus(status);
|
|
505
|
+
|
|
506
|
+
// Update time sync status
|
|
507
|
+
updateTimeSyncStatus(status);
|
|
508
|
+
|
|
509
|
+
// Update missing dependencies display
|
|
510
|
+
updateMissingDependencies(status.missing_dependencies || []);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function updateMissingDependencies(missingDeps) {
|
|
514
|
+
const dashboardContainer = document.getElementById('missingDependenciesAlert');
|
|
515
|
+
const configContainer = document.getElementById('configMissingDependenciesAlert');
|
|
516
|
+
|
|
517
|
+
const containers = [dashboardContainer, configContainer].filter(c => c);
|
|
518
|
+
|
|
519
|
+
if (missingDeps.length === 0) {
|
|
520
|
+
containers.forEach(container => container.style.display = 'none');
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
let html = '<strong><i class="bi bi-exclamation-triangle-fill me-2"></i>Missing Dependencies:</strong><ul class="mb-0 mt-2">';
|
|
525
|
+
missingDeps.forEach(dep => {
|
|
526
|
+
html += `<li><strong>${dep.device_name}</strong>: Missing ${dep.missing_packages}<br>`;
|
|
527
|
+
html += `<code class="small">${dep.install_cmd}</code></li>`;
|
|
528
|
+
});
|
|
529
|
+
html += '</ul>';
|
|
530
|
+
|
|
531
|
+
containers.forEach(container => {
|
|
532
|
+
container.innerHTML = html;
|
|
533
|
+
container.style.display = 'block';
|
|
534
|
+
});
|
|
399
535
|
}
|
|
400
536
|
|
|
401
537
|
function updateAutofocusStatus(status) {
|
|
@@ -448,6 +584,78 @@ function updateAutofocusStatus(status) {
|
|
|
448
584
|
}
|
|
449
585
|
}
|
|
450
586
|
|
|
587
|
+
function updateTimeSyncStatus(status) {
|
|
588
|
+
const statusEl = document.getElementById('timeSyncStatus');
|
|
589
|
+
const offsetEl = document.getElementById('timeOffsetDisplay');
|
|
590
|
+
|
|
591
|
+
if (!statusEl || !offsetEl) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const timeHealth = status.time_health;
|
|
596
|
+
|
|
597
|
+
// Handle missing health data
|
|
598
|
+
if (!timeHealth) {
|
|
599
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-secondary" data-bs-toggle="tooltip" title="Time sync status unknown">Unknown</span>';
|
|
600
|
+
offsetEl.textContent = '-';
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Determine badge color based on status
|
|
605
|
+
let badgeClass = 'bg-secondary';
|
|
606
|
+
let badgeText = 'Unknown';
|
|
607
|
+
let tooltipText = 'Time sync status unknown';
|
|
608
|
+
|
|
609
|
+
switch (timeHealth.status) {
|
|
610
|
+
case 'ok':
|
|
611
|
+
badgeClass = 'bg-success';
|
|
612
|
+
badgeText = 'OK';
|
|
613
|
+
tooltipText = 'Time sync within threshold';
|
|
614
|
+
break;
|
|
615
|
+
case 'critical':
|
|
616
|
+
badgeClass = 'bg-danger';
|
|
617
|
+
badgeText = 'Paused';
|
|
618
|
+
tooltipText = 'Time drift exceeded threshold - tasks paused';
|
|
619
|
+
break;
|
|
620
|
+
case 'unknown':
|
|
621
|
+
default:
|
|
622
|
+
badgeClass = 'bg-secondary';
|
|
623
|
+
badgeText = 'Unknown';
|
|
624
|
+
tooltipText = 'Unable to check time sync';
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
statusEl.innerHTML = `<span class="badge rounded-pill ${badgeClass}" data-bs-toggle="tooltip" title="${tooltipText}">${badgeText}</span>`;
|
|
629
|
+
|
|
630
|
+
// Format offset display
|
|
631
|
+
if (timeHealth.offset_ms !== null && timeHealth.offset_ms !== undefined) {
|
|
632
|
+
const offset = timeHealth.offset_ms;
|
|
633
|
+
const absOffset = Math.abs(offset);
|
|
634
|
+
const sign = offset >= 0 ? '+' : '-';
|
|
635
|
+
|
|
636
|
+
if (absOffset < 1) {
|
|
637
|
+
offsetEl.textContent = `${sign}${absOffset.toFixed(2)}ms`;
|
|
638
|
+
} else if (absOffset < 1000) {
|
|
639
|
+
offsetEl.textContent = `${sign}${absOffset.toFixed(0)}ms`;
|
|
640
|
+
} else {
|
|
641
|
+
offsetEl.textContent = `${sign}${(absOffset / 1000).toFixed(2)}s`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Add source indicator
|
|
645
|
+
if (timeHealth.source && timeHealth.source !== 'unknown') {
|
|
646
|
+
offsetEl.textContent += ` (${timeHealth.source})`;
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
offsetEl.textContent = '-';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Reinitialize tooltips
|
|
653
|
+
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
654
|
+
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
655
|
+
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
451
659
|
function formatElapsedTime(milliseconds) {
|
|
452
660
|
const seconds = Math.floor(milliseconds / 1000);
|
|
453
661
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -480,19 +688,31 @@ function formatMinutes(minutes) {
|
|
|
480
688
|
|
|
481
689
|
function updateProcessingState(isActive) {
|
|
482
690
|
const statusEl = document.getElementById('processingStatus');
|
|
483
|
-
const
|
|
484
|
-
const icon = document.getElementById('processingButtonIcon');
|
|
691
|
+
const switchEl = document.getElementById('toggleProcessingSwitch');
|
|
485
692
|
|
|
486
|
-
if (!statusEl || !
|
|
693
|
+
if (!statusEl || !switchEl) return;
|
|
487
694
|
|
|
488
695
|
if (isActive) {
|
|
489
|
-
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">
|
|
490
|
-
|
|
491
|
-
button.title = 'Pause task processing';
|
|
696
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">Enabled</span>';
|
|
697
|
+
switchEl.checked = true;
|
|
492
698
|
} else {
|
|
493
|
-
statusEl.innerHTML = '<span class="badge rounded-pill bg-
|
|
494
|
-
|
|
495
|
-
|
|
699
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-secondary">Disabled</span>';
|
|
700
|
+
switchEl.checked = false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function updateAutomatedSchedulingState(isEnabled) {
|
|
705
|
+
const statusEl = document.getElementById('automatedSchedulingStatus');
|
|
706
|
+
const switchEl = document.getElementById('toggleAutomatedSchedulingSwitch');
|
|
707
|
+
|
|
708
|
+
if (!statusEl || !switchEl) return;
|
|
709
|
+
|
|
710
|
+
if (isEnabled) {
|
|
711
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">Enabled</span>';
|
|
712
|
+
switchEl.checked = true;
|
|
713
|
+
} else {
|
|
714
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-secondary">Disabled</span>';
|
|
715
|
+
switchEl.checked = false;
|
|
496
716
|
}
|
|
497
717
|
}
|
|
498
718
|
|
|
@@ -787,16 +1007,19 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
787
1007
|
loadTasks();
|
|
788
1008
|
loadLogs();
|
|
789
1009
|
|
|
790
|
-
//
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1010
|
+
// Initialize Bootstrap tooltips
|
|
1011
|
+
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
1012
|
+
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
|
1013
|
+
|
|
1014
|
+
// Add pause/resume switch handler
|
|
1015
|
+
const toggleSwitch = document.getElementById('toggleProcessingSwitch');
|
|
1016
|
+
if (toggleSwitch) {
|
|
1017
|
+
toggleSwitch.addEventListener('change', async (e) => {
|
|
1018
|
+
const isChecked = e.target.checked;
|
|
1019
|
+
const endpoint = isChecked ? '/api/tasks/resume' : '/api/tasks/pause';
|
|
797
1020
|
|
|
798
1021
|
try {
|
|
799
|
-
|
|
1022
|
+
toggleSwitch.disabled = true;
|
|
800
1023
|
const response = await fetch(endpoint, { method: 'POST' });
|
|
801
1024
|
const result = await response.json();
|
|
802
1025
|
|
|
@@ -805,13 +1028,50 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
805
1028
|
// Show specific error message (e.g., "Cannot resume during autofocus")
|
|
806
1029
|
alert((result.error || 'Failed to toggle task processing') +
|
|
807
1030
|
(response.status === 409 ? '' : ' - Unknown error'));
|
|
1031
|
+
// Revert switch state on error
|
|
1032
|
+
toggleSwitch.checked = !isChecked;
|
|
808
1033
|
}
|
|
809
1034
|
// State will be updated via WebSocket broadcast within 2 seconds
|
|
810
1035
|
} catch (error) {
|
|
811
1036
|
console.error('Error toggling processing:', error);
|
|
812
1037
|
alert('Error toggling task processing');
|
|
1038
|
+
// Revert switch state on error
|
|
1039
|
+
toggleSwitch.checked = !isChecked;
|
|
1040
|
+
} finally {
|
|
1041
|
+
toggleSwitch.disabled = false;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Add automated scheduling switch handler
|
|
1047
|
+
const automatedSchedulingSwitch = document.getElementById('toggleAutomatedSchedulingSwitch');
|
|
1048
|
+
if (automatedSchedulingSwitch) {
|
|
1049
|
+
automatedSchedulingSwitch.addEventListener('change', async (e) => {
|
|
1050
|
+
const isChecked = e.target.checked;
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
automatedSchedulingSwitch.disabled = true;
|
|
1054
|
+
const response = await fetch('/api/telescope/automated-scheduling', {
|
|
1055
|
+
method: 'PATCH',
|
|
1056
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1057
|
+
body: JSON.stringify({ enabled: isChecked })
|
|
1058
|
+
});
|
|
1059
|
+
const result = await response.json();
|
|
1060
|
+
|
|
1061
|
+
if (!response.ok) {
|
|
1062
|
+
console.error('Failed to toggle automated scheduling:', result);
|
|
1063
|
+
alert(result.error || 'Failed to toggle automated scheduling');
|
|
1064
|
+
// Revert switch state on error
|
|
1065
|
+
automatedSchedulingSwitch.checked = !isChecked;
|
|
1066
|
+
}
|
|
1067
|
+
// State will be updated via WebSocket broadcast
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
console.error('Error toggling automated scheduling:', error);
|
|
1070
|
+
alert('Error toggling automated scheduling');
|
|
1071
|
+
// Revert switch state on error
|
|
1072
|
+
automatedSchedulingSwitch.checked = !isChecked;
|
|
813
1073
|
} finally {
|
|
814
|
-
|
|
1074
|
+
automatedSchedulingSwitch.disabled = false;
|
|
815
1075
|
}
|
|
816
1076
|
});
|
|
817
1077
|
}
|