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.
Files changed (51) hide show
  1. citrascope/api/abstract_api_client.py +14 -0
  2. citrascope/api/citra_api_client.py +41 -0
  3. citrascope/citra_scope_daemon.py +75 -0
  4. citrascope/hardware/abstract_astro_hardware_adapter.py +97 -2
  5. citrascope/hardware/adapter_registry.py +15 -3
  6. citrascope/hardware/devices/__init__.py +17 -0
  7. citrascope/hardware/devices/abstract_hardware_device.py +79 -0
  8. citrascope/hardware/devices/camera/__init__.py +13 -0
  9. citrascope/hardware/devices/camera/abstract_camera.py +114 -0
  10. citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
  11. citrascope/hardware/devices/camera/usb_camera.py +407 -0
  12. citrascope/hardware/devices/camera/ximea_camera.py +756 -0
  13. citrascope/hardware/devices/device_registry.py +273 -0
  14. citrascope/hardware/devices/filter_wheel/__init__.py +7 -0
  15. citrascope/hardware/devices/filter_wheel/abstract_filter_wheel.py +73 -0
  16. citrascope/hardware/devices/focuser/__init__.py +7 -0
  17. citrascope/hardware/devices/focuser/abstract_focuser.py +78 -0
  18. citrascope/hardware/devices/mount/__init__.py +7 -0
  19. citrascope/hardware/devices/mount/abstract_mount.py +115 -0
  20. citrascope/hardware/direct_hardware_adapter.py +805 -0
  21. citrascope/hardware/dummy_adapter.py +202 -0
  22. citrascope/hardware/filter_sync.py +94 -0
  23. citrascope/hardware/indi_adapter.py +6 -2
  24. citrascope/hardware/kstars_dbus_adapter.py +46 -37
  25. citrascope/hardware/nina_adv_http_adapter.py +13 -11
  26. citrascope/settings/citrascope_settings.py +6 -0
  27. citrascope/tasks/runner.py +2 -0
  28. citrascope/tasks/scope/static_telescope_task.py +17 -12
  29. citrascope/tasks/task.py +3 -0
  30. citrascope/time/__init__.py +14 -0
  31. citrascope/time/time_health.py +103 -0
  32. citrascope/time/time_monitor.py +186 -0
  33. citrascope/time/time_sources.py +261 -0
  34. citrascope/web/app.py +260 -60
  35. citrascope/web/static/app.js +121 -731
  36. citrascope/web/static/components.js +136 -0
  37. citrascope/web/static/config.js +259 -420
  38. citrascope/web/static/filters.js +55 -0
  39. citrascope/web/static/formatters.js +129 -0
  40. citrascope/web/static/store-init.js +204 -0
  41. citrascope/web/static/style.css +44 -0
  42. citrascope/web/templates/_config.html +175 -0
  43. citrascope/web/templates/_config_hardware.html +208 -0
  44. citrascope/web/templates/_monitoring.html +242 -0
  45. citrascope/web/templates/dashboard.html +109 -377
  46. {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/METADATA +18 -1
  47. citrascope-0.9.0.dist-info/RECORD +69 -0
  48. citrascope-0.7.0.dist-info/RECORD +0 -41
  49. {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/WHEEL +0 -0
  50. {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/entry_points.txt +0 -0
  51. {citrascope-0.7.0.dist-info → citrascope-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,208 @@
1
+ <!-- Hardware Configuration Card -->
2
+ <div class="col-12" x-data="{ hardwareCardExpanded: $persist(true).as('hardwareCard') }">
3
+ <div class="card bg-dark text-light border-secondary">
4
+ <div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" @click="hardwareCardExpanded = !hardwareCardExpanded">
5
+ <div class="d-flex align-items-center gap-2">
6
+ <i class="bi bi-hdd-network"></i>
7
+ <h5 class="mb-0">Hardware Configuration</h5>
8
+ <small class="text-muted" x-show="!hardwareCardExpanded" x-text="($store.citrascope.hardwareAdapters.find(a => a.value === $store.citrascope.config.hardware_adapter)?.label || $store.citrascope.config.hardware_adapter || 'Not configured')"></small>
9
+ </div>
10
+ <i class="bi" :class="hardwareCardExpanded ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
11
+ </div>
12
+ <div class="card-body" x-show="hardwareCardExpanded">
13
+ <div class="row g-3 mb-3">
14
+ <div class="col-12">
15
+ <label for="hardwareAdapterSelect" class="form-label">Hardware Adapter <span class="text-danger">*</span></label>
16
+ <select id="hardwareAdapterSelect" class="form-select" required x-model="$store.citrascope.config.hardware_adapter"
17
+ @change="$store.citrascope.handleAdapterChange($event.target.value)">
18
+ <option value="">-- Select Hardware Adapter --</option>
19
+ <template x-for="adapter in $store.citrascope.hardwareAdapters" :key="adapter.value">
20
+ <option :value="adapter.value" x-text="adapter.label"></option>
21
+ </template>
22
+ </select>
23
+ </div>
24
+ </div>
25
+
26
+ <!-- Dynamic Adapter Settings Container -->
27
+ <div id="adapter-settings-container">
28
+ <template x-if="$store.citrascope.adapterFields.length > 0">
29
+ <div>
30
+ <h5 class="mb-3">Adapter Settings</h5>
31
+ <!-- Group fields by group property -->
32
+ <template x-for="[groupName, fields] in $store.citrascope.groupedAdapterFields" :key="groupName">
33
+ <div class="card bg-dark border-secondary mb-3">
34
+ <div class="card-header">
35
+ <h6 class="mb-0" x-text="groupName"></h6>
36
+ </div>
37
+ <div class="card-body">
38
+ <div class="row g-3">
39
+ <template x-for="field in fields" :key="field.name">
40
+ <div x-data="adapterField(field)"
41
+ class="col-12 col-md-4" x-show="field && field.name">
42
+ <label :for="inputId" class="form-label">
43
+ <span x-text="field.friendly_name || field.name"></span>
44
+ <span class="text-danger" x-show="field.required">*</span>
45
+ </label>
46
+
47
+ <!-- Boolean checkbox -->
48
+ <template x-if="isBoolean">
49
+ <div class="form-check mt-2">
50
+ <input class="form-check-input adapter-setting" type="checkbox"
51
+ :id="inputId" :data-field="field.name" :data-type="field.type"
52
+ x-model="field.value">
53
+ <label class="form-check-label" :for="inputId" x-text="field.description"></label>
54
+ </div>
55
+ </template>
56
+
57
+ <!-- Select dropdown -->
58
+ <template x-if="isSelect">
59
+ <select :id="inputId" class="form-select adapter-setting"
60
+ :data-field="field.name" :data-type="field.type"
61
+ :required="field.required" @change="handleChange">
62
+ <option value="" x-text="'-- Select ' + (field.friendly_name || field.name) + ' --'"></option>
63
+ <template x-for="(opt, idx) in (field.options || [])" :key="(typeof opt === 'object' ? opt.value : opt) + '_' + idx">
64
+ <option :value="typeof opt === 'object' ? opt.value : opt"
65
+ :selected="(typeof opt === 'object' ? opt.value : opt) === field.value"
66
+ x-text="typeof opt === 'object' ? opt.label : opt"></option>
67
+ </template>
68
+ </select>
69
+ </template>
70
+
71
+ <!-- Number input -->
72
+ <template x-if="isNumber">
73
+ <input type="number" :id="inputId"
74
+ class="form-control adapter-setting"
75
+ :data-field="field.name" :data-type="field.type"
76
+ :placeholder="field.placeholder || ''"
77
+ :min="field.min" :max="field.max"
78
+ :step="field.type === 'float' ? 'any' : '1'"
79
+ :required="field.required"
80
+ x-model="field.value" @input="handleChange">
81
+ </template>
82
+
83
+ <!-- Text input -->
84
+ <template x-if="isText">
85
+ <input type="text" :id="inputId"
86
+ class="form-control adapter-setting"
87
+ :data-field="field.name" :data-type="field.type"
88
+ :placeholder="field.placeholder || ''"
89
+ :pattern="field.pattern || null"
90
+ :required="field.required"
91
+ x-model="field.value">
92
+ </template>
93
+
94
+ <small class="text-muted" x-show="!isBoolean && field.description"
95
+ x-text="field.description"></small>
96
+ </div>
97
+ </template>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </template>
102
+ </div>
103
+ </template>
104
+ </div>
105
+
106
+ <!-- Missing Dependencies Alert (Config Page) -->
107
+ <div class="alert alert-warning mt-3" role="alert"
108
+ x-show="$store.citrascope.status?.missing_dependencies?.length > 0">
109
+ <template x-if="$store.citrascope.status?.missing_dependencies?.length > 0">
110
+ <div>
111
+ <strong><i class="bi bi-exclamation-triangle-fill me-2"></i>Missing Dependencies:</strong>
112
+ <ul class="mb-0 mt-2">
113
+ <template x-for="dep in $store.citrascope.status.missing_dependencies" :key="dep.device_name">
114
+ <li>
115
+ <strong x-text="dep.device_name"></strong>: Missing <span x-text="dep.missing_packages"></span><br>
116
+ <code class="small" x-text="dep.install_cmd"></code>
117
+ </li>
118
+ </template>
119
+ </ul>
120
+ </div>
121
+ </template>
122
+ </div>
123
+
124
+ <!-- Filter Configuration Section (shown when adapter supports filters) -->
125
+ <div id="filterConfigSection" x-show="$store.citrascope.filterConfigVisible" style="margin-top: 1.5rem;">
126
+ <hr class="border-secondary">
127
+ <h5 class="mb-3">Filter Configuration</h5>
128
+ <p class="text-muted mb-3" x-show="$store.citrascope.filterAdapterChangeMessageVisible">
129
+ Save configuration to edit filter settings for this adapter
130
+ </p>
131
+ <div class="row">
132
+ <!-- Filter List Column -->
133
+ <div class="col-12 col-md-6">
134
+ <h6 class="mb-2">Filters</h6>
135
+ <div id="filterTableContainer">
136
+ <table class="table table-dark table-sm">
137
+ <thead>
138
+ <tr>
139
+ <th>Enabled</th>
140
+ <th>Name</th>
141
+ <th>Focus Position</th>
142
+ </tr>
143
+ </thead>
144
+ <tbody>
145
+ <template x-for="[filterId, filter] in Object.entries($store.citrascope.filters || {})" :key="filterId">
146
+ <tr x-data="filterRow(filter, filterId)">
147
+ <td>
148
+ <input type="checkbox" class="form-check-input filter-enabled-checkbox"
149
+ :data-filter-id="filterId" x-model="filter.enabled">
150
+ </td>
151
+ <td>
152
+ <span class="badge" :style="badgeStyle" x-text="filter.name"></span>
153
+ </td>
154
+ <td>
155
+ <input type="number" class="form-control form-control-sm filter-focus-input"
156
+ :data-filter-id="filterId" x-model.number="filter.focus_position" min="0" step="1">
157
+ </td>
158
+ </tr>
159
+ </template>
160
+ </tbody>
161
+ </table>
162
+ <div class="text-muted small" x-show="Object.keys($store.citrascope.filters || {}).length === 0">
163
+ No filters configured. Connect to hardware to discover filters.
164
+ </div>
165
+ </div>
166
+ </div>
167
+
168
+ <!-- Autofocus Column -->
169
+ <div class="col-12 col-md-6">
170
+ <h6 class="mb-2">Autofocus</h6>
171
+ <div class="mb-3">
172
+ <button type="button" class="btn btn-sm w-100"
173
+ :class="$store.citrascope.status?.autofocus_requested ? 'btn-outline-warning' : 'btn-outline-primary'"
174
+ @click="window.triggerAutofocus()">
175
+ <span x-text="$store.citrascope.status?.autofocus_requested ? 'Cancel Autofocus' : 'Run Autofocus'">Run Autofocus</span>
176
+ <span x-show="$store.citrascope.isAutofocusing" class="spinner-border spinner-border-sm ms-2" role="status"></span>
177
+ </button>
178
+ </div>
179
+ <div class="mb-3">
180
+ <label class="form-label small text-muted">Last Autofocus</label>
181
+ <div class="text-light" x-text="$store.citrascope.formatLastAutofocus($store.citrascope.status)">Never</div>
182
+ </div>
183
+ <div class="mb-3">
184
+ <div class="form-check">
185
+ <input class="form-check-input" type="checkbox" id="scheduled_autofocus_enabled" x-model="$store.citrascope.config.scheduled_autofocus_enabled">
186
+ <label class="form-check-label" for="scheduled_autofocus_enabled">
187
+ Enable Scheduled Autofocus
188
+ </label>
189
+ </div>
190
+ </div>
191
+ <div class="mb-3">
192
+ <label for="autofocus_interval_minutes" class="form-label small">Autofocus Interval</label>
193
+ <select id="autofocus_interval_minutes" class="form-select form-select-sm" x-model.number="$store.citrascope.config.autofocus_interval_minutes">
194
+ <option value="30">30 minutes</option>
195
+ <option value="60">60 minutes</option>
196
+ <option value="120">120 minutes (2 hours)</option>
197
+ <option value="180">180 minutes (3 hours)</option>
198
+ </select>
199
+ </div>
200
+ <div class="small text-muted" x-show="$store.citrascope.status?.next_autofocus_minutes != null">
201
+ Next autofocus in: <span x-text="$store.citrascope.status?.next_autofocus_minutes === 0 ? 'now (overdue)' : $store.citrascope.formatMinutes($store.citrascope.status?.next_autofocus_minutes || 0)">--</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
@@ -0,0 +1,242 @@
1
+ <div id="monitoringSection" x-show="$store.citrascope.currentSection === 'monitoring'"
2
+ x-effect="$store.citrascope.status && $nextTick(() => { document.querySelectorAll('[data-bs-toggle=tooltip]').forEach(el => { try { bootstrap.Tooltip.getOrCreateInstance(el); } catch (_) {} }); })">
3
+ <div class="container">
4
+ <div class="row g-3 mb-3">
5
+ <div class="col-12 col-md-6">
6
+ <div class="card bg-dark text-light border-secondary h-100">
7
+ <div class="card-header">
8
+ System Status
9
+ </div>
10
+ <div class="card-body">
11
+ <div class="row mb-2">
12
+ <div class="col-6 fw-semibold">Daemon</div>
13
+ <div class="col-6">
14
+ <span class="badge rounded-pill" data-bs-toggle="tooltip" data-bs-placement="bottom"
15
+ :class="$store.citrascope.wsConnected ? 'bg-success' : ($store.citrascope.wsReconnecting ? 'bg-warning text-dark' : 'bg-secondary')"
16
+ :title="$store.citrascope.wsConnected ? 'Dashboard connected - receiving live updates' : ($store.citrascope.wsReconnecting ? 'Dashboard reconnecting' : 'Dashboard disconnected')"
17
+ x-text="$store.citrascope.wsConnected ? 'Connected' : ($store.citrascope.wsReconnecting ? 'Reconnecting' : 'Connecting...')"></span>
18
+ </div>
19
+ </div>
20
+ <div class="row mb-2">
21
+ <div class="col-6 fw-semibold">Telescope</div>
22
+ <div class="col-6">
23
+ <span class="badge rounded-pill" :class="$store.citrascope.status?.telescope_connected ? 'bg-success' : 'bg-danger'"
24
+ x-text="$store.citrascope.status?.telescope_connected ? 'Connected' : 'Disconnected'"></span>
25
+ </div>
26
+ </div>
27
+ <div class="row mb-2">
28
+ <div class="col-6 fw-semibold">Camera</div>
29
+ <div class="col-6 d-flex align-items-center gap-2">
30
+ <span class="badge rounded-pill" :class="$store.citrascope.status?.camera_connected ? 'bg-success' : 'bg-danger'"
31
+ x-text="$store.citrascope.status?.camera_connected ? 'Connected' : 'Disconnected'"></span>
32
+ <button class="btn btn-sm btn-outline-light"
33
+ x-show="$store.citrascope.status?.camera_connected && $store.citrascope.status?.supports_direct_camera_control"
34
+ @click="$store.citrascope.showCameraControl()">
35
+ <i class="bi bi-camera"></i> Control
36
+ </button>
37
+ </div>
38
+ </div>
39
+ <div class="row mb-2">
40
+ <div class="col-6 fw-semibold">
41
+ <span data-bs-toggle="tooltip" data-bs-theme="dark" data-bs-placement="top" title="Controls whether the Citra.space server will automatically assign new observation tasks to this telescope">
42
+ Automated Scheduling
43
+ </span>
44
+ </div>
45
+ <div class="col-6 d-flex align-items-center gap-2">
46
+ <span><span class="badge rounded-pill" :class="$store.citrascope.status?.automated_scheduling ? 'bg-success' : 'bg-secondary'"
47
+ x-text="$store.citrascope.status?.automated_scheduling ? 'Enabled' : 'Disabled'"></span></span>
48
+ <div class="form-check form-switch">
49
+ <input class="form-check-input" type="checkbox" title="Toggle automated scheduling"
50
+ :checked="$store.citrascope.status?.automated_scheduling"
51
+ @change="$store.citrascope.toggleAutomatedScheduling($event.target.checked)">
52
+ <label class="form-check-label visually-hidden">Automated Scheduling</label>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div class="row mb-2">
57
+ <div class="col-6 fw-semibold">
58
+ <span data-bs-toggle="tooltip" data-bs-theme="dark" data-bs-placement="top" title="Controls whether this local daemon will execute tasks from its queue. Pause to safely stop observations without canceling scheduled tasks">
59
+ Task Processing
60
+ </span>
61
+ </div>
62
+ <div class="col-6 d-flex align-items-center gap-2">
63
+ <span><span class="badge rounded-pill" :class="$store.citrascope.status?.processing_active ? 'bg-success' : 'bg-secondary'"
64
+ x-text="$store.citrascope.status?.processing_active ? 'Enabled' : 'Disabled'"></span></span>
65
+ <div class="form-check form-switch">
66
+ <input class="form-check-input" type="checkbox" title="Toggle task processing"
67
+ :checked="$store.citrascope.status?.processing_active !== false"
68
+ @change="$store.citrascope.toggleProcessing($event.target.checked)">
69
+ <label class="form-check-label visually-hidden">Task Processing</label>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <div class="row mb-2">
74
+ <div class="col-6 fw-semibold">
75
+ <span data-bs-toggle="tooltip" data-bs-theme="dark" data-bs-placement="top" title="System clock synchronization status. Accurate time is critical for astronomical observations">
76
+ Time Sync
77
+ </span>
78
+ </div>
79
+ <div class="col-6 d-flex align-items-center gap-2">
80
+ <span>
81
+ <span class="badge rounded-pill" data-bs-toggle="tooltip"
82
+ :class="$store.citrascope.status?.time_health?.status === 'ok' ? 'bg-success' : ($store.citrascope.status?.time_health?.status === 'critical' ? 'bg-danger' : 'bg-secondary')"
83
+ :title="$store.citrascope.status?.time_health?.status === 'ok' ? 'Time sync within threshold' : ($store.citrascope.status?.time_health?.status === 'critical' ? 'Time drift exceeded' : 'Unknown')"
84
+ x-text="$store.citrascope.formatTimeOffset($store.citrascope.status?.time_health)"></span>
85
+ </span>
86
+ </div>
87
+ </div>
88
+ <!-- Missing Dependencies Alert -->
89
+ <div class="alert alert-warning mt-3 mb-0" role="alert"
90
+ x-show="$store.citrascope.status?.missing_dependencies?.length > 0">
91
+ <template x-if="$store.citrascope.status?.missing_dependencies?.length > 0">
92
+ <div>
93
+ <strong><i class="bi bi-exclamation-triangle-fill me-2"></i>Missing Dependencies:</strong>
94
+ <ul class="mb-0 mt-2">
95
+ <template x-for="dep in $store.citrascope.status.missing_dependencies" :key="dep.device_name">
96
+ <li>
97
+ <strong x-text="dep.device_name"></strong>: Missing <span x-text="dep.missing_packages"></span><br>
98
+ <code class="small" x-text="dep.install_cmd"></code>
99
+ </li>
100
+ </template>
101
+ </ul>
102
+ </div>
103
+ </template>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ <div class="col-12 col-md-6">
109
+ <div class="card bg-dark text-light border-secondary h-100">
110
+ <div class="card-header">
111
+ Telescope
112
+ </div>
113
+ <div class="card-body">
114
+ <div class="row mb-2">
115
+ <div class="col-6 fw-semibold">Adapter</div>
116
+ <div class="col-6" x-text="$store.citrascope.status?.hardware_adapter || '-'"></div>
117
+ </div>
118
+ <div class="row mb-2">
119
+ <div class="col-6 fw-semibold">RA / DEC</div>
120
+ <div class="col-6" x-text="($store.citrascope.status?.telescope_ra != null && $store.citrascope.status?.telescope_dec != null) ? ($store.citrascope.status.telescope_ra.toFixed(3) + '° / ' + $store.citrascope.status.telescope_dec.toFixed(3) + '°') : '-'"></div>
121
+ </div>
122
+ <div class="row mb-2">
123
+ <div class="col-6 fw-semibold">Enabled Filters</div>
124
+ <div class="col-6 d-flex flex-wrap gap-1">
125
+ <template x-if="$store.citrascope.enabledFilters.length === 0">
126
+ <span>-</span>
127
+ </template>
128
+ <template x-for="f in $store.citrascope.enabledFilters" :key="f.name">
129
+ <span class="badge me-1" :style="'background-color: ' + f.color + '; color: white;'" x-text="f.name"></span>
130
+ </template>
131
+ </div>
132
+ </div>
133
+ <div class="row">
134
+ <div class="col-6 fw-semibold">Ground Station</div>
135
+ <div class="col-6">
136
+ <template x-if="$store.citrascope.status?.ground_station_name && $store.citrascope.status?.ground_station_url">
137
+ <a :href="$store.citrascope.status.ground_station_url" target="_blank" class="ground-station-link" x-text="$store.citrascope.status.ground_station_name + ' ↗'"></a>
138
+ </template>
139
+ <template x-if="$store.citrascope.status?.ground_station_name && !$store.citrascope.status?.ground_station_url">
140
+ <span x-text="$store.citrascope.status.ground_station_name"></span>
141
+ </template>
142
+ <template x-if="!$store.citrascope.status?.ground_station_name">
143
+ <span>-</span>
144
+ </template>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ </div>
152
+ </div>
153
+ <div class="container">
154
+ <div class="row g-3 mb-3">
155
+ <div class="col-12">
156
+ <div class="card bg-dark text-light border-secondary">
157
+ <div class="card-header">
158
+ Current Task
159
+ </div>
160
+ <div class="card-body">
161
+ <div>
162
+ <template x-if="$store.citrascope.currentTaskId">
163
+ <div>
164
+ <template x-for="task in $store.citrascope.tasks.filter(t => t.id === $store.citrascope.currentTaskId)" :key="task.id">
165
+ <div>
166
+ <div class="d-flex align-items-center gap-2 mb-2">
167
+ <div class="spinner-border spinner-border-sm text-success" role="status">
168
+ <span class="visually-hidden">Loading...</span>
169
+ </div>
170
+ <div class="fw-bold" style="font-size: 1.3em;" x-text="task.target"></div>
171
+ </div>
172
+ <div class="text-secondary small">
173
+ <span x-text="'Task ID: ' + task.id"></span>
174
+ </div>
175
+ </div>
176
+ </template>
177
+ <template x-if="!$store.citrascope.tasks.find(t => t.id === $store.citrascope.currentTaskId)">
178
+ <div>
179
+ <div class="d-flex align-items-center gap-2 mb-2">
180
+ <div class="spinner-border spinner-border-sm text-success" role="status">
181
+ <span class="visually-hidden">Loading...</span>
182
+ </div>
183
+ <div class="fw-bold" style="font-size: 1.3em;" x-text="$store.citrascope.currentTaskId"></div>
184
+ </div>
185
+ <div class="text-secondary small">
186
+ <span x-text="'Task ID: ' + $store.citrascope.currentTaskId"></span>
187
+ </div>
188
+ </div>
189
+ </template>
190
+ </div>
191
+ </template>
192
+ <template x-if="!$store.citrascope.currentTaskId && !$store.citrascope.countdown">
193
+ <p class="no-task-message text-muted-dark">No active task</p>
194
+ </template>
195
+ <template x-if="!$store.citrascope.currentTaskId && $store.citrascope.countdown">
196
+ <p class="no-task-message">No active task - next task in <span x-text="$store.citrascope.countdown"></span></p>
197
+ </template>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ <div class="row g-3 mb-3">
204
+ <div class="col-12">
205
+ <div class="card bg-dark text-light border-secondary">
206
+ <div class="card-header d-flex align-items-center justify-content-between">
207
+ <span>Task Queue</span>
208
+ <span class="small text-secondary"><span x-text="$store.citrascope.status?.tasks_pending ?? $store.citrascope.tasks?.length ?? 0">0</span> pending</span>
209
+ </div>
210
+ <div class="card-body p-0">
211
+ <div class="table-responsive">
212
+ <p class="p-3 text-muted-dark" x-show="$store.citrascope.tasks.length === 0">No pending tasks</p>
213
+ <table class="table table-dark table-hover mb-0" x-show="$store.citrascope.tasks.length > 0">
214
+ <thead>
215
+ <tr>
216
+ <th>Target</th>
217
+ <th>Start Time</th>
218
+ <th>End Time</th>
219
+ <th>Status</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody>
223
+ <template x-for="task in $store.citrascope.tasks" :key="task.id">
224
+ <tr x-data="taskRow(task)"
225
+ class="task-row" :class="rowClass">
226
+ <td class="fw-semibold" x-text="task.target"></td>
227
+ <td class="text-secondary small" x-text="$store.citrascope.formatLocalTime(task.start_time)"></td>
228
+ <td class="text-secondary small" x-text="task.stop_time ? $store.citrascope.formatLocalTime(task.stop_time) : '-'"></td>
229
+ <td>
230
+ <span class="badge rounded-pill" :class="statusBadgeClass" x-text="statusText"></span>
231
+ </td>
232
+ </tr>
233
+ </template>
234
+ </tbody>
235
+ </table>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>