citrascope 0.7.0__py3-none-any.whl → 0.9.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 +97 -2
- citrascope/hardware/adapter_registry.py +15 -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 +114 -0
- citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
- citrascope/hardware/devices/camera/usb_camera.py +407 -0
- citrascope/hardware/devices/camera/ximea_camera.py +756 -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 +805 -0
- citrascope/hardware/dummy_adapter.py +202 -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 +14 -0
- citrascope/time/time_health.py +103 -0
- citrascope/time/time_monitor.py +186 -0
- citrascope/time/time_sources.py +261 -0
- citrascope/web/app.py +260 -60
- citrascope/web/static/app.js +121 -731
- citrascope/web/static/components.js +136 -0
- citrascope/web/static/config.js +259 -420
- citrascope/web/static/filters.js +55 -0
- citrascope/web/static/formatters.js +129 -0
- citrascope/web/static/store-init.js +204 -0
- citrascope/web/static/style.css +44 -0
- citrascope/web/templates/_config.html +175 -0
- citrascope/web/templates/_config_hardware.html +208 -0
- citrascope/web/templates/_monitoring.html +242 -0
- citrascope/web/templates/dashboard.html +109 -377
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/METADATA +18 -1
- citrascope-0.9.0.dist-info/RECORD +69 -0
- citrascope-0.7.0.dist-info/RECORD +0 -41
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/WHEEL +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/entry_points.txt +0 -0
- {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/licenses/LICENSE +0 -0
citrascope/web/static/app.js
CHANGED
|
@@ -1,86 +1,111 @@
|
|
|
1
|
-
// CitraScope Dashboard - Main Application
|
|
1
|
+
// CitraScope Dashboard - Main Application (Alpine.js)
|
|
2
2
|
import { connectWebSocket } from './websocket.js';
|
|
3
|
-
import { initConfig,
|
|
3
|
+
import { initConfig, initFilterConfig, setupAutofocusButton, createToast } from './config.js';
|
|
4
4
|
import { getTasks, getLogs } from './api.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
// Store and components are registered in store-init.js (loaded before Alpine)
|
|
7
|
+
|
|
8
|
+
// --- Store update handlers (replace DOM manipulation) ---
|
|
9
|
+
function updateStoreFromStatus(status) {
|
|
10
|
+
const store = Alpine.store('citrascope');
|
|
11
|
+
store.status = status;
|
|
12
|
+
|
|
13
|
+
if (status.current_task && status.current_task !== 'None') {
|
|
14
|
+
store.isTaskActive = true;
|
|
15
|
+
store.currentTaskId = status.current_task;
|
|
16
|
+
store.nextTaskStartTime = null;
|
|
17
|
+
} else {
|
|
18
|
+
store.isTaskActive = false;
|
|
19
|
+
store.currentTaskId = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Set nextTaskStartTime from tasks if we have them and no active task
|
|
23
|
+
if (!store.isTaskActive && store.tasks.length > 0) {
|
|
24
|
+
const sorted = [...store.tasks].sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
|
|
25
|
+
store.nextTaskStartTime = sorted[0].start_time;
|
|
26
|
+
}
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
function updateStoreFromTasks(tasks) {
|
|
30
|
+
const store = Alpine.store('citrascope');
|
|
31
|
+
const sorted = [...(tasks || [])].sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
|
|
32
|
+
store.tasks = sorted;
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
if (!store.isTaskActive && sorted.length > 0) {
|
|
35
|
+
store.nextTaskStartTime = sorted[0].start_time;
|
|
36
|
+
} else if (store.isTaskActive) {
|
|
37
|
+
store.nextTaskStartTime = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function appendLogToStore(log) {
|
|
42
|
+
const store = Alpine.store('citrascope');
|
|
43
|
+
store.logs = [...store.logs, log];
|
|
44
|
+
store.latestLog = log;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateStoreFromConnection(connected, reconnectInfo = '') {
|
|
48
|
+
const store = Alpine.store('citrascope');
|
|
49
|
+
store.wsConnected = connected;
|
|
50
|
+
store.wsReconnecting = !!reconnectInfo;
|
|
51
|
+
}
|
|
26
52
|
|
|
53
|
+
// --- Countdown tick (updates store.countdown) ---
|
|
54
|
+
let countdownInterval = null;
|
|
55
|
+
|
|
56
|
+
function startCountdownUpdater() {
|
|
57
|
+
if (countdownInterval) return;
|
|
58
|
+
countdownInterval = setInterval(() => {
|
|
59
|
+
const store = Alpine.store('citrascope');
|
|
60
|
+
if (!store.nextTaskStartTime || store.isTaskActive) {
|
|
61
|
+
store.countdown = '';
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const now = new Date();
|
|
65
|
+
const timeUntil = new Date(store.nextTaskStartTime) - now;
|
|
66
|
+
store.countdown = timeUntil > 0 ? store.formatCountdown(timeUntil) : 'Starting soon...';
|
|
67
|
+
}, 1000);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Version checking ---
|
|
71
|
+
function compareVersions(v1, v2) {
|
|
72
|
+
v1 = (v1 || '').replace(/^v/, '');
|
|
73
|
+
v2 = (v2 || '').replace(/^v/, '');
|
|
27
74
|
const parts1 = v1.split('.').map(n => parseInt(n) || 0);
|
|
28
75
|
const parts2 = v2.split('.').map(n => parseInt(n) || 0);
|
|
29
|
-
|
|
30
76
|
const maxLen = Math.max(parts1.length, parts2.length);
|
|
31
|
-
|
|
32
77
|
for (let i = 0; i < maxLen; i++) {
|
|
33
78
|
const num1 = parts1[i] || 0;
|
|
34
79
|
const num2 = parts2[i] || 0;
|
|
35
|
-
|
|
36
80
|
if (num1 > num2) return 1;
|
|
37
81
|
if (num1 < num2) return -1;
|
|
38
82
|
}
|
|
39
|
-
|
|
40
83
|
return 0;
|
|
41
84
|
}
|
|
42
85
|
|
|
43
|
-
/**
|
|
44
|
-
* Fetch and display current version
|
|
45
|
-
*/
|
|
46
86
|
async function fetchVersion() {
|
|
47
87
|
try {
|
|
48
88
|
const response = await fetch('/api/version');
|
|
49
89
|
const data = await response.json();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
}
|
|
90
|
+
const store = Alpine.store('citrascope');
|
|
91
|
+
if (data.version) {
|
|
92
|
+
store.version = data.version === 'development' ? 'dev' : 'v' + data.version;
|
|
93
|
+
} else {
|
|
94
|
+
store.version = 'v?';
|
|
61
95
|
}
|
|
62
96
|
} catch (error) {
|
|
63
97
|
console.error('Error fetching version:', error);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (headerVersionEl) {
|
|
67
|
-
headerVersionEl.textContent = 'v?';
|
|
68
|
-
}
|
|
98
|
+
Alpine.store('citrascope').version = 'v?';
|
|
69
99
|
}
|
|
70
100
|
}
|
|
71
101
|
|
|
72
|
-
/**
|
|
73
|
-
* Check for available updates from GitHub
|
|
74
|
-
* Returns the check result for modal display
|
|
75
|
-
*/
|
|
76
102
|
async function checkForUpdates() {
|
|
103
|
+
const store = Alpine.store('citrascope');
|
|
77
104
|
try {
|
|
78
|
-
// Get current version
|
|
79
105
|
const versionResponse = await fetch('/api/version');
|
|
80
106
|
const versionData = await versionResponse.json();
|
|
81
107
|
const currentVersion = versionData.version;
|
|
82
108
|
|
|
83
|
-
// Check GitHub for latest release
|
|
84
109
|
const githubResponse = await fetch('https://api.github.com/repos/citra-space/citrascope/releases/latest');
|
|
85
110
|
if (!githubResponse.ok) {
|
|
86
111
|
return { status: 'error', currentVersion };
|
|
@@ -90,729 +115,94 @@ async function checkForUpdates() {
|
|
|
90
115
|
const latestVersion = releaseData.tag_name.replace(/^v/, '');
|
|
91
116
|
const releaseUrl = releaseData.html_url;
|
|
92
117
|
|
|
93
|
-
// Skip comparison for development versions
|
|
94
118
|
if (currentVersion === 'development' || currentVersion === 'unknown') {
|
|
119
|
+
store.updateIndicator = '';
|
|
95
120
|
return { status: 'up-to-date', currentVersion };
|
|
96
121
|
}
|
|
97
122
|
|
|
98
|
-
// Compare versions
|
|
99
123
|
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
};
|
|
124
|
+
store.updateIndicator = `${latestVersion} Available!`;
|
|
125
|
+
return { status: 'update-available', currentVersion, latestVersion, releaseUrl };
|
|
113
126
|
} else {
|
|
114
|
-
|
|
115
|
-
const indicator = document.getElementById('updateIndicator');
|
|
116
|
-
if (indicator) {
|
|
117
|
-
indicator.style.display = 'none';
|
|
118
|
-
}
|
|
119
|
-
|
|
127
|
+
store.updateIndicator = '';
|
|
120
128
|
return { status: 'up-to-date', currentVersion };
|
|
121
129
|
}
|
|
122
130
|
} catch (error) {
|
|
123
|
-
// Network error
|
|
124
131
|
console.debug('Update check failed:', error);
|
|
125
132
|
return { status: 'error', currentVersion: 'unknown' };
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
|
|
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 ---
|
|
164
|
-
let nextTaskStartTime = null;
|
|
165
|
-
let countdownInterval = null;
|
|
166
|
-
let isTaskActive = false;
|
|
167
|
-
let currentTaskId = null;
|
|
168
|
-
let currentTasks = []; // Store tasks for lookup
|
|
169
|
-
|
|
170
|
-
// --- Utility Functions ---
|
|
171
|
-
function stripAnsiCodes(text) {
|
|
172
|
-
// Remove ANSI color codes (e.g., [92m, [0m, etc.)
|
|
173
|
-
return text.replace(/\x1B\[\d+m/g, '').replace(/\[\d+m/g, '');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function formatLocalTime(isoString) {
|
|
177
|
-
const date = new Date(isoString);
|
|
178
|
-
return date.toLocaleString(undefined, {
|
|
179
|
-
month: 'short',
|
|
180
|
-
day: 'numeric',
|
|
181
|
-
hour: '2-digit',
|
|
182
|
-
minute: '2-digit',
|
|
183
|
-
second: '2-digit',
|
|
184
|
-
hour12: true
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function formatCountdown(milliseconds) {
|
|
189
|
-
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
190
|
-
|
|
191
|
-
if (totalSeconds < 0) return 'Starting soon...';
|
|
192
|
-
|
|
193
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
194
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
195
|
-
const seconds = totalSeconds % 60;
|
|
196
|
-
|
|
197
|
-
if (hours > 0) {
|
|
198
|
-
return `${hours}h ${minutes}m ${seconds}s`;
|
|
199
|
-
} else if (minutes > 0) {
|
|
200
|
-
return `${minutes}m ${seconds}s`;
|
|
201
|
-
} else {
|
|
202
|
-
return `${seconds}s`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function updateCountdown() {
|
|
207
|
-
if (!nextTaskStartTime || isTaskActive) return;
|
|
208
|
-
|
|
209
|
-
const now = new Date();
|
|
210
|
-
const timeUntil = nextTaskStartTime - now;
|
|
211
|
-
|
|
212
|
-
const currentTaskDisplay = document.getElementById('currentTaskDisplay');
|
|
213
|
-
if (currentTaskDisplay && timeUntil > 0) {
|
|
214
|
-
const countdown = formatCountdown(timeUntil);
|
|
215
|
-
currentTaskDisplay.innerHTML = `<p class="no-task-message">No active task - next task in ${countdown}</p>`;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function startCountdown(startTime) {
|
|
220
|
-
nextTaskStartTime = new Date(startTime);
|
|
221
|
-
|
|
222
|
-
// Clear any existing interval
|
|
223
|
-
if (countdownInterval) {
|
|
224
|
-
clearInterval(countdownInterval);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Update immediately
|
|
228
|
-
updateCountdown();
|
|
229
|
-
|
|
230
|
-
// Update every second
|
|
231
|
-
countdownInterval = setInterval(updateCountdown, 1000);
|
|
232
|
-
}
|
|
136
|
+
// Version modal moved to Alpine store method
|
|
233
137
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
138
|
+
// --- Navigation (Alpine-driven in Phase 3, keep hash sync for now) ---
|
|
139
|
+
function navigateToSection(section) {
|
|
140
|
+
const store = Alpine.store('citrascope');
|
|
141
|
+
store.currentSection = section;
|
|
142
|
+
window.location.hash = section;
|
|
143
|
+
if (section === 'config') {
|
|
144
|
+
initFilterConfig();
|
|
239
145
|
}
|
|
240
146
|
}
|
|
241
147
|
|
|
242
|
-
// --- Navigation Logic ---
|
|
243
148
|
function initNavigation() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const nav = document.getElementById('mainNav');
|
|
251
|
-
if (nav) {
|
|
252
|
-
// Find all nav links and all dashboard sections with id ending in 'Section'
|
|
253
|
-
const navLinks = nav.querySelectorAll('a[data-section]');
|
|
254
|
-
const sections = {};
|
|
255
|
-
navLinks.forEach(link => {
|
|
256
|
-
const section = link.getAttribute('data-section');
|
|
257
|
-
const sectionEl = document.getElementById(section + 'Section');
|
|
258
|
-
if (sectionEl) {
|
|
259
|
-
sections[section] = sectionEl
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
console.log(`No section element found for section: ${section}`);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
function activateNav(link) {
|
|
267
|
-
navLinks.forEach(a => {
|
|
268
|
-
a.classList.remove('text-white');
|
|
269
|
-
a.removeAttribute('aria-current');
|
|
270
|
-
});
|
|
271
|
-
link.classList.add('text-white');
|
|
272
|
-
link.setAttribute('aria-current', 'page');
|
|
149
|
+
window.addEventListener('hashchange', () => {
|
|
150
|
+
const hash = window.location.hash.substring(1);
|
|
151
|
+
if (hash && (hash === 'monitoring' || hash === 'config')) {
|
|
152
|
+
const store = Alpine.store('citrascope');
|
|
153
|
+
store.currentSection = hash;
|
|
154
|
+
if (hash === 'config') initFilterConfig();
|
|
273
155
|
}
|
|
156
|
+
});
|
|
274
157
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
console.log(`No section found to show for section: ${section}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
nav.addEventListener('click', function(e) {
|
|
283
|
-
const link = e.target.closest('a[data-section]');
|
|
284
|
-
if (link) {
|
|
285
|
-
e.preventDefault();
|
|
286
|
-
const section = link.getAttribute('data-section');
|
|
287
|
-
activateNav(link);
|
|
288
|
-
showSection(section);
|
|
289
|
-
|
|
290
|
-
// Reload filter config when config section is shown
|
|
291
|
-
if (section === 'config') {
|
|
292
|
-
initFilterConfig();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// Default to first nav item
|
|
298
|
-
const first = nav.querySelector('a[data-section]');
|
|
299
|
-
if (first) {
|
|
300
|
-
activateNav(first);
|
|
301
|
-
showSection(first.getAttribute('data-section'));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// --- WebSocket Status Display ---
|
|
307
|
-
function updateWSStatus(connected, reconnectInfo = '') {
|
|
308
|
-
const statusEl = document.getElementById('wsStatus');
|
|
309
|
-
const template = document.getElementById('connectionStatusTemplate');
|
|
310
|
-
const content = template.content.cloneNode(true);
|
|
311
|
-
const badge = content.querySelector('.connection-status-badge');
|
|
312
|
-
const statusText = content.querySelector('.status-text');
|
|
313
|
-
|
|
314
|
-
if (connected) {
|
|
315
|
-
badge.classList.add('bg-success');
|
|
316
|
-
badge.setAttribute('title', 'Dashboard connected - receiving live updates');
|
|
317
|
-
statusText.textContent = 'Connected';
|
|
318
|
-
} else if (reconnectInfo) {
|
|
319
|
-
badge.classList.add('bg-warning', 'text-dark');
|
|
320
|
-
badge.setAttribute('title', 'Dashboard reconnecting - attempting to restore connection');
|
|
321
|
-
statusText.textContent = 'Reconnecting';
|
|
322
|
-
} else {
|
|
323
|
-
badge.classList.add('bg-danger');
|
|
324
|
-
badge.setAttribute('title', 'Dashboard disconnected - no live updates');
|
|
325
|
-
statusText.textContent = 'Disconnected';
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
statusEl.innerHTML = '';
|
|
329
|
-
statusEl.appendChild(content);
|
|
330
|
-
|
|
331
|
-
// Reinitialize tooltips after updating the DOM
|
|
332
|
-
const tooltipTrigger = statusEl.querySelector('[data-bs-toggle="tooltip"]');
|
|
333
|
-
if (tooltipTrigger) {
|
|
334
|
-
new bootstrap.Tooltip(tooltipTrigger);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// --- Status Updates ---
|
|
339
|
-
function updateStatus(status) {
|
|
340
|
-
document.getElementById('hardwareAdapter').textContent = status.hardware_adapter || '-';
|
|
341
|
-
document.getElementById('telescopeConnected').innerHTML = status.telescope_connected
|
|
342
|
-
? '<span class="badge rounded-pill bg-success">Connected</span>'
|
|
343
|
-
: '<span class="badge rounded-pill bg-danger">Disconnected</span>';
|
|
344
|
-
document.getElementById('cameraConnected').innerHTML = status.camera_connected
|
|
345
|
-
? '<span class="badge rounded-pill bg-success">Connected</span>'
|
|
346
|
-
: '<span class="badge rounded-pill bg-danger">Disconnected</span>';
|
|
347
|
-
|
|
348
|
-
// Update current task display
|
|
349
|
-
if (status.current_task && status.current_task !== 'None') {
|
|
350
|
-
isTaskActive = true;
|
|
351
|
-
currentTaskId = status.current_task;
|
|
352
|
-
stopCountdown();
|
|
353
|
-
updateCurrentTaskDisplay();
|
|
354
|
-
} else if (isTaskActive) {
|
|
355
|
-
// Task just finished, set to idle state
|
|
356
|
-
isTaskActive = false;
|
|
357
|
-
currentTaskId = null;
|
|
358
|
-
updateCurrentTaskDisplay();
|
|
359
|
-
}
|
|
360
|
-
// If isTaskActive is already false, don't touch the display (countdown is updating it)
|
|
361
|
-
|
|
362
|
-
if (status.tasks_pending !== undefined) {
|
|
363
|
-
document.getElementById('tasksPending').textContent = status.tasks_pending || '0';
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (status.telescope_ra !== null) {
|
|
367
|
-
document.getElementById('telescopeRA').textContent = status.telescope_ra.toFixed(4) + '°';
|
|
368
|
-
}
|
|
369
|
-
if (status.telescope_dec !== null) {
|
|
370
|
-
document.getElementById('telescopeDEC').textContent = status.telescope_dec.toFixed(4) + '°';
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Update ground station information
|
|
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
|
-
// Update autofocus status
|
|
398
|
-
updateAutofocusStatus(status);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function updateAutofocusStatus(status) {
|
|
402
|
-
// Update autofocus button state
|
|
403
|
-
const button = document.getElementById('runAutofocusButton');
|
|
404
|
-
const buttonText = document.getElementById('autofocusButtonText');
|
|
405
|
-
|
|
406
|
-
if (button && buttonText && status.autofocus_requested !== undefined) {
|
|
407
|
-
if (status.autofocus_requested) {
|
|
408
|
-
buttonText.textContent = 'Cancel Autofocus';
|
|
409
|
-
button.dataset.action = 'cancel';
|
|
410
|
-
button.classList.remove('btn-outline-primary');
|
|
411
|
-
button.classList.add('btn-outline-warning');
|
|
412
|
-
} else {
|
|
413
|
-
buttonText.textContent = 'Run Autofocus';
|
|
414
|
-
button.dataset.action = 'request';
|
|
415
|
-
button.classList.remove('btn-outline-warning');
|
|
416
|
-
button.classList.add('btn-outline-primary');
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Update last autofocus display
|
|
421
|
-
const lastAutofocusDisplay = document.getElementById('lastAutofocusDisplay');
|
|
422
|
-
if (lastAutofocusDisplay) {
|
|
423
|
-
if (status.last_autofocus_timestamp) {
|
|
424
|
-
const timestamp = status.last_autofocus_timestamp * 1000; // Convert to milliseconds
|
|
425
|
-
const now = Date.now();
|
|
426
|
-
const elapsed = now - timestamp;
|
|
427
|
-
lastAutofocusDisplay.textContent = formatElapsedTime(elapsed);
|
|
428
|
-
} else {
|
|
429
|
-
lastAutofocusDisplay.textContent = 'Never';
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Update next autofocus display
|
|
434
|
-
const nextAutofocusDisplay = document.getElementById('nextAutofocusDisplay');
|
|
435
|
-
const nextAutofocusTime = document.getElementById('nextAutofocusTime');
|
|
436
|
-
|
|
437
|
-
if (nextAutofocusDisplay && nextAutofocusTime) {
|
|
438
|
-
if (status.next_autofocus_minutes !== null && status.next_autofocus_minutes !== undefined) {
|
|
439
|
-
nextAutofocusDisplay.style.display = 'block';
|
|
440
|
-
if (status.next_autofocus_minutes === 0) {
|
|
441
|
-
nextAutofocusTime.textContent = 'now (overdue)';
|
|
442
|
-
} else {
|
|
443
|
-
nextAutofocusTime.textContent = formatMinutes(status.next_autofocus_minutes);
|
|
444
|
-
}
|
|
445
|
-
} else {
|
|
446
|
-
nextAutofocusDisplay.style.display = 'none';
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function formatElapsedTime(milliseconds) {
|
|
452
|
-
const seconds = Math.floor(milliseconds / 1000);
|
|
453
|
-
const minutes = Math.floor(seconds / 60);
|
|
454
|
-
const hours = Math.floor(minutes / 60);
|
|
455
|
-
const days = Math.floor(hours / 24);
|
|
456
|
-
|
|
457
|
-
if (days > 0) {
|
|
458
|
-
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
459
|
-
} else if (hours > 0) {
|
|
460
|
-
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
461
|
-
} else if (minutes > 0) {
|
|
462
|
-
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
|
463
|
-
} else {
|
|
464
|
-
return 'just now';
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function formatMinutes(minutes) {
|
|
469
|
-
const hours = Math.floor(minutes / 60);
|
|
470
|
-
const mins = Math.floor(minutes % 60);
|
|
471
|
-
|
|
472
|
-
if (hours > 0) {
|
|
473
|
-
if (mins > 0) {
|
|
474
|
-
return `${hours}h ${mins}m`;
|
|
475
|
-
}
|
|
476
|
-
return `${hours}h`;
|
|
477
|
-
}
|
|
478
|
-
return `${mins}m`;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function updateProcessingState(isActive) {
|
|
482
|
-
const statusEl = document.getElementById('processingStatus');
|
|
483
|
-
const button = document.getElementById('toggleProcessingButton');
|
|
484
|
-
const icon = document.getElementById('processingButtonIcon');
|
|
485
|
-
|
|
486
|
-
if (!statusEl || !button || !icon) return;
|
|
487
|
-
|
|
488
|
-
if (isActive) {
|
|
489
|
-
statusEl.innerHTML = '<span class="badge rounded-pill bg-success">Active</span>';
|
|
490
|
-
icon.textContent = 'Pause';
|
|
491
|
-
button.title = 'Pause task processing';
|
|
492
|
-
} else {
|
|
493
|
-
statusEl.innerHTML = '<span class="badge rounded-pill bg-warning text-dark">Paused</span>';
|
|
494
|
-
icon.textContent = 'Resume';
|
|
495
|
-
button.title = 'Resume task processing';
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// --- Task Management ---
|
|
500
|
-
function getCurrentTaskDetails() {
|
|
501
|
-
if (!currentTaskId) return null;
|
|
502
|
-
return currentTasks.find(task => task.id === currentTaskId);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function updateCurrentTaskDisplay() {
|
|
506
|
-
const currentTaskDisplay = document.getElementById('currentTaskDisplay');
|
|
507
|
-
if (!currentTaskDisplay) return;
|
|
508
|
-
|
|
509
|
-
if (currentTaskId) {
|
|
510
|
-
const taskDetails = getCurrentTaskDetails();
|
|
511
|
-
if (taskDetails) {
|
|
512
|
-
currentTaskDisplay.innerHTML = `
|
|
513
|
-
<div class="d-flex align-items-center gap-2 mb-2">
|
|
514
|
-
<div class="spinner-border spinner-border-sm text-success" role="status">
|
|
515
|
-
<span class="visually-hidden">Loading...</span>
|
|
516
|
-
</div>
|
|
517
|
-
<div class="fw-bold" style="font-size: 1.3em;">${taskDetails.target}</div>
|
|
518
|
-
</div>
|
|
519
|
-
<div class="text-secondary small">
|
|
520
|
-
<span>Task ID: ${currentTaskId}</span>
|
|
521
|
-
</div>
|
|
522
|
-
`;
|
|
523
|
-
}
|
|
524
|
-
// Don't show fallback - just wait for task details to arrive
|
|
525
|
-
} else if (!isTaskActive && !nextTaskStartTime) {
|
|
526
|
-
// Only show "No active task" if we're not in countdown mode
|
|
527
|
-
currentTaskDisplay.innerHTML = '<p class="no-task-message">No active task</p>';
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function updateTasks(tasks) {
|
|
532
|
-
currentTasks = tasks;
|
|
533
|
-
renderTasks(tasks);
|
|
534
|
-
// Re-render current task display with updated task info
|
|
535
|
-
updateCurrentTaskDisplay();
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async function loadTasks() {
|
|
539
|
-
try {
|
|
540
|
-
const tasks = await getTasks();
|
|
541
|
-
renderTasks(tasks);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
console.error('Failed to load tasks:', error);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function renderTasks(tasks) {
|
|
548
|
-
try {
|
|
549
|
-
const taskList = document.getElementById('taskList');
|
|
550
|
-
|
|
551
|
-
if (tasks.length === 0) {
|
|
552
|
-
taskList.innerHTML = '<p class="p-3 text-muted-dark">No pending tasks</p>';
|
|
553
|
-
stopCountdown();
|
|
554
|
-
} else {
|
|
555
|
-
// Sort tasks by start time (earliest first)
|
|
556
|
-
const sortedTasks = tasks.sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
|
|
557
|
-
|
|
558
|
-
// Start countdown for next task if no current task is active
|
|
559
|
-
if (!isTaskActive && sortedTasks.length > 0) {
|
|
560
|
-
startCountdown(sortedTasks[0].start_time);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Create table structure
|
|
564
|
-
const table = document.createElement('table');
|
|
565
|
-
table.className = 'table table-dark table-hover mb-0';
|
|
566
|
-
|
|
567
|
-
const thead = document.createElement('thead');
|
|
568
|
-
thead.innerHTML = `
|
|
569
|
-
<tr>
|
|
570
|
-
<th>Target</th>
|
|
571
|
-
<th>Start Time</th>
|
|
572
|
-
<th>End Time</th>
|
|
573
|
-
<th>Status</th>
|
|
574
|
-
</tr>
|
|
575
|
-
`;
|
|
576
|
-
table.appendChild(thead);
|
|
577
|
-
|
|
578
|
-
const tbody = document.createElement('tbody');
|
|
579
|
-
const template = document.getElementById('taskRowTemplate');
|
|
580
|
-
|
|
581
|
-
sortedTasks.forEach(task => {
|
|
582
|
-
const isActive = task.id === currentTaskId;
|
|
583
|
-
const row = template.content.cloneNode(true);
|
|
584
|
-
const tr = row.querySelector('.task-row');
|
|
585
|
-
|
|
586
|
-
if (isActive) {
|
|
587
|
-
tr.classList.add('table-active');
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
row.querySelector('.task-target').textContent = task.target;
|
|
591
|
-
row.querySelector('.task-start').textContent = formatLocalTime(task.start_time);
|
|
592
|
-
row.querySelector('.task-end').textContent = task.stop_time ? formatLocalTime(task.stop_time) : '-';
|
|
593
|
-
|
|
594
|
-
const badge = row.querySelector('.task-status');
|
|
595
|
-
badge.classList.add(isActive ? 'bg-success' : 'bg-info');
|
|
596
|
-
badge.textContent = isActive ? 'Active' : task.status;
|
|
597
|
-
|
|
598
|
-
tbody.appendChild(row);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
table.appendChild(tbody);
|
|
602
|
-
taskList.innerHTML = '';
|
|
603
|
-
taskList.appendChild(table);
|
|
604
|
-
}
|
|
605
|
-
} catch (error) {
|
|
606
|
-
console.error('Failed to render tasks:', error);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// --- Log Display ---
|
|
611
|
-
async function loadLogs() {
|
|
612
|
-
try {
|
|
613
|
-
const data = await getLogs(100);
|
|
614
|
-
const logContainer = document.getElementById('logContainer');
|
|
615
|
-
|
|
616
|
-
if (data.logs.length === 0) {
|
|
617
|
-
logContainer.innerHTML = '<p class="text-muted-dark">No logs available</p>';
|
|
618
|
-
} else {
|
|
619
|
-
logContainer.innerHTML = '';
|
|
620
|
-
data.logs.forEach(log => {
|
|
621
|
-
appendLog(log);
|
|
622
|
-
});
|
|
623
|
-
// Scroll to bottom
|
|
624
|
-
logContainer.scrollTop = logContainer.scrollHeight;
|
|
625
|
-
}
|
|
626
|
-
} catch (error) {
|
|
627
|
-
console.error('Failed to load logs:', error);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function appendLog(log) {
|
|
632
|
-
const logContainer = document.getElementById('logContainer');
|
|
633
|
-
const template = document.getElementById('logEntryTemplate');
|
|
634
|
-
const entry = template.content.cloneNode(true);
|
|
635
|
-
|
|
636
|
-
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
|
637
|
-
const cleanMessage = stripAnsiCodes(log.message);
|
|
638
|
-
|
|
639
|
-
entry.querySelector('.log-timestamp').textContent = timestamp;
|
|
640
|
-
const levelSpan = entry.querySelector('.log-level');
|
|
641
|
-
levelSpan.classList.add(`log-level-${log.level}`);
|
|
642
|
-
levelSpan.textContent = log.level;
|
|
643
|
-
entry.querySelector('.log-message').textContent = cleanMessage;
|
|
644
|
-
|
|
645
|
-
const logEntryElement = logContainer.appendChild(entry);
|
|
646
|
-
|
|
647
|
-
const scrollParent = logContainer.closest('.accordion-body');
|
|
648
|
-
if (scrollParent) {
|
|
649
|
-
const isNearBottom = (scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight) < 100;
|
|
650
|
-
if (isNearBottom) {
|
|
651
|
-
// Get the actual appended element (first child of the DocumentFragment)
|
|
652
|
-
const lastEntry = logContainer.lastElementChild;
|
|
653
|
-
if (lastEntry) {
|
|
654
|
-
lastEntry.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// --- Roll-up Terminal Overlay Logic (Bootstrap Accordion) ---
|
|
661
|
-
let isLogExpanded = false;
|
|
662
|
-
let latestLog = null;
|
|
663
|
-
|
|
664
|
-
function updateLatestLogLine() {
|
|
665
|
-
const latestLogLine = document.getElementById('latestLogLine');
|
|
666
|
-
if (!latestLogLine) return;
|
|
667
|
-
if (isLogExpanded) {
|
|
668
|
-
latestLogLine.textContent = 'Activity';
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
if (latestLog) {
|
|
672
|
-
const template = document.getElementById('latestLogLineTemplate');
|
|
673
|
-
const content = template.content.cloneNode(true);
|
|
674
|
-
|
|
675
|
-
const timestamp = new Date(latestLog.timestamp).toLocaleTimeString();
|
|
676
|
-
const cleanMessage = stripAnsiCodes(latestLog.message);
|
|
677
|
-
// Truncate message to ~150 chars for collapsed header (approx 2 lines)
|
|
678
|
-
const truncatedMessage = cleanMessage.length > 150 ? cleanMessage.substring(0, 150) + '...' : cleanMessage;
|
|
679
|
-
|
|
680
|
-
content.querySelector('.log-timestamp').textContent = timestamp;
|
|
681
|
-
const levelSpan = content.querySelector('.log-level');
|
|
682
|
-
levelSpan.classList.add(`log-level-${latestLog.level}`);
|
|
683
|
-
levelSpan.textContent = latestLog.level;
|
|
684
|
-
content.querySelector('.log-message').textContent = truncatedMessage;
|
|
685
|
-
|
|
686
|
-
latestLogLine.innerHTML = '';
|
|
687
|
-
latestLogLine.appendChild(content);
|
|
158
|
+
const hash = window.location.hash.substring(1);
|
|
159
|
+
if (hash && (hash === 'monitoring' || hash === 'config')) {
|
|
160
|
+
navigateToSection(hash);
|
|
688
161
|
} else {
|
|
689
|
-
|
|
162
|
+
navigateToSection('monitoring');
|
|
690
163
|
}
|
|
691
164
|
}
|
|
692
165
|
|
|
693
|
-
|
|
694
|
-
// Bootstrap accordion events for log terminal
|
|
695
|
-
const logAccordionCollapse = document.getElementById('logAccordionCollapse');
|
|
696
|
-
if (logAccordionCollapse) {
|
|
697
|
-
logAccordionCollapse.addEventListener('shown.bs.collapse', () => {
|
|
698
|
-
isLogExpanded = true;
|
|
699
|
-
updateLatestLogLine();
|
|
700
|
-
const logContainer = document.getElementById('logContainer');
|
|
701
|
-
if (logContainer) {
|
|
702
|
-
setTimeout(() => {
|
|
703
|
-
const lastLog = logContainer.lastElementChild;
|
|
704
|
-
if (lastLog) {
|
|
705
|
-
lastLog.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
|
706
|
-
} else {
|
|
707
|
-
logContainer.scrollTop = logContainer.scrollHeight;
|
|
708
|
-
}
|
|
709
|
-
}, 100);
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
logAccordionCollapse.addEventListener('hide.bs.collapse', () => {
|
|
713
|
-
isLogExpanded = false;
|
|
714
|
-
updateLatestLogLine();
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
// Start collapsed by default
|
|
718
|
-
isLogExpanded = false;
|
|
719
|
-
updateLatestLogLine();
|
|
720
|
-
});
|
|
721
|
-
// --- End Roll-up Terminal Overlay Logic ---
|
|
166
|
+
// Config module will need to update store.config when loaded - we'll handle in config.js
|
|
722
167
|
|
|
723
|
-
//
|
|
724
|
-
const origAppendLog = appendLog;
|
|
725
|
-
appendLog = function(log) {
|
|
726
|
-
latestLog = log;
|
|
727
|
-
if (!isLogExpanded) {
|
|
728
|
-
updateLatestLogLine();
|
|
729
|
-
}
|
|
730
|
-
origAppendLog(log);
|
|
731
|
-
};
|
|
168
|
+
// Camera control and version modal moved to Alpine store methods
|
|
732
169
|
|
|
733
|
-
//
|
|
734
|
-
const origLoadLogs = loadLogs;
|
|
735
|
-
loadLogs = async function() {
|
|
736
|
-
await origLoadLogs();
|
|
737
|
-
if (!isLogExpanded) {
|
|
738
|
-
updateLatestLogLine();
|
|
739
|
-
}
|
|
740
|
-
};
|
|
170
|
+
// Camera capture moved to Alpine store method
|
|
741
171
|
|
|
742
|
-
// --- Initialize
|
|
743
|
-
document.addEventListener('DOMContentLoaded', async
|
|
744
|
-
// Initialize UI navigation
|
|
172
|
+
// --- Initialize ---
|
|
173
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
745
174
|
initNavigation();
|
|
746
|
-
|
|
747
|
-
// Initialize configuration management (loads config)
|
|
748
175
|
await initConfig();
|
|
749
|
-
|
|
750
|
-
// Initialize filter configuration
|
|
751
176
|
await initFilterConfig();
|
|
752
|
-
|
|
753
|
-
// Setup autofocus button (only once)
|
|
754
177
|
setupAutofocusButton();
|
|
755
|
-
|
|
756
|
-
// Update app URL links from loaded config
|
|
757
|
-
updateAppUrlLinks();
|
|
758
|
-
|
|
759
|
-
// Fetch and display version
|
|
760
178
|
fetchVersion();
|
|
761
|
-
|
|
762
|
-
// Check for updates on load and every hour
|
|
763
179
|
checkForUpdates();
|
|
764
|
-
setInterval(checkForUpdates, 3600000);
|
|
180
|
+
setInterval(checkForUpdates, 3600000);
|
|
765
181
|
|
|
766
|
-
// Wire up version click to open modal
|
|
767
|
-
const headerVersion = document.getElementById('headerVersion');
|
|
768
|
-
if (headerVersion) {
|
|
769
|
-
headerVersion.addEventListener('click', showVersionModal);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Wire up update indicator badge click to open modal
|
|
773
|
-
const updateIndicator = document.getElementById('updateIndicator');
|
|
774
|
-
if (updateIndicator) {
|
|
775
|
-
updateIndicator.addEventListener('click', showVersionModal);
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Connect WebSocket with handlers
|
|
779
182
|
connectWebSocket({
|
|
780
|
-
onStatus:
|
|
781
|
-
onLog:
|
|
782
|
-
onTasks:
|
|
783
|
-
onConnectionChange:
|
|
183
|
+
onStatus: updateStoreFromStatus,
|
|
184
|
+
onLog: appendLogToStore,
|
|
185
|
+
onTasks: updateStoreFromTasks,
|
|
186
|
+
onConnectionChange: updateStoreFromConnection
|
|
784
187
|
});
|
|
785
188
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
189
|
+
const tasksData = await getTasks();
|
|
190
|
+
const tasks = Array.isArray(tasksData) ? tasksData : (tasksData?.tasks || []);
|
|
191
|
+
updateStoreFromTasks(tasks);
|
|
789
192
|
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const endpoint = currentlyPaused ? '/api/tasks/resume' : '/api/tasks/pause';
|
|
193
|
+
const logsData = await getLogs(100);
|
|
194
|
+
const store = Alpine.store('citrascope');
|
|
195
|
+
store.logs = (logsData.logs || []).map(log => ({ ...log }));
|
|
196
|
+
if (store.logs.length > 0) {
|
|
197
|
+
store.latestLog = store.logs[store.logs.length - 1];
|
|
198
|
+
}
|
|
797
199
|
|
|
798
|
-
|
|
799
|
-
toggleButton.disabled = true;
|
|
800
|
-
const response = await fetch(endpoint, { method: 'POST' });
|
|
801
|
-
const result = await response.json();
|
|
200
|
+
startCountdownUpdater();
|
|
802
201
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
alert((result.error || 'Failed to toggle task processing') +
|
|
807
|
-
(response.status === 409 ? '' : ' - Unknown error'));
|
|
808
|
-
}
|
|
809
|
-
// State will be updated via WebSocket broadcast within 2 seconds
|
|
810
|
-
} catch (error) {
|
|
811
|
-
console.error('Error toggling processing:', error);
|
|
812
|
-
alert('Error toggling task processing');
|
|
813
|
-
} finally {
|
|
814
|
-
toggleButton.disabled = false;
|
|
815
|
-
}
|
|
816
|
-
});
|
|
202
|
+
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
203
|
+
for (const el of tooltipTriggerList) {
|
|
204
|
+
new bootstrap.Tooltip(el);
|
|
817
205
|
}
|
|
206
|
+
|
|
207
|
+
// Toggle switches now use Alpine @change directives in templates
|
|
818
208
|
});
|