citrascope 0.1.0__py3-none-any.whl → 0.3.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 +8 -5
- citrascope/api/abstract_api_client.py +7 -0
- citrascope/api/citra_api_client.py +30 -1
- citrascope/citra_scope_daemon.py +214 -61
- citrascope/hardware/abstract_astro_hardware_adapter.py +70 -2
- citrascope/hardware/adapter_registry.py +94 -0
- citrascope/hardware/indi_adapter.py +456 -16
- citrascope/hardware/kstars_dbus_adapter.py +179 -0
- citrascope/hardware/nina_adv_http_adapter.py +593 -0
- citrascope/hardware/nina_adv_http_survey_template.json +328 -0
- citrascope/logging/__init__.py +2 -1
- citrascope/logging/_citrascope_logger.py +80 -1
- citrascope/logging/web_log_handler.py +74 -0
- citrascope/settings/citrascope_settings.py +145 -0
- citrascope/settings/settings_file_manager.py +126 -0
- citrascope/tasks/runner.py +124 -28
- citrascope/tasks/scope/base_telescope_task.py +25 -10
- citrascope/tasks/scope/static_telescope_task.py +11 -3
- citrascope/web/__init__.py +1 -0
- citrascope/web/app.py +470 -0
- citrascope/web/server.py +123 -0
- citrascope/web/static/api.js +82 -0
- citrascope/web/static/app.js +500 -0
- citrascope/web/static/config.js +362 -0
- citrascope/web/static/img/citra.png +0 -0
- citrascope/web/static/img/favicon.png +0 -0
- citrascope/web/static/style.css +120 -0
- citrascope/web/static/websocket.js +127 -0
- citrascope/web/templates/dashboard.html +354 -0
- {citrascope-0.1.0.dist-info → citrascope-0.3.0.dist-info}/METADATA +68 -36
- citrascope-0.3.0.dist-info/RECORD +38 -0
- {citrascope-0.1.0.dist-info → citrascope-0.3.0.dist-info}/WHEEL +1 -1
- citrascope/settings/_citrascope_settings.py +0 -42
- citrascope-0.1.0.dist-info/RECORD +0 -21
- {citrascope-0.1.0.dist-info → citrascope-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// Configuration management for CitraScope
|
|
2
|
+
|
|
3
|
+
import { getConfig, saveConfig, getConfigStatus, getHardwareAdapters, getAdapterSchema } from './api.js';
|
|
4
|
+
|
|
5
|
+
let currentAdapterSchema = [];
|
|
6
|
+
export let currentConfig = {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Initialize configuration management
|
|
10
|
+
*/
|
|
11
|
+
export async function initConfig() {
|
|
12
|
+
// Populate hardware adapter dropdown
|
|
13
|
+
await loadAdapterOptions();
|
|
14
|
+
|
|
15
|
+
// Hardware adapter selection change
|
|
16
|
+
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
17
|
+
if (adapterSelect) {
|
|
18
|
+
adapterSelect.addEventListener('change', async function(e) {
|
|
19
|
+
const adapter = e.target.value;
|
|
20
|
+
if (adapter) {
|
|
21
|
+
await loadAdapterSchema(adapter);
|
|
22
|
+
} else {
|
|
23
|
+
document.getElementById('adapter-settings-container').innerHTML = '';
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Config form submission
|
|
29
|
+
const configForm = document.getElementById('configForm');
|
|
30
|
+
if (configForm) {
|
|
31
|
+
configForm.addEventListener('submit', saveConfiguration);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Load initial config
|
|
35
|
+
await loadConfiguration();
|
|
36
|
+
checkConfigStatus();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if configuration is needed and show setup wizard if not configured
|
|
41
|
+
*/
|
|
42
|
+
async function checkConfigStatus() {
|
|
43
|
+
try {
|
|
44
|
+
const status = await getConfigStatus();
|
|
45
|
+
|
|
46
|
+
if (!status.configured) {
|
|
47
|
+
// Show setup wizard if not configured
|
|
48
|
+
const wizardModal = new bootstrap.Modal(document.getElementById('setupWizard'));
|
|
49
|
+
wizardModal.show();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (status.error) {
|
|
53
|
+
showConfigError(status.error);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Failed to check config status:', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load available hardware adapters and populate dropdown
|
|
62
|
+
*/
|
|
63
|
+
async function loadAdapterOptions() {
|
|
64
|
+
try {
|
|
65
|
+
const data = await getHardwareAdapters();
|
|
66
|
+
const adapterSelect = document.getElementById('hardwareAdapterSelect');
|
|
67
|
+
|
|
68
|
+
if (adapterSelect && data.adapters) {
|
|
69
|
+
// Clear existing options except the first placeholder
|
|
70
|
+
while (adapterSelect.options.length > 1) {
|
|
71
|
+
adapterSelect.remove(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add options from API
|
|
75
|
+
data.adapters.forEach(adapterName => {
|
|
76
|
+
const option = document.createElement('option');
|
|
77
|
+
option.value = adapterName;
|
|
78
|
+
option.textContent = data.descriptions[adapterName] || adapterName;
|
|
79
|
+
adapterSelect.appendChild(option);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Failed to load hardware adapters:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load configuration from API and populate form
|
|
89
|
+
*/
|
|
90
|
+
async function loadConfiguration() {
|
|
91
|
+
try {
|
|
92
|
+
const config = await getConfig();
|
|
93
|
+
currentConfig = config; // Save for reuse when saving
|
|
94
|
+
|
|
95
|
+
// Display config file path
|
|
96
|
+
const configPathElement = document.getElementById('configFilePath');
|
|
97
|
+
if (configPathElement && config.config_file_path) {
|
|
98
|
+
configPathElement.textContent = config.config_file_path;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Display log file path
|
|
102
|
+
const logPathElement = document.getElementById('logFilePath');
|
|
103
|
+
if (logPathElement) {
|
|
104
|
+
if (config.log_file_path) {
|
|
105
|
+
logPathElement.textContent = config.log_file_path;
|
|
106
|
+
} else {
|
|
107
|
+
logPathElement.textContent = 'Disabled';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Display images directory path
|
|
112
|
+
const imagesDirElement = document.getElementById('imagesDirPath');
|
|
113
|
+
if (imagesDirElement && config.images_dir_path) {
|
|
114
|
+
imagesDirElement.textContent = config.images_dir_path;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Core fields
|
|
118
|
+
document.getElementById('personal_access_token').value = config.personal_access_token || '';
|
|
119
|
+
document.getElementById('telescopeId').value = config.telescope_id || '';
|
|
120
|
+
document.getElementById('hardwareAdapterSelect').value = config.hardware_adapter || '';
|
|
121
|
+
document.getElementById('logLevel').value = config.log_level || 'INFO';
|
|
122
|
+
document.getElementById('keep_images').checked = config.keep_images || false;
|
|
123
|
+
document.getElementById('file_logging_enabled').checked = config.file_logging_enabled !== undefined ? config.file_logging_enabled : true;
|
|
124
|
+
|
|
125
|
+
// Load adapter-specific settings if adapter is selected
|
|
126
|
+
if (config.hardware_adapter) {
|
|
127
|
+
await loadAdapterSchema(config.hardware_adapter);
|
|
128
|
+
populateAdapterSettings(config.adapter_settings || {});
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Failed to load config:', error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Load adapter schema and render settings form
|
|
137
|
+
*/
|
|
138
|
+
async function loadAdapterSchema(adapterName) {
|
|
139
|
+
try {
|
|
140
|
+
const data = await getAdapterSchema(adapterName);
|
|
141
|
+
currentAdapterSchema = data.schema || [];
|
|
142
|
+
renderAdapterSettings(currentAdapterSchema);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to load adapter schema:', error);
|
|
145
|
+
showConfigError(`Failed to load settings for ${adapterName}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render adapter-specific settings form
|
|
151
|
+
*/
|
|
152
|
+
function renderAdapterSettings(schema) {
|
|
153
|
+
const container = document.getElementById('adapter-settings-container');
|
|
154
|
+
|
|
155
|
+
if (!schema || schema.length === 0) {
|
|
156
|
+
container.innerHTML = '';
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let html = '<h5 class="mb-3">Adapter Settings</h5><div class="row g-3 mb-4">';
|
|
161
|
+
|
|
162
|
+
schema.forEach(field => {
|
|
163
|
+
const isRequired = field.required ? '<span class="text-danger">*</span>' : '';
|
|
164
|
+
const placeholder = field.placeholder || '';
|
|
165
|
+
const description = field.description || '';
|
|
166
|
+
const displayName = field.friendly_name || field.name;
|
|
167
|
+
|
|
168
|
+
html += '<div class="col-12 col-md-6">';
|
|
169
|
+
html += `<label for="adapter_${field.name}" class="form-label">${displayName} ${isRequired}</label>`;
|
|
170
|
+
|
|
171
|
+
if (field.type === 'bool') {
|
|
172
|
+
html += `<div class="form-check mt-2">`;
|
|
173
|
+
html += `<input class="form-check-input adapter-setting" type="checkbox" id="adapter_${field.name}" data-field="${field.name}" data-type="${field.type}">`;
|
|
174
|
+
html += `<label class="form-check-label" for="adapter_${field.name}">${description}</label>`;
|
|
175
|
+
html += `</div>`;
|
|
176
|
+
} else if (field.options && field.options.length > 0) {
|
|
177
|
+
const displayName = field.friendly_name || field.name;
|
|
178
|
+
html += `<select id="adapter_${field.name}" class="form-select adapter-setting" data-field="${field.name}" data-type="${field.type}" ${field.required ? 'required' : ''}>`;
|
|
179
|
+
html += `<option value="">-- Select ${displayName} --</option>`;
|
|
180
|
+
field.options.forEach(opt => {
|
|
181
|
+
html += `<option value="${opt}">${opt}</option>`;
|
|
182
|
+
});
|
|
183
|
+
html += `</select>`;
|
|
184
|
+
} else if (field.type === 'int' || field.type === 'float') {
|
|
185
|
+
const min = field.min !== undefined ? `min="${field.min}"` : '';
|
|
186
|
+
const max = field.max !== undefined ? `max="${field.max}"` : '';
|
|
187
|
+
html += `<input type="number" id="adapter_${field.name}" class="form-control adapter-setting" `;
|
|
188
|
+
html += `data-field="${field.name}" data-type="${field.type}" `;
|
|
189
|
+
html += `placeholder="${placeholder}" ${min} ${max} ${field.required ? 'required' : ''}>`;
|
|
190
|
+
} else {
|
|
191
|
+
// Default to text input
|
|
192
|
+
const pattern = field.pattern ? `pattern="${field.pattern}"` : '';
|
|
193
|
+
html += `<input type="text" id="adapter_${field.name}" class="form-control adapter-setting" `;
|
|
194
|
+
html += `data-field="${field.name}" data-type="${field.type}" `;
|
|
195
|
+
html += `placeholder="${placeholder}" ${pattern} ${field.required ? 'required' : ''}>`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (description && field.type !== 'bool') {
|
|
199
|
+
html += `<small class="text-muted">${description}</small>`;
|
|
200
|
+
}
|
|
201
|
+
html += '</div>';
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
html += '</div>';
|
|
205
|
+
container.innerHTML = html;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Populate adapter settings with values
|
|
210
|
+
*/
|
|
211
|
+
function populateAdapterSettings(adapterSettings) {
|
|
212
|
+
Object.entries(adapterSettings).forEach(([key, value]) => {
|
|
213
|
+
const input = document.getElementById(`adapter_${key}`);
|
|
214
|
+
if (input) {
|
|
215
|
+
if (input.type === 'checkbox') {
|
|
216
|
+
input.checked = value;
|
|
217
|
+
} else {
|
|
218
|
+
input.value = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Collect adapter settings from form
|
|
226
|
+
*/
|
|
227
|
+
function collectAdapterSettings() {
|
|
228
|
+
const settings = {};
|
|
229
|
+
const inputs = document.querySelectorAll('.adapter-setting');
|
|
230
|
+
|
|
231
|
+
inputs.forEach(input => {
|
|
232
|
+
const fieldName = input.dataset.field;
|
|
233
|
+
const fieldType = input.dataset.type;
|
|
234
|
+
let value;
|
|
235
|
+
|
|
236
|
+
if (input.type === 'checkbox') {
|
|
237
|
+
value = input.checked;
|
|
238
|
+
} else {
|
|
239
|
+
value = input.value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Type conversion
|
|
243
|
+
if (value !== '' && value !== null) {
|
|
244
|
+
if (fieldType === 'int') {
|
|
245
|
+
value = parseInt(value, 10);
|
|
246
|
+
} else if (fieldType === 'float') {
|
|
247
|
+
value = parseFloat(value);
|
|
248
|
+
} else if (fieldType === 'bool') {
|
|
249
|
+
// Already handled above
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
settings[fieldName] = value;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return settings;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Save configuration form handler
|
|
261
|
+
*/
|
|
262
|
+
async function saveConfiguration(event) {
|
|
263
|
+
event.preventDefault();
|
|
264
|
+
|
|
265
|
+
const saveButton = document.getElementById('saveConfigButton');
|
|
266
|
+
const buttonText = document.getElementById('saveButtonText');
|
|
267
|
+
const spinner = document.getElementById('saveButtonSpinner');
|
|
268
|
+
|
|
269
|
+
// Show loading state
|
|
270
|
+
saveButton.disabled = true;
|
|
271
|
+
spinner.style.display = 'inline-block';
|
|
272
|
+
buttonText.textContent = 'Saving...';
|
|
273
|
+
|
|
274
|
+
// Hide previous messages
|
|
275
|
+
hideConfigMessages();
|
|
276
|
+
|
|
277
|
+
const config = {
|
|
278
|
+
personal_access_token: document.getElementById('personal_access_token').value,
|
|
279
|
+
telescope_id: document.getElementById('telescopeId').value,
|
|
280
|
+
hardware_adapter: document.getElementById('hardwareAdapterSelect').value,
|
|
281
|
+
adapter_settings: collectAdapterSettings(),
|
|
282
|
+
log_level: document.getElementById('logLevel').value,
|
|
283
|
+
keep_images: document.getElementById('keep_images').checked,
|
|
284
|
+
file_logging_enabled: document.getElementById('file_logging_enabled').checked,
|
|
285
|
+
// Preserve API settings from loaded config
|
|
286
|
+
host: currentConfig.host || 'api.citra.space',
|
|
287
|
+
port: currentConfig.port || 443,
|
|
288
|
+
use_ssl: currentConfig.use_ssl !== undefined ? currentConfig.use_ssl : true,
|
|
289
|
+
max_task_retries: currentConfig.max_task_retries || 3,
|
|
290
|
+
initial_retry_delay_seconds: currentConfig.initial_retry_delay_seconds || 30,
|
|
291
|
+
max_retry_delay_seconds: currentConfig.max_retry_delay_seconds || 300,
|
|
292
|
+
log_retention_days: currentConfig.log_retention_days || 30,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const result = await saveConfig(config);
|
|
297
|
+
|
|
298
|
+
if (result.ok) {
|
|
299
|
+
showConfigSuccess(result.data.message || 'Configuration saved and applied successfully!');
|
|
300
|
+
} else {
|
|
301
|
+
showConfigError(result.data.error || result.data.message || 'Failed to save configuration');
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
showConfigError('Failed to save configuration: ' + error.message);
|
|
305
|
+
} finally {
|
|
306
|
+
// Reset button state
|
|
307
|
+
saveButton.disabled = false;
|
|
308
|
+
spinner.style.display = 'none';
|
|
309
|
+
buttonText.textContent = 'Save Configuration';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Show configuration error message
|
|
315
|
+
*/
|
|
316
|
+
function showConfigError(message) {
|
|
317
|
+
const errorDiv = document.getElementById('configError');
|
|
318
|
+
errorDiv.textContent = message;
|
|
319
|
+
errorDiv.style.display = 'block';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Show configuration success message
|
|
324
|
+
*/
|
|
325
|
+
function showConfigSuccess(message) {
|
|
326
|
+
const successDiv = document.getElementById('configSuccess');
|
|
327
|
+
successDiv.textContent = message;
|
|
328
|
+
successDiv.style.display = 'block';
|
|
329
|
+
|
|
330
|
+
// Auto-hide after 5 seconds
|
|
331
|
+
setTimeout(() => {
|
|
332
|
+
successDiv.style.display = 'none';
|
|
333
|
+
}, 5000);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Hide all configuration messages
|
|
338
|
+
*/
|
|
339
|
+
function hideConfigMessages() {
|
|
340
|
+
document.getElementById('configError').style.display = 'none';
|
|
341
|
+
document.getElementById('configSuccess').style.display = 'none';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Show configuration section (called from setup wizard)
|
|
346
|
+
*/
|
|
347
|
+
export function showConfigSection() {
|
|
348
|
+
// Close setup wizard modal
|
|
349
|
+
const wizardModal = bootstrap.Modal.getInstance(document.getElementById('setupWizard'));
|
|
350
|
+
if (wizardModal) {
|
|
351
|
+
wizardModal.hide();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Show config section
|
|
355
|
+
const configLink = document.querySelector('a[data-section="config"]');
|
|
356
|
+
if (configLink) {
|
|
357
|
+
configLink.click();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Make showConfigSection available globally for onclick handlers in HTML
|
|
362
|
+
window.showConfigSection = showConfigSection;
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* CitraScope Web Interface - Custom Styles
|
|
2
|
+
* Bootstrap 5 handles most styling; these are overrides and additions only
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* Log terminal container - custom scrollbar and background watermark */
|
|
6
|
+
.log-container {
|
|
7
|
+
background: #0d1117 url('/static/img/citra.png') no-repeat 85% center;
|
|
8
|
+
background-size: auto 70%;
|
|
9
|
+
background-blend-mode: soft-light;
|
|
10
|
+
opacity: 1;
|
|
11
|
+
font-family: monospace;
|
|
12
|
+
line-height: 1.5;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Dark scrollbar for log container */
|
|
16
|
+
.log-container::-webkit-scrollbar {
|
|
17
|
+
width: 12px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.log-container::-webkit-scrollbar-track {
|
|
21
|
+
background: #0d1117;
|
|
22
|
+
border-radius: 5px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.log-container::-webkit-scrollbar-thumb {
|
|
26
|
+
background: #30363d;
|
|
27
|
+
border-radius: 5px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.log-container::-webkit-scrollbar-thumb:hover {
|
|
31
|
+
background: #484f58;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Body padding for fixed bottom log terminal */
|
|
35
|
+
body {
|
|
36
|
+
padding-bottom: 100px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Logo styling */
|
|
40
|
+
.logo-img {
|
|
41
|
+
height: 1.5em;
|
|
42
|
+
width: auto;
|
|
43
|
+
vertical-align: middle;
|
|
44
|
+
display: inline-block;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Status badge container */
|
|
48
|
+
.status-badge-container {
|
|
49
|
+
gap: 0.5em;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Muted text color for placeholders */
|
|
53
|
+
.text-muted-dark {
|
|
54
|
+
color: #a0aec0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Log accordion customization */
|
|
58
|
+
.log-accordion-button {
|
|
59
|
+
border-bottom: 1px solid #444 !important;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.log-accordion-body {
|
|
63
|
+
max-height: 40vh;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.log-container {
|
|
68
|
+
max-height: 40vh;
|
|
69
|
+
overflow-y: auto;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.log-latest-line {
|
|
73
|
+
font-family: monospace;
|
|
74
|
+
color: #e2e8f0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Task display */
|
|
78
|
+
.task-title {
|
|
79
|
+
font-size: 1.1em;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.no-task-message {
|
|
83
|
+
color: #a0aec0;
|
|
84
|
+
margin: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Ground station link */
|
|
88
|
+
.ground-station-link {
|
|
89
|
+
color: #4299e1;
|
|
90
|
+
text-decoration: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ground-station-link:hover {
|
|
94
|
+
color: #63b3ed;
|
|
95
|
+
text-decoration: underline;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Log entry components */
|
|
99
|
+
.log-entry {
|
|
100
|
+
margin-bottom: 4px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.log-timestamp {
|
|
104
|
+
color: #a0aec0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.log-level {
|
|
108
|
+
font-weight: bold;
|
|
109
|
+
margin: 0 8px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.log-level-DEBUG { color: #a0aec0; }
|
|
113
|
+
.log-level-INFO { color: #48bb78; }
|
|
114
|
+
.log-level-WARNING { color: #f6ad55; }
|
|
115
|
+
.log-level-ERROR { color: #f56565; }
|
|
116
|
+
.log-level-CRITICAL { color: #c53030; }
|
|
117
|
+
|
|
118
|
+
.log-message {
|
|
119
|
+
color: #e2e8f0;
|
|
120
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// WebSocket connection management for CitraScope
|
|
2
|
+
|
|
3
|
+
let ws = null;
|
|
4
|
+
let reconnectAttempts = 0;
|
|
5
|
+
let reconnectTimer = null;
|
|
6
|
+
let connectionTimer = null;
|
|
7
|
+
const reconnectDelay = 5000; // Fixed 5 second delay between reconnect attempts
|
|
8
|
+
const connectionTimeout = 5000; // 5 second timeout for connection attempts
|
|
9
|
+
|
|
10
|
+
// Callbacks for handling messages
|
|
11
|
+
let onStatusUpdate = null;
|
|
12
|
+
let onLogMessage = null;
|
|
13
|
+
let onTasksUpdate = null;
|
|
14
|
+
let onConnectionChange = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize WebSocket connection
|
|
18
|
+
* @param {object} handlers - Event handlers {onStatus, onLog, onTasks, onConnectionChange}
|
|
19
|
+
*/
|
|
20
|
+
export function connectWebSocket(handlers = {}) {
|
|
21
|
+
onStatusUpdate = handlers.onStatus || null;
|
|
22
|
+
onLogMessage = handlers.onLog || null;
|
|
23
|
+
onTasksUpdate = handlers.onTasks || null;
|
|
24
|
+
onConnectionChange = handlers.onConnectionChange || null;
|
|
25
|
+
|
|
26
|
+
connect();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function connect() {
|
|
30
|
+
// Clear any existing reconnect timer
|
|
31
|
+
if (reconnectTimer) {
|
|
32
|
+
clearTimeout(reconnectTimer);
|
|
33
|
+
reconnectTimer = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Clear any existing connection timeout
|
|
37
|
+
if (connectionTimer) {
|
|
38
|
+
clearTimeout(connectionTimer);
|
|
39
|
+
connectionTimer = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
43
|
+
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
|
44
|
+
|
|
45
|
+
console.log('Attempting WebSocket connection to:', wsUrl);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Close existing connection if any
|
|
49
|
+
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
50
|
+
ws.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ws = new WebSocket(wsUrl);
|
|
54
|
+
|
|
55
|
+
// Set a timeout for connection attempt
|
|
56
|
+
connectionTimer = setTimeout(() => {
|
|
57
|
+
console.log('WebSocket connection timeout');
|
|
58
|
+
if (ws && ws.readyState !== WebSocket.OPEN) {
|
|
59
|
+
ws.close();
|
|
60
|
+
scheduleReconnect();
|
|
61
|
+
}
|
|
62
|
+
}, connectionTimeout);
|
|
63
|
+
|
|
64
|
+
ws.onopen = () => {
|
|
65
|
+
console.log('WebSocket connected successfully');
|
|
66
|
+
if (connectionTimer) {
|
|
67
|
+
clearTimeout(connectionTimer);
|
|
68
|
+
connectionTimer = null;
|
|
69
|
+
}
|
|
70
|
+
reconnectAttempts = 0;
|
|
71
|
+
notifyConnectionChange(true);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
ws.onmessage = (event) => {
|
|
75
|
+
const message = JSON.parse(event.data);
|
|
76
|
+
if (message.type === 'status' && onStatusUpdate) {
|
|
77
|
+
onStatusUpdate(message.data);
|
|
78
|
+
} else if (message.type === 'log' && onLogMessage) {
|
|
79
|
+
onLogMessage(message.data);
|
|
80
|
+
} else if (message.type === 'tasks' && onTasksUpdate) {
|
|
81
|
+
onTasksUpdate(message.data);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
ws.onclose = (event) => {
|
|
86
|
+
console.log('WebSocket closed', event.code, event.reason);
|
|
87
|
+
if (connectionTimer) {
|
|
88
|
+
clearTimeout(connectionTimer);
|
|
89
|
+
connectionTimer = null;
|
|
90
|
+
}
|
|
91
|
+
ws = null;
|
|
92
|
+
scheduleReconnect();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
ws.onerror = (error) => {
|
|
96
|
+
console.error('WebSocket error:', error);
|
|
97
|
+
console.log('WebSocket readyState:', ws?.readyState);
|
|
98
|
+
// Close will be called automatically after error
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Failed to create WebSocket:', error);
|
|
102
|
+
if (connectionTimer) {
|
|
103
|
+
clearTimeout(connectionTimer);
|
|
104
|
+
connectionTimer = null;
|
|
105
|
+
}
|
|
106
|
+
ws = null;
|
|
107
|
+
scheduleReconnect();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function scheduleReconnect() {
|
|
112
|
+
// Fixed 5 second delay between reconnect attempts
|
|
113
|
+
const delay = reconnectDelay;
|
|
114
|
+
|
|
115
|
+
notifyConnectionChange(false, 'reconnecting');
|
|
116
|
+
|
|
117
|
+
console.log(`Scheduling reconnect in ${delay/1000}s... (attempt ${reconnectAttempts + 1})`);
|
|
118
|
+
|
|
119
|
+
reconnectAttempts++;
|
|
120
|
+
reconnectTimer = setTimeout(connect, delay);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function notifyConnectionChange(connected, reconnectInfo = '') {
|
|
124
|
+
if (onConnectionChange) {
|
|
125
|
+
onConnectionChange(connected, reconnectInfo);
|
|
126
|
+
}
|
|
127
|
+
}
|