citrascope 0.6.1__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 +97 -38
- citrascope/hardware/abstract_astro_hardware_adapter.py +144 -8
- 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 +67 -96
- citrascope/hardware/nina_adv_http_adapter.py +81 -64
- citrascope/hardware/nina_adv_http_survey_template.json +4 -4
- citrascope/settings/citrascope_settings.py +25 -0
- citrascope/tasks/runner.py +105 -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 +274 -51
- citrascope/web/static/app.js +379 -36
- citrascope/web/static/config.js +448 -108
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/style.css +39 -0
- citrascope/web/templates/dashboard.html +176 -36
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/METADATA +17 -1
- citrascope-0.8.0.dist-info/RECORD +62 -0
- citrascope-0.6.1.dist-info/RECORD +0 -41
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/WHEEL +0 -0
- {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.6.1.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
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
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
|
+
}
|
|
295
318
|
});
|
|
296
319
|
|
|
297
|
-
//
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
@@ -393,23 +494,225 @@ function updateStatus(status) {
|
|
|
393
494
|
if (status.processing_active !== undefined) {
|
|
394
495
|
updateProcessingState(status.processing_active);
|
|
395
496
|
}
|
|
497
|
+
|
|
498
|
+
// Update automated scheduling state
|
|
499
|
+
if (status.automated_scheduling !== undefined) {
|
|
500
|
+
updateAutomatedSchedulingState(status.automated_scheduling);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Update autofocus status
|
|
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
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function updateAutofocusStatus(status) {
|
|
538
|
+
// Update autofocus button state
|
|
539
|
+
const button = document.getElementById('runAutofocusButton');
|
|
540
|
+
const buttonText = document.getElementById('autofocusButtonText');
|
|
541
|
+
|
|
542
|
+
if (button && buttonText && status.autofocus_requested !== undefined) {
|
|
543
|
+
if (status.autofocus_requested) {
|
|
544
|
+
buttonText.textContent = 'Cancel Autofocus';
|
|
545
|
+
button.dataset.action = 'cancel';
|
|
546
|
+
button.classList.remove('btn-outline-primary');
|
|
547
|
+
button.classList.add('btn-outline-warning');
|
|
548
|
+
} else {
|
|
549
|
+
buttonText.textContent = 'Run Autofocus';
|
|
550
|
+
button.dataset.action = 'request';
|
|
551
|
+
button.classList.remove('btn-outline-warning');
|
|
552
|
+
button.classList.add('btn-outline-primary');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Update last autofocus display
|
|
557
|
+
const lastAutofocusDisplay = document.getElementById('lastAutofocusDisplay');
|
|
558
|
+
if (lastAutofocusDisplay) {
|
|
559
|
+
if (status.last_autofocus_timestamp) {
|
|
560
|
+
const timestamp = status.last_autofocus_timestamp * 1000; // Convert to milliseconds
|
|
561
|
+
const now = Date.now();
|
|
562
|
+
const elapsed = now - timestamp;
|
|
563
|
+
lastAutofocusDisplay.textContent = formatElapsedTime(elapsed);
|
|
564
|
+
} else {
|
|
565
|
+
lastAutofocusDisplay.textContent = 'Never';
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Update next autofocus display
|
|
570
|
+
const nextAutofocusDisplay = document.getElementById('nextAutofocusDisplay');
|
|
571
|
+
const nextAutofocusTime = document.getElementById('nextAutofocusTime');
|
|
572
|
+
|
|
573
|
+
if (nextAutofocusDisplay && nextAutofocusTime) {
|
|
574
|
+
if (status.next_autofocus_minutes !== null && status.next_autofocus_minutes !== undefined) {
|
|
575
|
+
nextAutofocusDisplay.style.display = 'block';
|
|
576
|
+
if (status.next_autofocus_minutes === 0) {
|
|
577
|
+
nextAutofocusTime.textContent = 'now (overdue)';
|
|
578
|
+
} else {
|
|
579
|
+
nextAutofocusTime.textContent = formatMinutes(status.next_autofocus_minutes);
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
nextAutofocusDisplay.style.display = 'none';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
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
|
+
|
|
659
|
+
function formatElapsedTime(milliseconds) {
|
|
660
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
661
|
+
const minutes = Math.floor(seconds / 60);
|
|
662
|
+
const hours = Math.floor(minutes / 60);
|
|
663
|
+
const days = Math.floor(hours / 24);
|
|
664
|
+
|
|
665
|
+
if (days > 0) {
|
|
666
|
+
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
667
|
+
} else if (hours > 0) {
|
|
668
|
+
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
669
|
+
} else if (minutes > 0) {
|
|
670
|
+
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
|
671
|
+
} else {
|
|
672
|
+
return 'just now';
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function formatMinutes(minutes) {
|
|
677
|
+
const hours = Math.floor(minutes / 60);
|
|
678
|
+
const mins = Math.floor(minutes % 60);
|
|
679
|
+
|
|
680
|
+
if (hours > 0) {
|
|
681
|
+
if (mins > 0) {
|
|
682
|
+
return `${hours}h ${mins}m`;
|
|
683
|
+
}
|
|
684
|
+
return `${hours}h`;
|
|
685
|
+
}
|
|
686
|
+
return `${mins}m`;
|
|
396
687
|
}
|
|
397
688
|
|
|
398
689
|
function updateProcessingState(isActive) {
|
|
399
690
|
const statusEl = document.getElementById('processingStatus');
|
|
400
|
-
const
|
|
401
|
-
const icon = document.getElementById('processingButtonIcon');
|
|
691
|
+
const switchEl = document.getElementById('toggleProcessingSwitch');
|
|
402
692
|
|
|
403
|
-
if (!statusEl || !
|
|
693
|
+
if (!statusEl || !switchEl) return;
|
|
404
694
|
|
|
405
695
|
if (isActive) {
|
|
406
|
-
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">
|
|
407
|
-
|
|
408
|
-
button.title = 'Pause task processing';
|
|
696
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">Enabled</span>';
|
|
697
|
+
switchEl.checked = true;
|
|
409
698
|
} else {
|
|
410
|
-
statusEl.innerHTML = '<span class="badge rounded-pill bg-
|
|
411
|
-
|
|
412
|
-
|
|
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;
|
|
413
716
|
}
|
|
414
717
|
}
|
|
415
718
|
|
|
@@ -704,16 +1007,19 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
704
1007
|
loadTasks();
|
|
705
1008
|
loadLogs();
|
|
706
1009
|
|
|
707
|
-
//
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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';
|
|
714
1020
|
|
|
715
1021
|
try {
|
|
716
|
-
|
|
1022
|
+
toggleSwitch.disabled = true;
|
|
717
1023
|
const response = await fetch(endpoint, { method: 'POST' });
|
|
718
1024
|
const result = await response.json();
|
|
719
1025
|
|
|
@@ -722,13 +1028,50 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
722
1028
|
// Show specific error message (e.g., "Cannot resume during autofocus")
|
|
723
1029
|
alert((result.error || 'Failed to toggle task processing') +
|
|
724
1030
|
(response.status === 409 ? '' : ' - Unknown error'));
|
|
1031
|
+
// Revert switch state on error
|
|
1032
|
+
toggleSwitch.checked = !isChecked;
|
|
725
1033
|
}
|
|
726
1034
|
// State will be updated via WebSocket broadcast within 2 seconds
|
|
727
1035
|
} catch (error) {
|
|
728
1036
|
console.error('Error toggling processing:', error);
|
|
729
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;
|
|
730
1073
|
} finally {
|
|
731
|
-
|
|
1074
|
+
automatedSchedulingSwitch.disabled = false;
|
|
732
1075
|
}
|
|
733
1076
|
});
|
|
734
1077
|
}
|