citrascope 0.3.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/__main__.py +12 -15
- citrascope/api/citra_api_client.py +13 -1
- citrascope/citra_scope_daemon.py +91 -19
- citrascope/constants.py +23 -0
- citrascope/hardware/abstract_astro_hardware_adapter.py +61 -0
- citrascope/hardware/nina_adv_http_adapter.py +106 -77
- citrascope/logging/web_log_handler.py +9 -8
- citrascope/settings/citrascope_settings.py +34 -45
- citrascope/tasks/runner.py +36 -2
- citrascope/web/app.py +137 -13
- citrascope/web/server.py +10 -1
- citrascope/web/static/app.js +246 -17
- citrascope/web/static/config.js +248 -9
- citrascope/web/static/style.css +32 -0
- citrascope/web/templates/dashboard.html +143 -7
- {citrascope-0.3.0.dist-info → citrascope-0.5.0.dist-info}/METADATA +40 -32
- {citrascope-0.3.0.dist-info → citrascope-0.5.0.dist-info}/RECORD +19 -19
- docs/index.md +0 -47
- {citrascope-0.3.0.dist-info → citrascope-0.5.0.dist-info}/WHEEL +0 -0
- {citrascope-0.3.0.dist-info → citrascope-0.5.0.dist-info}/entry_points.txt +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 } 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
|
|
|
@@ -412,12 +591,14 @@ function updateLatestLogLine() {
|
|
|
412
591
|
|
|
413
592
|
const timestamp = new Date(latestLog.timestamp).toLocaleTimeString();
|
|
414
593
|
const cleanMessage = stripAnsiCodes(latestLog.message);
|
|
594
|
+
// Truncate message to ~150 chars for collapsed header (approx 2 lines)
|
|
595
|
+
const truncatedMessage = cleanMessage.length > 150 ? cleanMessage.substring(0, 150) + '...' : cleanMessage;
|
|
415
596
|
|
|
416
597
|
content.querySelector('.log-timestamp').textContent = timestamp;
|
|
417
598
|
const levelSpan = content.querySelector('.log-level');
|
|
418
599
|
levelSpan.classList.add(`log-level-${latestLog.level}`);
|
|
419
600
|
levelSpan.textContent = latestLog.level;
|
|
420
|
-
content.querySelector('.log-message').textContent =
|
|
601
|
+
content.querySelector('.log-message').textContent = truncatedMessage;
|
|
421
602
|
|
|
422
603
|
latestLogLine.innerHTML = '';
|
|
423
604
|
latestLogLine.appendChild(content);
|
|
@@ -483,9 +664,28 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
483
664
|
// Initialize configuration management (loads config)
|
|
484
665
|
await initConfig();
|
|
485
666
|
|
|
667
|
+
// Initialize filter configuration
|
|
668
|
+
await initFilterConfig();
|
|
669
|
+
|
|
670
|
+
// Setup autofocus button (only once)
|
|
671
|
+
setupAutofocusButton();
|
|
672
|
+
|
|
486
673
|
// Update app URL links from loaded config
|
|
487
674
|
updateAppUrlLinks();
|
|
488
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
|
+
|
|
489
689
|
// Connect WebSocket with handlers
|
|
490
690
|
connectWebSocket({
|
|
491
691
|
onStatus: updateStatus,
|
|
@@ -497,4 +697,33 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|
|
497
697
|
// Load initial data
|
|
498
698
|
loadTasks();
|
|
499
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
|
+
}
|
|
500
729
|
});
|
citrascope/web/static/config.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { getConfig, saveConfig, getConfigStatus, getHardwareAdapters, getAdapterSchema } from './api.js';
|
|
4
4
|
|
|
5
|
+
// API Host constants - must match backend constants in app.py
|
|
6
|
+
const PROD_API_HOST = 'api.citra.space';
|
|
7
|
+
const DEV_API_HOST = 'dev.api.citra.space';
|
|
8
|
+
const DEFAULT_API_PORT = 443;
|
|
9
|
+
|
|
5
10
|
let currentAdapterSchema = [];
|
|
6
11
|
export let currentConfig = {};
|
|
7
12
|
|
|
8
|
-
/**
|
|
9
|
-
* Initialize configuration management
|
|
10
|
-
*/
|
|
11
13
|
export async function initConfig() {
|
|
12
14
|
// Populate hardware adapter dropdown
|
|
13
15
|
await loadAdapterOptions();
|
|
@@ -25,6 +27,19 @@ export async function initConfig() {
|
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
// API endpoint selection change
|
|
31
|
+
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
32
|
+
if (apiEndpointSelect) {
|
|
33
|
+
apiEndpointSelect.addEventListener('change', function(e) {
|
|
34
|
+
const customContainer = document.getElementById('customHostContainer');
|
|
35
|
+
if (e.target.value === 'custom') {
|
|
36
|
+
customContainer.style.display = 'block';
|
|
37
|
+
} else {
|
|
38
|
+
customContainer.style.display = 'none';
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
// Config form submission
|
|
29
44
|
const configForm = document.getElementById('configForm');
|
|
30
45
|
if (configForm) {
|
|
@@ -114,6 +129,27 @@ async function loadConfiguration() {
|
|
|
114
129
|
imagesDirElement.textContent = config.images_dir_path;
|
|
115
130
|
}
|
|
116
131
|
|
|
132
|
+
// API endpoint selector
|
|
133
|
+
const apiEndpointSelect = document.getElementById('apiEndpoint');
|
|
134
|
+
const customHostContainer = document.getElementById('customHostContainer');
|
|
135
|
+
const customHost = document.getElementById('customHost');
|
|
136
|
+
const customPort = document.getElementById('customPort');
|
|
137
|
+
const customUseSsl = document.getElementById('customUseSsl');
|
|
138
|
+
|
|
139
|
+
if (config.host === PROD_API_HOST) {
|
|
140
|
+
apiEndpointSelect.value = 'production';
|
|
141
|
+
customHostContainer.style.display = 'none';
|
|
142
|
+
} else if (config.host === DEV_API_HOST) {
|
|
143
|
+
apiEndpointSelect.value = 'development';
|
|
144
|
+
customHostContainer.style.display = 'none';
|
|
145
|
+
} else {
|
|
146
|
+
apiEndpointSelect.value = 'custom';
|
|
147
|
+
customHostContainer.style.display = 'block';
|
|
148
|
+
customHost.value = config.host || '';
|
|
149
|
+
customPort.value = config.port || DEFAULT_API_PORT;
|
|
150
|
+
customUseSsl.checked = config.use_ssl !== undefined ? config.use_ssl : true;
|
|
151
|
+
}
|
|
152
|
+
|
|
117
153
|
// Core fields
|
|
118
154
|
document.getElementById('personal_access_token').value = config.personal_access_token || '';
|
|
119
155
|
document.getElementById('telescopeId').value = config.telescope_id || '';
|
|
@@ -274,6 +310,24 @@ async function saveConfiguration(event) {
|
|
|
274
310
|
// Hide previous messages
|
|
275
311
|
hideConfigMessages();
|
|
276
312
|
|
|
313
|
+
// Determine API host settings based on endpoint selection
|
|
314
|
+
const apiEndpoint = document.getElementById('apiEndpoint').value;
|
|
315
|
+
let host, port, use_ssl;
|
|
316
|
+
|
|
317
|
+
if (apiEndpoint === 'production') {
|
|
318
|
+
host = PROD_API_HOST;
|
|
319
|
+
port = DEFAULT_API_PORT;
|
|
320
|
+
use_ssl = true;
|
|
321
|
+
} else if (apiEndpoint === 'development') {
|
|
322
|
+
host = DEV_API_HOST;
|
|
323
|
+
port = DEFAULT_API_PORT;
|
|
324
|
+
use_ssl = true;
|
|
325
|
+
} else { // custom
|
|
326
|
+
host = document.getElementById('customHost').value;
|
|
327
|
+
port = parseInt(document.getElementById('customPort').value, 10);
|
|
328
|
+
use_ssl = document.getElementById('customUseSsl').checked;
|
|
329
|
+
}
|
|
330
|
+
|
|
277
331
|
const config = {
|
|
278
332
|
personal_access_token: document.getElementById('personal_access_token').value,
|
|
279
333
|
telescope_id: document.getElementById('telescopeId').value,
|
|
@@ -282,10 +336,11 @@ async function saveConfiguration(event) {
|
|
|
282
336
|
log_level: document.getElementById('logLevel').value,
|
|
283
337
|
keep_images: document.getElementById('keep_images').checked,
|
|
284
338
|
file_logging_enabled: document.getElementById('file_logging_enabled').checked,
|
|
285
|
-
//
|
|
286
|
-
host:
|
|
287
|
-
port:
|
|
288
|
-
use_ssl:
|
|
339
|
+
// API settings from endpoint selector
|
|
340
|
+
host: host,
|
|
341
|
+
port: port,
|
|
342
|
+
use_ssl: use_ssl,
|
|
343
|
+
// Preserve other settings from loaded config
|
|
289
344
|
max_task_retries: currentConfig.max_task_retries || 3,
|
|
290
345
|
initial_retry_delay_seconds: currentConfig.initial_retry_delay_seconds || 30,
|
|
291
346
|
max_retry_delay_seconds: currentConfig.max_retry_delay_seconds || 300,
|
|
@@ -296,9 +351,23 @@ async function saveConfiguration(event) {
|
|
|
296
351
|
const result = await saveConfig(config);
|
|
297
352
|
|
|
298
353
|
if (result.ok) {
|
|
299
|
-
|
|
354
|
+
// After config saved successfully, save any modified filter focus positions
|
|
355
|
+
const filterResults = await saveModifiedFilters();
|
|
356
|
+
|
|
357
|
+
// Build success message based on results
|
|
358
|
+
let message = result.data.message || 'Configuration saved and applied successfully!';
|
|
359
|
+
if (filterResults.success > 0) {
|
|
360
|
+
message += ` Updated ${filterResults.success} filter focus position${filterResults.success > 1 ? 's' : ''}.`;
|
|
361
|
+
}
|
|
362
|
+
if (filterResults.failed > 0) {
|
|
363
|
+
message += ` Warning: ${filterResults.failed} filter update${filterResults.failed > 1 ? 's' : ''} failed.`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
showConfigSuccess(message);
|
|
300
367
|
} else {
|
|
301
|
-
|
|
368
|
+
// Check for specific error codes
|
|
369
|
+
const errorMsg = result.data.error || result.data.message || 'Failed to save configuration';
|
|
370
|
+
showConfigError(errorMsg);
|
|
302
371
|
}
|
|
303
372
|
} catch (error) {
|
|
304
373
|
showConfigError('Failed to save configuration: ' + error.message);
|
|
@@ -358,5 +427,175 @@ export function showConfigSection() {
|
|
|
358
427
|
}
|
|
359
428
|
}
|
|
360
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Load and display filter configuration
|
|
432
|
+
*/
|
|
433
|
+
async function loadFilterConfig() {
|
|
434
|
+
const filterSection = document.getElementById('filterConfigSection');
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const response = await fetch('/api/adapter/filters');
|
|
438
|
+
|
|
439
|
+
if (response.status === 404 || response.status === 503) {
|
|
440
|
+
// Adapter doesn't support filters or isn't available
|
|
441
|
+
if (filterSection) filterSection.style.display = 'none';
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const data = await response.json();
|
|
446
|
+
|
|
447
|
+
if (response.ok && data.filters) {
|
|
448
|
+
// Show the filter section
|
|
449
|
+
if (filterSection) filterSection.style.display = 'block';
|
|
450
|
+
|
|
451
|
+
// Populate filter table
|
|
452
|
+
const tbody = document.getElementById('filterTableBody');
|
|
453
|
+
const noFiltersMsg = document.getElementById('noFiltersMessage');
|
|
454
|
+
|
|
455
|
+
if (tbody) {
|
|
456
|
+
tbody.innerHTML = '';
|
|
457
|
+
const filters = data.filters;
|
|
458
|
+
const filterIds = Object.keys(filters).sort();
|
|
459
|
+
|
|
460
|
+
if (filterIds.length === 0) {
|
|
461
|
+
if (noFiltersMsg) noFiltersMsg.style.display = 'block';
|
|
462
|
+
} else {
|
|
463
|
+
if (noFiltersMsg) noFiltersMsg.style.display = 'none';
|
|
464
|
+
|
|
465
|
+
filterIds.forEach(filterId => {
|
|
466
|
+
const filter = filters[filterId];
|
|
467
|
+
const row = document.createElement('tr');
|
|
468
|
+
row.innerHTML = `
|
|
469
|
+
<td>${filterId}</td>
|
|
470
|
+
<td>${filter.name}</td>
|
|
471
|
+
<td>
|
|
472
|
+
<input type="number"
|
|
473
|
+
class="form-control form-control-sm filter-focus-input"
|
|
474
|
+
data-filter-id="${filterId}"
|
|
475
|
+
value="${filter.focus_position}"
|
|
476
|
+
min="0"
|
|
477
|
+
step="1">
|
|
478
|
+
</td>
|
|
479
|
+
`;
|
|
480
|
+
tbody.appendChild(row);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
if (filterSection) filterSection.style.display = 'none';
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error('Error loading filter config:', error);
|
|
489
|
+
if (filterSection) filterSection.style.display = 'none';
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Save all filter focus positions (called during main config save)
|
|
495
|
+
* Returns: Object with { success: number, failed: number }
|
|
496
|
+
*/
|
|
497
|
+
async function saveModifiedFilters() {
|
|
498
|
+
const inputs = document.querySelectorAll('.filter-focus-input');
|
|
499
|
+
if (inputs.length === 0) return { success: 0, failed: 0 }; // No filters to save
|
|
500
|
+
|
|
501
|
+
let successCount = 0;
|
|
502
|
+
let failedCount = 0;
|
|
503
|
+
|
|
504
|
+
// Save all filter values
|
|
505
|
+
for (const input of inputs) {
|
|
506
|
+
const filterId = input.dataset.filterId;
|
|
507
|
+
const focusPosition = parseInt(input.value);
|
|
508
|
+
|
|
509
|
+
if (isNaN(focusPosition) || focusPosition < 0) {
|
|
510
|
+
failedCount++;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
const response = await fetch(`/api/adapter/filters/${filterId}`, {
|
|
516
|
+
method: 'PATCH',
|
|
517
|
+
headers: {
|
|
518
|
+
'Content-Type': 'application/json'
|
|
519
|
+
},
|
|
520
|
+
body: JSON.stringify({ focus_position: focusPosition })
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
if (response.ok) {
|
|
524
|
+
successCount++;
|
|
525
|
+
} else {
|
|
526
|
+
failedCount++;
|
|
527
|
+
console.error(`Failed to save filter ${filterId}: HTTP ${response.status}`);
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
failedCount++;
|
|
531
|
+
console.error(`Error saving filter ${filterId}:`, error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return { success: successCount, failed: failedCount };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Trigger autofocus routine
|
|
540
|
+
*/
|
|
541
|
+
async function triggerAutofocus() {
|
|
542
|
+
const button = document.getElementById('runAutofocusButton');
|
|
543
|
+
const buttonText = document.getElementById('autofocusButtonText');
|
|
544
|
+
const buttonSpinner = document.getElementById('autofocusButtonSpinner');
|
|
545
|
+
|
|
546
|
+
if (!button || !buttonText || !buttonSpinner) return;
|
|
547
|
+
|
|
548
|
+
// Clear any previous messages
|
|
549
|
+
hideConfigMessages();
|
|
550
|
+
|
|
551
|
+
// Disable button and show spinner
|
|
552
|
+
button.disabled = true;
|
|
553
|
+
buttonText.textContent = 'Running Autofocus...';
|
|
554
|
+
buttonSpinner.style.display = 'inline-block';
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const response = await fetch('/api/adapter/autofocus', {
|
|
558
|
+
method: 'POST'
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const data = await response.json();
|
|
562
|
+
|
|
563
|
+
if (response.ok) {
|
|
564
|
+
showConfigSuccess('Autofocus completed successfully');
|
|
565
|
+
// Reload filter config to show updated focus positions
|
|
566
|
+
await loadFilterConfig();
|
|
567
|
+
} else {
|
|
568
|
+
// Show clear error message (e.g., if autofocus already running or other conflict)
|
|
569
|
+
showConfigError(data.error || 'Autofocus failed');
|
|
570
|
+
}
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error('Error triggering autofocus:', error);
|
|
573
|
+
showConfigError('Failed to trigger autofocus');
|
|
574
|
+
} finally {
|
|
575
|
+
// Re-enable button
|
|
576
|
+
button.disabled = false;
|
|
577
|
+
buttonText.textContent = 'Run Autofocus';
|
|
578
|
+
buttonSpinner.style.display = 'none';
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Initialize filter configuration on page load
|
|
584
|
+
*/
|
|
585
|
+
export async function initFilterConfig() {
|
|
586
|
+
// Load filter config when config section is visible
|
|
587
|
+
await loadFilterConfig();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Setup autofocus button event listener (call once during init)
|
|
592
|
+
*/
|
|
593
|
+
export function setupAutofocusButton() {
|
|
594
|
+
const autofocusBtn = document.getElementById('runAutofocusButton');
|
|
595
|
+
if (autofocusBtn) {
|
|
596
|
+
autofocusBtn.addEventListener('click', triggerAutofocus);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
361
600
|
// Make showConfigSection available globally for onclick handlers in HTML
|
|
362
601
|
window.showConfigSection = showConfigSection;
|
citrascope/web/static/style.css
CHANGED
|
@@ -57,6 +57,29 @@ body {
|
|
|
57
57
|
/* Log accordion customization */
|
|
58
58
|
.log-accordion-button {
|
|
59
59
|
border-bottom: 1px solid #444 !important;
|
|
60
|
+
position: relative;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.accordion-social-links {
|
|
64
|
+
position: absolute;
|
|
65
|
+
right: 50px; /* Position to the left of the accordion arrow */
|
|
66
|
+
top: 50%;
|
|
67
|
+
transform: translateY(-50%);
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: 12px;
|
|
70
|
+
align-items: center;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.social-link {
|
|
74
|
+
color: #a0aec0;
|
|
75
|
+
transition: color 0.2s ease;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
text-decoration: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.social-link:hover {
|
|
82
|
+
color: #e2e8f0;
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
.log-accordion-body {
|
|
@@ -72,6 +95,15 @@ body {
|
|
|
72
95
|
.log-latest-line {
|
|
73
96
|
font-family: monospace;
|
|
74
97
|
color: #e2e8f0;
|
|
98
|
+
display: block;
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
101
|
+
display: -webkit-box;
|
|
102
|
+
-webkit-line-clamp: 2;
|
|
103
|
+
-webkit-box-orient: vertical;
|
|
104
|
+
line-height: 1.4em;
|
|
105
|
+
max-height: 2.8em;
|
|
106
|
+
padding-right: 130px; /* Make room for 3 social icons and accordion arrow */
|
|
75
107
|
}
|
|
76
108
|
|
|
77
109
|
/* Task display */
|