citrascope 0.4.0__py3-none-any.whl → 0.5.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 +81 -0
- citrascope/hardware/abstract_astro_hardware_adapter.py +61 -0
- citrascope/hardware/nina_adv_http_adapter.py +104 -75
- citrascope/settings/citrascope_settings.py +21 -27
- citrascope/tasks/runner.py +30 -0
- citrascope/web/app.py +120 -5
- citrascope/web/static/app.js +243 -16
- citrascope/web/static/config.js +186 -23
- citrascope/web/templates/dashboard.html +99 -16
- {citrascope-0.4.0.dist-info → citrascope-0.5.0.dist-info}/METADATA +1 -1
- {citrascope-0.4.0.dist-info → citrascope-0.5.0.dist-info}/RECORD +13 -13
- {citrascope-0.4.0.dist-info → citrascope-0.5.0.dist-info}/WHEEL +0 -0
- {citrascope-0.4.0.dist-info → citrascope-0.5.0.dist-info}/entry_points.txt +0 -0
citrascope/web/app.py
CHANGED
|
@@ -30,6 +30,7 @@ class SystemStatus(BaseModel):
|
|
|
30
30
|
camera_connected: bool = False
|
|
31
31
|
current_task: Optional[str] = None
|
|
32
32
|
tasks_pending: int = 0
|
|
33
|
+
processing_active: bool = True
|
|
33
34
|
hardware_adapter: str = "unknown"
|
|
34
35
|
telescope_ra: Optional[float] = None
|
|
35
36
|
telescope_dec: Optional[float] = None
|
|
@@ -240,6 +241,13 @@ class CitraScopeWebApp:
|
|
|
240
241
|
if not self.daemon:
|
|
241
242
|
return JSONResponse({"error": "Daemon not available"}, status_code=503)
|
|
242
243
|
|
|
244
|
+
# Block config changes during autofocus
|
|
245
|
+
if self.daemon.is_autofocus_in_progress():
|
|
246
|
+
return JSONResponse(
|
|
247
|
+
{"error": "Cannot save configuration while autofocus is running. Check logs for progress."},
|
|
248
|
+
status_code=409,
|
|
249
|
+
)
|
|
250
|
+
|
|
243
251
|
# Validate required fields
|
|
244
252
|
required_fields = ["personal_access_token", "telescope_id", "hardware_adapter"]
|
|
245
253
|
for field in required_fields:
|
|
@@ -310,11 +318,7 @@ class CitraScopeWebApp:
|
|
|
310
318
|
status_code=400,
|
|
311
319
|
)
|
|
312
320
|
|
|
313
|
-
|
|
314
|
-
from citrascope.settings.settings_file_manager import SettingsFileManager
|
|
315
|
-
|
|
316
|
-
config_manager = SettingsFileManager()
|
|
317
|
-
config_manager.save_config(config)
|
|
321
|
+
self.daemon.settings.update_and_save(config)
|
|
318
322
|
|
|
319
323
|
# Trigger hot-reload
|
|
320
324
|
success, error = self.daemon.reload_configuration()
|
|
@@ -371,6 +375,113 @@ class CitraScopeWebApp:
|
|
|
371
375
|
return {"logs": logs}
|
|
372
376
|
return {"logs": []}
|
|
373
377
|
|
|
378
|
+
@self.app.post("/api/tasks/pause")
|
|
379
|
+
async def pause_tasks():
|
|
380
|
+
"""Pause task processing."""
|
|
381
|
+
if not self.daemon or not self.daemon.task_manager:
|
|
382
|
+
return JSONResponse({"error": "Task manager not available"}, status_code=503)
|
|
383
|
+
|
|
384
|
+
self.daemon.task_manager.pause()
|
|
385
|
+
await self.broadcast_status()
|
|
386
|
+
|
|
387
|
+
return {"status": "paused", "message": "Task processing paused"}
|
|
388
|
+
|
|
389
|
+
@self.app.post("/api/tasks/resume")
|
|
390
|
+
async def resume_tasks():
|
|
391
|
+
"""Resume task processing."""
|
|
392
|
+
if not self.daemon or not self.daemon.task_manager:
|
|
393
|
+
return JSONResponse({"error": "Task manager not available"}, status_code=503)
|
|
394
|
+
|
|
395
|
+
# Block resume during autofocus
|
|
396
|
+
if self.daemon.is_autofocus_in_progress():
|
|
397
|
+
return JSONResponse(
|
|
398
|
+
{"error": "Cannot resume task processing during autofocus. Check logs for progress."},
|
|
399
|
+
status_code=409,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
self.daemon.task_manager.resume()
|
|
403
|
+
await self.broadcast_status()
|
|
404
|
+
|
|
405
|
+
return {"status": "active", "message": "Task processing resumed"}
|
|
406
|
+
|
|
407
|
+
@self.app.get("/api/adapter/filters")
|
|
408
|
+
async def get_filters():
|
|
409
|
+
"""Get current filter configuration."""
|
|
410
|
+
if not self.daemon or not self.daemon.hardware_adapter:
|
|
411
|
+
return JSONResponse({"error": "Hardware adapter not available"}, status_code=503)
|
|
412
|
+
|
|
413
|
+
if not self.daemon.hardware_adapter.supports_filter_management():
|
|
414
|
+
return JSONResponse({"error": "Adapter does not support filter management"}, status_code=404)
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
filter_config = self.daemon.hardware_adapter.get_filter_config()
|
|
418
|
+
return {"filters": filter_config}
|
|
419
|
+
except Exception as e:
|
|
420
|
+
CITRASCOPE_LOGGER.error(f"Error getting filter config: {e}", exc_info=True)
|
|
421
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
422
|
+
|
|
423
|
+
@self.app.patch("/api/adapter/filters/{filter_id}")
|
|
424
|
+
async def update_filter(filter_id: str, update: dict):
|
|
425
|
+
"""Update focus position for a specific filter."""
|
|
426
|
+
if not self.daemon or not self.daemon.hardware_adapter:
|
|
427
|
+
return JSONResponse({"error": "Hardware adapter not available"}, status_code=503)
|
|
428
|
+
|
|
429
|
+
if not self.daemon.hardware_adapter.supports_filter_management():
|
|
430
|
+
return JSONResponse({"error": "Adapter does not support filter management"}, status_code=404)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
# Validate focus_position is provided and is a valid integer within hardware limits
|
|
434
|
+
if "focus_position" not in update:
|
|
435
|
+
return JSONResponse({"error": "focus_position is required"}, status_code=400)
|
|
436
|
+
|
|
437
|
+
focus_position = update["focus_position"]
|
|
438
|
+
if not isinstance(focus_position, int):
|
|
439
|
+
return JSONResponse({"error": "focus_position must be an integer"}, status_code=400)
|
|
440
|
+
|
|
441
|
+
# Typical focuser range is 0-65535 (16-bit unsigned)
|
|
442
|
+
if focus_position < 0 or focus_position > 65535:
|
|
443
|
+
return JSONResponse({"error": "focus_position must be between 0 and 65535"}, status_code=400)
|
|
444
|
+
|
|
445
|
+
# Get current filter config
|
|
446
|
+
filter_config = self.daemon.hardware_adapter.get_filter_config()
|
|
447
|
+
if filter_id not in filter_config:
|
|
448
|
+
return JSONResponse({"error": f"Filter {filter_id} not found"}, status_code=404)
|
|
449
|
+
|
|
450
|
+
# Update via adapter interface
|
|
451
|
+
if not self.daemon.hardware_adapter.update_filter_focus(filter_id, focus_position):
|
|
452
|
+
return JSONResponse({"error": "Failed to update filter in adapter"}, status_code=500)
|
|
453
|
+
|
|
454
|
+
# Save to settings
|
|
455
|
+
self.daemon._save_filter_config()
|
|
456
|
+
|
|
457
|
+
return {"success": True, "filter_id": filter_id, "focus_position": focus_position}
|
|
458
|
+
except ValueError:
|
|
459
|
+
return JSONResponse({"error": "Invalid filter_id format"}, status_code=400)
|
|
460
|
+
except Exception as e:
|
|
461
|
+
CITRASCOPE_LOGGER.error(f"Error updating filter: {e}", exc_info=True)
|
|
462
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
463
|
+
|
|
464
|
+
@self.app.post("/api/adapter/autofocus")
|
|
465
|
+
async def trigger_autofocus():
|
|
466
|
+
"""Trigger autofocus routine."""
|
|
467
|
+
if not self.daemon:
|
|
468
|
+
return JSONResponse({"error": "Daemon not available"}, status_code=503)
|
|
469
|
+
|
|
470
|
+
if not self.daemon.hardware_adapter or not self.daemon.hardware_adapter.supports_filter_management():
|
|
471
|
+
return JSONResponse({"error": "Filter management not supported"}, status_code=404)
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Run autofocus in a separate thread to avoid blocking the async event loop
|
|
475
|
+
# Autofocus can take several minutes (slewing + focusing multiple filters)
|
|
476
|
+
success, error = await asyncio.to_thread(self.daemon.trigger_autofocus)
|
|
477
|
+
if success:
|
|
478
|
+
return {"success": True, "message": "Autofocus completed successfully"}
|
|
479
|
+
else:
|
|
480
|
+
return JSONResponse({"error": error}, status_code=500)
|
|
481
|
+
except Exception as e:
|
|
482
|
+
CITRASCOPE_LOGGER.error(f"Error triggering autofocus: {e}", exc_info=True)
|
|
483
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
484
|
+
|
|
374
485
|
@self.app.websocket("/ws")
|
|
375
486
|
async def websocket_endpoint(websocket: WebSocket):
|
|
376
487
|
"""WebSocket endpoint for real-time updates."""
|
|
@@ -439,6 +550,10 @@ class CitraScopeWebApp:
|
|
|
439
550
|
self.status.ground_station_name = gs_name
|
|
440
551
|
self.status.ground_station_url = f"{base_url}/ground-stations/{gs_id}" if gs_id else None
|
|
441
552
|
|
|
553
|
+
# Update task processing state
|
|
554
|
+
if hasattr(self.daemon, "task_manager") and self.daemon.task_manager:
|
|
555
|
+
self.status.processing_active = self.daemon.task_manager.is_processing_active()
|
|
556
|
+
|
|
442
557
|
self.status.last_update = datetime.now().isoformat()
|
|
443
558
|
|
|
444
559
|
except Exception as e:
|
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 } from './config.js';
|
|
3
|
+
import { initConfig, currentConfig, initFilterConfig, setupAutofocusButton } from './config.js';
|
|
4
4
|
import { getTasks, getLogs } from './api.js';
|
|
5
5
|
|
|
6
6
|
function updateAppUrlLinks() {
|
|
@@ -13,7 +13,154 @@ function updateAppUrlLinks() {
|
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// --- Version Checking ---
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare two semantic version strings
|
|
20
|
+
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
21
|
+
*/
|
|
22
|
+
function compareVersions(v1, v2) {
|
|
23
|
+
// Strip 'v' prefix if present
|
|
24
|
+
v1 = v1.replace(/^v/, '');
|
|
25
|
+
v2 = v2.replace(/^v/, '');
|
|
26
|
+
|
|
27
|
+
const parts1 = v1.split('.').map(n => parseInt(n) || 0);
|
|
28
|
+
const parts2 = v2.split('.').map(n => parseInt(n) || 0);
|
|
29
|
+
|
|
30
|
+
const maxLen = Math.max(parts1.length, parts2.length);
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < maxLen; i++) {
|
|
33
|
+
const num1 = parts1[i] || 0;
|
|
34
|
+
const num2 = parts2[i] || 0;
|
|
35
|
+
|
|
36
|
+
if (num1 > num2) return 1;
|
|
37
|
+
if (num1 < num2) return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetch and display current version
|
|
45
|
+
*/
|
|
46
|
+
async function fetchVersion() {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch('/api/version');
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
|
|
51
|
+
// Update header version
|
|
52
|
+
const headerVersionEl = document.getElementById('headerVersion');
|
|
53
|
+
|
|
54
|
+
if (headerVersionEl && data.version) {
|
|
55
|
+
// Show "dev" for development, "v" prefix for releases
|
|
56
|
+
if (data.version === 'development') {
|
|
57
|
+
headerVersionEl.textContent = 'dev';
|
|
58
|
+
} else {
|
|
59
|
+
headerVersionEl.textContent = 'v' + data.version;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error fetching version:', error);
|
|
64
|
+
const headerVersionEl = document.getElementById('headerVersion');
|
|
65
|
+
|
|
66
|
+
if (headerVersionEl) {
|
|
67
|
+
headerVersionEl.textContent = 'v?';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check for available updates from GitHub
|
|
74
|
+
* Returns the check result for modal display
|
|
75
|
+
*/
|
|
76
|
+
async function checkForUpdates() {
|
|
77
|
+
try {
|
|
78
|
+
// Get current version
|
|
79
|
+
const versionResponse = await fetch('/api/version');
|
|
80
|
+
const versionData = await versionResponse.json();
|
|
81
|
+
const currentVersion = versionData.version;
|
|
82
|
+
|
|
83
|
+
// Check GitHub for latest release
|
|
84
|
+
const githubResponse = await fetch('https://api.github.com/repos/citra-space/citrascope/releases/latest');
|
|
85
|
+
if (!githubResponse.ok) {
|
|
86
|
+
return { status: 'error', currentVersion };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const releaseData = await githubResponse.json();
|
|
90
|
+
const latestVersion = releaseData.tag_name.replace(/^v/, '');
|
|
91
|
+
const releaseUrl = releaseData.html_url;
|
|
92
|
+
|
|
93
|
+
// Skip comparison for development versions
|
|
94
|
+
if (currentVersion === 'development' || currentVersion === 'unknown') {
|
|
95
|
+
return { status: 'up-to-date', currentVersion };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Compare versions
|
|
99
|
+
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
100
|
+
// Update available - show indicator badge with version
|
|
101
|
+
const indicator = document.getElementById('updateIndicator');
|
|
102
|
+
if (indicator) {
|
|
103
|
+
indicator.textContent = `${latestVersion} Available!`;
|
|
104
|
+
indicator.style.display = 'inline-block';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
status: 'update-available',
|
|
109
|
+
currentVersion,
|
|
110
|
+
latestVersion,
|
|
111
|
+
releaseUrl
|
|
112
|
+
};
|
|
113
|
+
} else {
|
|
114
|
+
// Up to date - hide indicator badge
|
|
115
|
+
const indicator = document.getElementById('updateIndicator');
|
|
116
|
+
if (indicator) {
|
|
117
|
+
indicator.style.display = 'none';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { status: 'up-to-date', currentVersion };
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Network error
|
|
124
|
+
console.debug('Update check failed:', error);
|
|
125
|
+
return { status: 'error', currentVersion: 'unknown' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Show version check modal with results
|
|
131
|
+
*/
|
|
132
|
+
async function showVersionModal() {
|
|
133
|
+
const modal = new bootstrap.Modal(document.getElementById('versionModal'));
|
|
134
|
+
modal.show();
|
|
135
|
+
|
|
136
|
+
// Show loading state
|
|
137
|
+
document.getElementById('versionCheckLoading').style.display = 'block';
|
|
138
|
+
document.getElementById('versionCheckUpToDate').style.display = 'none';
|
|
139
|
+
document.getElementById('versionCheckUpdateAvailable').style.display = 'none';
|
|
140
|
+
document.getElementById('versionCheckError').style.display = 'none';
|
|
141
|
+
|
|
142
|
+
// Perform check
|
|
143
|
+
const result = await checkForUpdates();
|
|
144
|
+
|
|
145
|
+
// Hide loading
|
|
146
|
+
document.getElementById('versionCheckLoading').style.display = 'none';
|
|
147
|
+
|
|
148
|
+
// Show appropriate result
|
|
149
|
+
if (result.status === 'update-available') {
|
|
150
|
+
document.getElementById('modalCurrentVersion').textContent = 'v' + result.currentVersion;
|
|
151
|
+
document.getElementById('modalLatestVersion').textContent = 'v' + result.latestVersion;
|
|
152
|
+
document.getElementById('releaseNotesLink').href = result.releaseUrl;
|
|
153
|
+
document.getElementById('versionCheckUpdateAvailable').style.display = 'block';
|
|
154
|
+
} else if (result.status === 'up-to-date') {
|
|
155
|
+
document.getElementById('modalCurrentVersionUpToDate').textContent = result.currentVersion === 'development' ? 'development' : 'v' + result.currentVersion;
|
|
156
|
+
document.getElementById('versionCheckUpToDate').style.display = 'block';
|
|
157
|
+
} else {
|
|
158
|
+
document.getElementById('modalCurrentVersionError').textContent = result.currentVersion === 'development' ? 'development' : result.currentVersion;
|
|
159
|
+
document.getElementById('versionCheckError').style.display = 'block';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Task Management ---
|
|
17
164
|
let nextTaskStartTime = null;
|
|
18
165
|
let countdownInterval = null;
|
|
19
166
|
let isTaskActive = false;
|
|
@@ -139,6 +286,11 @@ function initNavigation() {
|
|
|
139
286
|
const section = link.getAttribute('data-section');
|
|
140
287
|
activateNav(link);
|
|
141
288
|
showSection(section);
|
|
289
|
+
|
|
290
|
+
// Reload filter config when config section is shown
|
|
291
|
+
if (section === 'config') {
|
|
292
|
+
initFilterConfig();
|
|
293
|
+
}
|
|
142
294
|
}
|
|
143
295
|
});
|
|
144
296
|
|
|
@@ -207,7 +359,9 @@ function updateStatus(status) {
|
|
|
207
359
|
}
|
|
208
360
|
// If isTaskActive is already false, don't touch the display (countdown is updating it)
|
|
209
361
|
|
|
210
|
-
|
|
362
|
+
if (status.tasks_pending !== undefined) {
|
|
363
|
+
document.getElementById('tasksPending').textContent = status.tasks_pending || '0';
|
|
364
|
+
}
|
|
211
365
|
|
|
212
366
|
if (status.telescope_ra !== null) {
|
|
213
367
|
document.getElementById('telescopeRA').textContent = status.telescope_ra.toFixed(4) + '°';
|
|
@@ -217,20 +371,45 @@ function updateStatus(status) {
|
|
|
217
371
|
}
|
|
218
372
|
|
|
219
373
|
// Update ground station information
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
374
|
+
if (status.ground_station_name !== undefined || status.ground_station_url !== undefined) {
|
|
375
|
+
const gsNameEl = document.getElementById('groundStationName');
|
|
376
|
+
const taskScopeButton = document.getElementById('taskScopeButton');
|
|
377
|
+
|
|
378
|
+
if (status.ground_station_name && status.ground_station_url) {
|
|
379
|
+
gsNameEl.innerHTML = `<a href="${status.ground_station_url}" target="_blank" class="ground-station-link">${status.ground_station_name} ↗</a>`;
|
|
380
|
+
// Update the Task My Scope button
|
|
381
|
+
taskScopeButton.href = status.ground_station_url;
|
|
382
|
+
taskScopeButton.style.display = 'inline-block';
|
|
383
|
+
} else if (status.ground_station_name) {
|
|
384
|
+
gsNameEl.textContent = status.ground_station_name;
|
|
385
|
+
taskScopeButton.style.display = 'none';
|
|
386
|
+
} else {
|
|
387
|
+
gsNameEl.textContent = '-';
|
|
388
|
+
taskScopeButton.style.display = 'none';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Update task processing state
|
|
393
|
+
if (status.processing_active !== undefined) {
|
|
394
|
+
updateProcessingState(status.processing_active);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function updateProcessingState(isActive) {
|
|
399
|
+
const statusEl = document.getElementById('processingStatus');
|
|
400
|
+
const button = document.getElementById('toggleProcessingButton');
|
|
401
|
+
const icon = document.getElementById('processingButtonIcon');
|
|
402
|
+
|
|
403
|
+
if (!statusEl || !button || !icon) return;
|
|
404
|
+
|
|
405
|
+
if (isActive) {
|
|
406
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">Active</span>';
|
|
407
|
+
icon.textContent = 'Pause';
|
|
408
|
+
button.title = 'Pause task processing';
|
|
231
409
|
} else {
|
|
232
|
-
|
|
233
|
-
|
|
410
|
+
statusEl.innerHTML = '<span class="badge rounded-pill bg-warning">Paused</span>';
|
|
411
|
+
icon.textContent = 'Resume';
|
|
412
|
+
button.title = 'Resume task processing';
|
|
234
413
|
}
|
|
235
414
|
}
|
|
236
415
|
|
|
@@ -485,9 +664,28 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
485
664
|
// Initialize configuration management (loads config)
|
|
486
665
|
await initConfig();
|
|
487
666
|
|
|
667
|
+
// Initialize filter configuration
|
|
668
|
+
await initFilterConfig();
|
|
669
|
+
|
|
670
|
+
// Setup autofocus button (only once)
|
|
671
|
+
setupAutofocusButton();
|
|
672
|
+
|
|
488
673
|
// Update app URL links from loaded config
|
|
489
674
|
updateAppUrlLinks();
|
|
490
675
|
|
|
676
|
+
// Fetch and display version
|
|
677
|
+
fetchVersion();
|
|
678
|
+
|
|
679
|
+
// Check for updates on load and every hour
|
|
680
|
+
checkForUpdates();
|
|
681
|
+
setInterval(checkForUpdates, 3600000); // Check every hour
|
|
682
|
+
|
|
683
|
+
// Wire up version click to open modal
|
|
684
|
+
const headerVersion = document.getElementById('headerVersion');
|
|
685
|
+
if (headerVersion) {
|
|
686
|
+
headerVersion.addEventListener('click', showVersionModal);
|
|
687
|
+
}
|
|
688
|
+
|
|
491
689
|
// Connect WebSocket with handlers
|
|
492
690
|
connectWebSocket({
|
|
493
691
|
onStatus: updateStatus,
|
|
@@ -499,4 +697,33 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
499
697
|
// Load initial data
|
|
500
698
|
loadTasks();
|
|
501
699
|
loadLogs();
|
|
700
|
+
|
|
701
|
+
// Add pause/resume button handler
|
|
702
|
+
const toggleButton = document.getElementById('toggleProcessingButton');
|
|
703
|
+
if (toggleButton) {
|
|
704
|
+
toggleButton.addEventListener('click', async () => {
|
|
705
|
+
const icon = document.getElementById('processingButtonIcon');
|
|
706
|
+
const currentlyPaused = icon && icon.textContent === 'Resume';
|
|
707
|
+
const endpoint = currentlyPaused ? '/api/tasks/resume' : '/api/tasks/pause';
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
toggleButton.disabled = true;
|
|
711
|
+
const response = await fetch(endpoint, { method: 'POST' });
|
|
712
|
+
const result = await response.json();
|
|
713
|
+
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
console.error('Failed to toggle processing:', result);
|
|
716
|
+
// Show specific error message (e.g., "Cannot resume during autofocus")
|
|
717
|
+
alert((result.error || 'Failed to toggle task processing') +
|
|
718
|
+
(response.status === 409 ? '' : ' - Unknown error'));
|
|
719
|
+
}
|
|
720
|
+
// State will be updated via WebSocket broadcast within 2 seconds
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error('Error toggling processing:', error);
|
|
723
|
+
alert('Error toggling task processing');
|
|
724
|
+
} finally {
|
|
725
|
+
toggleButton.disabled = false;
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
502
729
|
});
|