citrascope 0.8.0__py3-none-any.whl → 0.9.1__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/hardware/abstract_astro_hardware_adapter.py +17 -0
- citrascope/hardware/adapter_registry.py +5 -0
- citrascope/hardware/devices/camera/abstract_camera.py +12 -0
- citrascope/hardware/devices/camera/usb_camera.py +20 -15
- citrascope/hardware/devices/camera/ximea_camera.py +22 -10
- citrascope/hardware/direct_hardware_adapter.py +21 -3
- citrascope/hardware/dummy_adapter.py +202 -0
- citrascope/time/__init__.py +2 -1
- citrascope/time/time_health.py +8 -1
- citrascope/time/time_monitor.py +27 -5
- citrascope/time/time_sources.py +199 -0
- citrascope/web/app.py +31 -9
- citrascope/web/static/app.js +118 -988
- citrascope/web/static/components.js +136 -0
- citrascope/web/static/config.js +212 -508
- citrascope/web/static/formatters.js +129 -0
- citrascope/web/static/store-init.js +216 -0
- citrascope/web/static/style.css +5 -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 +72 -444
- {citrascope-0.8.0.dist-info → citrascope-0.9.1.dist-info}/METADATA +3 -2
- {citrascope-0.8.0.dist-info → citrascope-0.9.1.dist-info}/RECORD +27 -20
- {citrascope-0.8.0.dist-info → citrascope-0.9.1.dist-info}/WHEEL +0 -0
- {citrascope-0.8.0.dist-info → citrascope-0.9.1.dist-info}/entry_points.txt +0 -0
- {citrascope-0.8.0.dist-info → citrascope-0.9.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CitraScope Formatter Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared formatting functions for the dashboard UI.
|
|
5
|
+
* These functions are exposed in the Alpine.js store for use in templates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strip ANSI color codes from text
|
|
10
|
+
* @param {string} text - Text containing ANSI codes
|
|
11
|
+
* @returns {string} Text with ANSI codes removed
|
|
12
|
+
*/
|
|
13
|
+
export function stripAnsiCodes(text) {
|
|
14
|
+
const esc = String.fromCharCode(27);
|
|
15
|
+
return text.replace(new RegExp(esc + '\\[\\d+m', 'g'), '').replace(/\[\d+m/g, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format ISO date string to local time
|
|
20
|
+
* @param {string} isoString - ISO 8601 date string
|
|
21
|
+
* @returns {string} Formatted local time string
|
|
22
|
+
*/
|
|
23
|
+
export function formatLocalTime(isoString) {
|
|
24
|
+
const date = new Date(isoString);
|
|
25
|
+
return date.toLocaleString(undefined, {
|
|
26
|
+
month: 'short',
|
|
27
|
+
day: 'numeric',
|
|
28
|
+
hour: '2-digit',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
second: '2-digit',
|
|
31
|
+
hour12: true
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format milliseconds as countdown string
|
|
37
|
+
* @param {number} milliseconds - Time in milliseconds
|
|
38
|
+
* @returns {string} Formatted countdown string (e.g., "2h 30m 15s")
|
|
39
|
+
*/
|
|
40
|
+
export function formatCountdown(milliseconds) {
|
|
41
|
+
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
42
|
+
if (totalSeconds < 0) return 'Starting soon...';
|
|
43
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
44
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
45
|
+
const seconds = totalSeconds % 60;
|
|
46
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
47
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
48
|
+
return `${seconds}s`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format elapsed time for "X ago" display
|
|
53
|
+
* @param {number} milliseconds - Elapsed time in milliseconds
|
|
54
|
+
* @returns {string} Human-readable elapsed time (e.g., "2 hours ago")
|
|
55
|
+
*/
|
|
56
|
+
export function formatElapsedTime(milliseconds) {
|
|
57
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
58
|
+
const minutes = Math.floor(seconds / 60);
|
|
59
|
+
const hours = Math.floor(minutes / 60);
|
|
60
|
+
const days = Math.floor(hours / 24);
|
|
61
|
+
if (days > 0) return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
62
|
+
if (hours > 0) return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
63
|
+
if (minutes > 0) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
|
64
|
+
return 'just now';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format minutes as "Xh Ym" display
|
|
69
|
+
* @param {number} minutes - Time in minutes
|
|
70
|
+
* @returns {string} Formatted time string (e.g., "2h 30m")
|
|
71
|
+
*/
|
|
72
|
+
export function formatMinutes(minutes) {
|
|
73
|
+
const hours = Math.floor(minutes / 60);
|
|
74
|
+
const mins = Math.floor(minutes % 60);
|
|
75
|
+
if (hours > 0) return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
76
|
+
return `${mins}m`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format last autofocus timestamp
|
|
81
|
+
* @param {Object} status - Status object containing last_autofocus_timestamp
|
|
82
|
+
* @returns {string} Formatted autofocus time or "Never"
|
|
83
|
+
*/
|
|
84
|
+
export function formatLastAutofocus(status) {
|
|
85
|
+
if (!status || !status.last_autofocus_timestamp) return 'Never';
|
|
86
|
+
const elapsed = Date.now() - status.last_autofocus_timestamp * 1000;
|
|
87
|
+
return formatElapsedTime(elapsed);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format time offset with source information - compact format for status pill
|
|
92
|
+
* @param {Object} timeHealth - Time health object with offset_ms, source, and optional metadata
|
|
93
|
+
* @returns {string} Formatted time offset (e.g., "17ns, 10 sats" or "+2ms, ntp")
|
|
94
|
+
*/
|
|
95
|
+
export function formatTimeOffset(timeHealth) {
|
|
96
|
+
if (!timeHealth || timeHealth.offset_ms == null) return 'Unknown';
|
|
97
|
+
|
|
98
|
+
const o = timeHealth.offset_ms;
|
|
99
|
+
const abs = Math.abs(o);
|
|
100
|
+
const s = o >= 0 ? '+' : '';
|
|
101
|
+
|
|
102
|
+
// Format offset with appropriate units
|
|
103
|
+
let offsetStr;
|
|
104
|
+
if (abs < 0.001) {
|
|
105
|
+
// Sub-microsecond: show as nanoseconds
|
|
106
|
+
offsetStr = `${s}${Math.round(abs * 1000000)}ns`;
|
|
107
|
+
} else if (abs < 1) {
|
|
108
|
+
// Sub-millisecond: show as microseconds
|
|
109
|
+
offsetStr = `${s}${Math.round(abs * 1000)}µs`;
|
|
110
|
+
} else if (abs < 1000) {
|
|
111
|
+
// Milliseconds
|
|
112
|
+
offsetStr = `${s}${abs.toFixed(0)}ms`;
|
|
113
|
+
} else {
|
|
114
|
+
// Seconds
|
|
115
|
+
offsetStr = `${s}${(abs / 1000).toFixed(1)}s`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add source/satellite info
|
|
119
|
+
if (timeHealth.source === 'gps' && timeHealth.metadata?.satellites != null) {
|
|
120
|
+
// GPS with satellite count
|
|
121
|
+
return `${offsetStr}, ${timeHealth.metadata.satellites} sats`;
|
|
122
|
+
} else if (timeHealth.source && timeHealth.source !== 'unknown') {
|
|
123
|
+
// Other sources (ntp, chrony)
|
|
124
|
+
return `${offsetStr}, ${timeHealth.source}`;
|
|
125
|
+
} else {
|
|
126
|
+
// No source info
|
|
127
|
+
return offsetStr;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CitraScope Alpine store - must register BEFORE Alpine starts.
|
|
3
|
+
* Load this script before Alpine.js so the alpine:init listener is attached in time.
|
|
4
|
+
*/
|
|
5
|
+
import * as formatters from './formatters.js';
|
|
6
|
+
import * as components from './components.js';
|
|
7
|
+
|
|
8
|
+
(() => {
|
|
9
|
+
document.addEventListener('alpine:init', () => {
|
|
10
|
+
// Register Alpine components FIRST (before Alpine starts processing the DOM)
|
|
11
|
+
window.Alpine.data('adapterField', components.adapterField);
|
|
12
|
+
window.Alpine.data('taskRow', components.taskRow);
|
|
13
|
+
window.Alpine.data('filterRow', components.filterRow);
|
|
14
|
+
window.Alpine.data('logEntry', components.logEntry);
|
|
15
|
+
|
|
16
|
+
// Register store
|
|
17
|
+
window.Alpine.store('citrascope', {
|
|
18
|
+
status: {},
|
|
19
|
+
tasks: [],
|
|
20
|
+
logs: [],
|
|
21
|
+
latestLog: null,
|
|
22
|
+
wsConnected: false,
|
|
23
|
+
wsReconnecting: false,
|
|
24
|
+
currentTaskId: null,
|
|
25
|
+
isTaskActive: false,
|
|
26
|
+
nextTaskStartTime: null,
|
|
27
|
+
countdown: '',
|
|
28
|
+
config: {},
|
|
29
|
+
apiEndpoint: 'production',
|
|
30
|
+
hardwareAdapters: [], // [{value, label}]
|
|
31
|
+
filters: {},
|
|
32
|
+
savedAdapter: null,
|
|
33
|
+
enabledFilters: [],
|
|
34
|
+
filterConfigVisible: false,
|
|
35
|
+
filterAdapterChangeMessageVisible: false,
|
|
36
|
+
currentSection: 'monitoring',
|
|
37
|
+
version: '',
|
|
38
|
+
updateIndicator: '',
|
|
39
|
+
versionCheckState: 'idle',
|
|
40
|
+
versionCheckResult: null,
|
|
41
|
+
|
|
42
|
+
// Loading states for async operations
|
|
43
|
+
isSavingConfig: false,
|
|
44
|
+
isCapturing: false,
|
|
45
|
+
isAutofocusing: false,
|
|
46
|
+
captureResult: null,
|
|
47
|
+
exposureDuration: 0.1,
|
|
48
|
+
|
|
49
|
+
// Spread all formatter functions from shared module
|
|
50
|
+
...formatters,
|
|
51
|
+
|
|
52
|
+
// Unified adapter fields (schema + values merged)
|
|
53
|
+
adapterFields: [],
|
|
54
|
+
|
|
55
|
+
// Computed property: Group adapter fields by their group property
|
|
56
|
+
get groupedAdapterFields() {
|
|
57
|
+
const grouped = {};
|
|
58
|
+
this.adapterFields.forEach(f => {
|
|
59
|
+
const g = f.group || 'General';
|
|
60
|
+
if (!grouped[g]) grouped[g] = [];
|
|
61
|
+
grouped[g].push(f);
|
|
62
|
+
});
|
|
63
|
+
return Object.entries(grouped);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Store methods
|
|
67
|
+
async captureImage() {
|
|
68
|
+
if (Number.isNaN(this.exposureDuration) || this.exposureDuration <= 0) {
|
|
69
|
+
// Import createToast from config.js
|
|
70
|
+
const { createToast } = await import('./config.js');
|
|
71
|
+
createToast('Invalid exposure duration', 'danger', false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.isCapturing = true;
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch('/api/camera/capture', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ duration: this.exposureDuration })
|
|
81
|
+
});
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
|
|
84
|
+
if (response.ok && data.success) {
|
|
85
|
+
this.captureResult = data;
|
|
86
|
+
const { createToast } = await import('./config.js');
|
|
87
|
+
createToast('Image captured successfully', 'success', true);
|
|
88
|
+
} else {
|
|
89
|
+
const { createToast } = await import('./config.js');
|
|
90
|
+
createToast(data.error || 'Failed to capture image', 'danger', false);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Capture error:', error);
|
|
94
|
+
const { createToast } = await import('./config.js');
|
|
95
|
+
createToast('Failed to capture image: ' + error.message, 'danger', false);
|
|
96
|
+
} finally {
|
|
97
|
+
this.isCapturing = false;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
async toggleProcessing(enabled) {
|
|
102
|
+
const endpoint = enabled ? '/api/tasks/resume' : '/api/tasks/pause';
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(endpoint, { method: 'POST' });
|
|
105
|
+
const result = await response.json();
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
alert(result.error || 'Failed to toggle task processing');
|
|
108
|
+
// Revert on error
|
|
109
|
+
this.status.processing_active = !enabled;
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error toggling processing:', error);
|
|
113
|
+
alert('Error toggling task processing');
|
|
114
|
+
this.status.processing_active = !enabled;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async toggleAutomatedScheduling(enabled) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch('/api/telescope/automated-scheduling', {
|
|
121
|
+
method: 'PATCH',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({ enabled: enabled })
|
|
124
|
+
});
|
|
125
|
+
const result = await response.json();
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
alert(result.error || 'Failed to toggle automated scheduling');
|
|
128
|
+
// Revert on error
|
|
129
|
+
this.status.automated_scheduling = !enabled;
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('Error toggling automated scheduling:', error);
|
|
133
|
+
alert('Error toggling automated scheduling');
|
|
134
|
+
this.status.automated_scheduling = !enabled;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
showCameraControl() {
|
|
139
|
+
this.captureResult = null; // Reset capture result when opening modal
|
|
140
|
+
const modal = new bootstrap.Modal(document.getElementById('cameraControlModal'));
|
|
141
|
+
modal.show();
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async showVersionModal() {
|
|
145
|
+
this.versionCheckState = 'loading';
|
|
146
|
+
this.versionCheckResult = null;
|
|
147
|
+
|
|
148
|
+
const modal = new bootstrap.Modal(document.getElementById('versionModal'));
|
|
149
|
+
modal.show();
|
|
150
|
+
|
|
151
|
+
// Check for updates (inline implementation)
|
|
152
|
+
try {
|
|
153
|
+
const versionResponse = await fetch('/api/version');
|
|
154
|
+
const versionData = await versionResponse.json();
|
|
155
|
+
const currentVersion = versionData.version;
|
|
156
|
+
|
|
157
|
+
const githubResponse = await fetch('https://api.github.com/repos/citra-space/citrascope/releases/latest');
|
|
158
|
+
if (!githubResponse.ok) {
|
|
159
|
+
this.versionCheckState = 'error';
|
|
160
|
+
this.versionCheckResult = { status: 'error', currentVersion };
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const releaseData = await githubResponse.json();
|
|
165
|
+
const latestVersion = releaseData.tag_name.replace(/^v/, '');
|
|
166
|
+
const releaseUrl = releaseData.html_url;
|
|
167
|
+
|
|
168
|
+
if (currentVersion === 'development' || currentVersion === 'unknown') {
|
|
169
|
+
this.updateIndicator = '';
|
|
170
|
+
this.versionCheckState = 'up-to-date';
|
|
171
|
+
this.versionCheckResult = { status: 'up-to-date', currentVersion };
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Compare versions
|
|
176
|
+
const v1 = latestVersion.split('.').map(n => parseInt(n) || 0);
|
|
177
|
+
const v2 = currentVersion.split('.').map(n => parseInt(n) || 0);
|
|
178
|
+
const maxLen = Math.max(v1.length, v2.length);
|
|
179
|
+
let comparison = 0;
|
|
180
|
+
for (let i = 0; i < maxLen; i++) {
|
|
181
|
+
const num1 = v1[i] || 0;
|
|
182
|
+
const num2 = v2[i] || 0;
|
|
183
|
+
if (num1 > num2) { comparison = 1; break; }
|
|
184
|
+
if (num1 < num2) { comparison = -1; break; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (comparison > 0) {
|
|
188
|
+
this.updateIndicator = `${latestVersion} Available!`;
|
|
189
|
+
this.versionCheckState = 'update-available';
|
|
190
|
+
this.versionCheckResult = { status: 'update-available', currentVersion, latestVersion, releaseUrl };
|
|
191
|
+
} else {
|
|
192
|
+
this.updateIndicator = '';
|
|
193
|
+
this.versionCheckState = 'up-to-date';
|
|
194
|
+
this.versionCheckResult = { status: 'up-to-date', currentVersion };
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.debug('Update check failed:', error);
|
|
198
|
+
this.versionCheckState = 'error';
|
|
199
|
+
this.versionCheckResult = { status: 'error', currentVersion: 'unknown' };
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
showConfigSection() {
|
|
204
|
+
// Close setup wizard modal
|
|
205
|
+
const wizardModal = bootstrap.Modal.getInstance(document.getElementById('setupWizard'));
|
|
206
|
+
if (wizardModal) {
|
|
207
|
+
wizardModal.hide();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Navigate to config section
|
|
211
|
+
this.currentSection = 'config';
|
|
212
|
+
window.location.hash = 'config';
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
})();
|
citrascope/web/static/style.css
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* Bootstrap 5 handles most styling; these are overrides and additions only
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/* Alpine.js: hide elements until Alpine has processed them */
|
|
6
|
+
[x-cloak] {
|
|
7
|
+
display: none !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
/* Log terminal container - custom scrollbar and background watermark */
|
|
6
11
|
.log-container {
|
|
7
12
|
background: #0d1117 url('/static/img/citra.png') no-repeat 85% center;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<div class="container my-4" id="configSection" x-show="$store.citrascope.currentSection === 'config'" x-cloak>
|
|
2
|
+
<form @submit.prevent="window.saveConfiguration($event)">
|
|
3
|
+
<div class="row g-3 mb-3">
|
|
4
|
+
<!-- API Configuration Card -->
|
|
5
|
+
<div class="col-12" x-data="{ apiCardExpanded: $persist(true).as('apiCard') }">
|
|
6
|
+
<div class="card bg-dark text-light border-secondary">
|
|
7
|
+
<div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" @click="apiCardExpanded = !apiCardExpanded">
|
|
8
|
+
<div class="d-flex align-items-center gap-2">
|
|
9
|
+
<i class="bi bi-cloud-arrow-up"></i>
|
|
10
|
+
<h5 class="mb-0">API Configuration</h5>
|
|
11
|
+
<small class="text-muted" x-show="!apiCardExpanded" x-text="$store.citrascope.apiEndpoint === 'production' ? 'Production' : ($store.citrascope.apiEndpoint === 'development' ? 'Development' : 'Custom')"></small>
|
|
12
|
+
</div>
|
|
13
|
+
<i class="bi" :class="apiCardExpanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card-body" x-show="apiCardExpanded">
|
|
16
|
+
<div class="row g-3">
|
|
17
|
+
<div class="col-12">
|
|
18
|
+
<label for="apiEndpoint" class="form-label">API Endpoint</label>
|
|
19
|
+
<select id="apiEndpoint" class="form-select" x-model="$store.citrascope.apiEndpoint">
|
|
20
|
+
<option value="production">Production (api.citra.space)</option>
|
|
21
|
+
<option value="development">Development (dev.api.citra.space)</option>
|
|
22
|
+
<option value="custom">Custom</option>
|
|
23
|
+
</select>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="col-12" id="customHostContainer" x-show="$store.citrascope.apiEndpoint === 'custom'">
|
|
26
|
+
<label for="customHost" class="form-label">Custom API Host</label>
|
|
27
|
+
<input type="text" id="customHost" class="form-control" placeholder="api.example.com" x-model="$store.citrascope.config.host">
|
|
28
|
+
<div class="row g-2 mt-2">
|
|
29
|
+
<div class="col-6">
|
|
30
|
+
<label for="customPort" class="form-label small">Port</label>
|
|
31
|
+
<input type="number" id="customPort" class="form-control" placeholder="443" x-model.number="$store.citrascope.config.port">
|
|
32
|
+
</div>
|
|
33
|
+
<div class="col-6 d-flex align-items-end">
|
|
34
|
+
<div class="form-check">
|
|
35
|
+
<input class="form-check-input" type="checkbox" id="customUseSsl" x-model="$store.citrascope.config.use_ssl">
|
|
36
|
+
<label class="form-check-label" for="customUseSsl">
|
|
37
|
+
Use SSL
|
|
38
|
+
</label>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="col-12 col-md-6">
|
|
44
|
+
<label for="personal_access_token" class="form-label">Personal Access Token <span class="text-danger">*</span></label>
|
|
45
|
+
<input type="password" id="personal_access_token" class="form-control" placeholder="Enter your Citra API token" required x-model="$store.citrascope.config.personal_access_token">
|
|
46
|
+
<small class="text-muted">Get your token and telescope ID from <a :href="$store.citrascope.config.app_url || ''" target="_blank" x-text="($store.citrascope.config.app_url || '').replace('https://', '')"></a></small>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="col-12 col-md-6">
|
|
49
|
+
<label for="telescopeId" class="form-label">Telescope ID <span class="text-danger">*</span></label>
|
|
50
|
+
<input type="text" id="telescopeId" class="form-control" placeholder="Enter telescope ID" required x-model="$store.citrascope.config.telescope_id">
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
{% include '_config_hardware.html' %}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
<!-- Time Synchronization Settings Card -->
|
|
62
|
+
<div class="col-12" x-data="{ timeCardExpanded: $persist(true).as('timeCard') }">
|
|
63
|
+
<div class="card bg-dark text-light border-secondary">
|
|
64
|
+
<div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" @click="timeCardExpanded = !timeCardExpanded">
|
|
65
|
+
<div class="d-flex align-items-center gap-2">
|
|
66
|
+
<i class="bi bi-clock-history"></i>
|
|
67
|
+
<h5 class="mb-0">Time Synchronization</h5>
|
|
68
|
+
<small class="text-muted" x-show="!timeCardExpanded" x-text="'Check every ' + ($store.citrascope.config.time_check_interval_minutes || 5) + 'min • ' + ($store.citrascope.config.time_offset_pause_ms || 500) + 'ms threshold'"></small>
|
|
69
|
+
</div>
|
|
70
|
+
<i class="bi" :class="timeCardExpanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="card-body" x-show="timeCardExpanded">
|
|
73
|
+
<div class="row g-3">
|
|
74
|
+
<div class="col-12 col-md-6">
|
|
75
|
+
<label for="time_offset_pause_ms" class="form-label">Pause Threshold (ms)</label>
|
|
76
|
+
<input type="number" class="form-control form-control-sm" id="time_offset_pause_ms" min="1" max="10000" step="1" x-model.number="$store.citrascope.config.time_offset_pause_ms">
|
|
77
|
+
<small class="text-muted">Clock drift that triggers task pause</small>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="col-12 col-md-6">
|
|
80
|
+
<label for="time_check_interval_minutes" class="form-label">Check Interval (minutes)</label>
|
|
81
|
+
<input type="number" class="form-control form-control-sm" id="time_check_interval_minutes" min="1" max="60" step="1" x-model.number="$store.citrascope.config.time_check_interval_minutes">
|
|
82
|
+
<small class="text-muted">How often to check time synchronization</small>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Logging Settings Card -->
|
|
90
|
+
<div class="col-12" x-data="{ loggingCardExpanded: $persist(true).as('loggingCard') }">
|
|
91
|
+
<div class="card bg-dark text-light border-secondary">
|
|
92
|
+
<div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" @click="loggingCardExpanded = !loggingCardExpanded">
|
|
93
|
+
<div class="d-flex align-items-center gap-2">
|
|
94
|
+
<i class="bi bi-file-earmark-text"></i>
|
|
95
|
+
<h5 class="mb-0">Logging Settings</h5>
|
|
96
|
+
<small class="text-muted" x-show="!loggingCardExpanded" x-text="($store.citrascope.config.log_level || 'INFO') + ($store.citrascope.config.file_logging_enabled !== false ? ' • File logging' : ' • Console only')"></small>
|
|
97
|
+
</div>
|
|
98
|
+
<i class="bi" :class="loggingCardExpanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="card-body" x-show="loggingCardExpanded">
|
|
101
|
+
<div class="row g-3">
|
|
102
|
+
<div class="col-12 col-md-6">
|
|
103
|
+
<label for="logLevel" class="form-label">Log Level</label>
|
|
104
|
+
<select id="logLevel" class="form-select" x-model="$store.citrascope.config.log_level">
|
|
105
|
+
<option value="DEBUG">DEBUG</option>
|
|
106
|
+
<option value="INFO">INFO</option>
|
|
107
|
+
<option value="WARNING">WARNING</option>
|
|
108
|
+
<option value="ERROR">ERROR</option>
|
|
109
|
+
</select>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="col-12 col-md-6">
|
|
112
|
+
<div class="form-check mt-4">
|
|
113
|
+
<input class="form-check-input" type="checkbox" id="file_logging_enabled" x-model="$store.citrascope.config.file_logging_enabled">
|
|
114
|
+
<label class="form-check-label" for="file_logging_enabled">
|
|
115
|
+
Enable file logging
|
|
116
|
+
</label>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="col-12">
|
|
120
|
+
<small class="text-muted">
|
|
121
|
+
Log file: <span class="text-secondary" x-text="$store.citrascope.config.log_file_path || 'Disabled'">Loading...</span>
|
|
122
|
+
</small>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Task Settings Card -->
|
|
130
|
+
<div class="col-12" x-data="{ taskCardExpanded: $persist(true).as('taskCard') }">
|
|
131
|
+
<div class="card bg-dark text-light border-secondary">
|
|
132
|
+
<div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" @click="taskCardExpanded = !taskCardExpanded">
|
|
133
|
+
<div class="d-flex align-items-center gap-2">
|
|
134
|
+
<i class="bi bi-gear"></i>
|
|
135
|
+
<h5 class="mb-0">Task Settings</h5>
|
|
136
|
+
<small class="text-muted" x-show="!taskCardExpanded" x-text="$store.citrascope.config.keep_images ? 'Keep images' : 'Delete images after upload'"></small>
|
|
137
|
+
</div>
|
|
138
|
+
<i class="bi" :class="taskCardExpanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="card-body" x-show="taskCardExpanded">
|
|
141
|
+
<div class="row g-3">
|
|
142
|
+
<div class="col-12 col-md-6">
|
|
143
|
+
<div class="form-check mt-2">
|
|
144
|
+
<input class="form-check-input" type="checkbox" id="keep_images" x-model="$store.citrascope.config.keep_images">
|
|
145
|
+
<label class="form-check-label" for="keep_images">
|
|
146
|
+
Keep captured images
|
|
147
|
+
</label>
|
|
148
|
+
</div>
|
|
149
|
+
<small class="text-muted ms-4">By default, images are deleted after upload unless this is enabled</small>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="col-12">
|
|
152
|
+
<small class="text-muted">
|
|
153
|
+
Images directory: <span class="text-secondary" x-text="$store.citrascope.config.images_dir_path || 'Loading...'">Loading...</span>
|
|
154
|
+
</small>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- Save Button -->
|
|
163
|
+
<div class="row">
|
|
164
|
+
<div class="col">
|
|
165
|
+
<button type="submit" class="btn btn-primary" :disabled="$store.citrascope.isSavingConfig">
|
|
166
|
+
<span x-text="$store.citrascope.isSavingConfig ? 'Saving...' : 'Save Configuration'"></span>
|
|
167
|
+
<span x-show="$store.citrascope.isSavingConfig" class="spinner-border spinner-border-sm ms-2" role="status"></span>
|
|
168
|
+
</button>
|
|
169
|
+
<small class="text-muted ms-3">
|
|
170
|
+
Config file: <span class="text-secondary" x-text="$store.citrascope.config.config_file_path || 'Loading...'">Loading...</span>
|
|
171
|
+
</small>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</form>
|
|
175
|
+
</div>
|