embedded-lab-control 0.2.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.
- embedded_lab_control-0.2.0.dist-info/METADATA +855 -0
- embedded_lab_control-0.2.0.dist-info/RECORD +64 -0
- embedded_lab_control-0.2.0.dist-info/WHEEL +5 -0
- embedded_lab_control-0.2.0.dist-info/entry_points.txt +2 -0
- embedded_lab_control-0.2.0.dist-info/licenses/LICENSE +21 -0
- embedded_lab_control-0.2.0.dist-info/top_level.txt +1 -0
- labctl/__init__.py +8 -0
- labctl/actuators/__init__.py +56 -0
- labctl/actuators/base.py +183 -0
- labctl/actuators/lcus1.py +232 -0
- labctl/actuators/mock.py +81 -0
- labctl/actuators/runtime.py +1178 -0
- labctl/cli.py +7485 -0
- labctl/core/__init__.py +36 -0
- labctl/core/admin_files.py +388 -0
- labctl/core/audit.py +269 -0
- labctl/core/claim_keepalive.py +342 -0
- labctl/core/config.py +1068 -0
- labctl/core/database.py +880 -0
- labctl/core/images.py +503 -0
- labctl/core/manager.py +2815 -0
- labctl/core/models.py +815 -0
- labctl/core/safe_open.py +109 -0
- labctl/extras.py +128 -0
- labctl/health/__init__.py +43 -0
- labctl/health/alerts.py +275 -0
- labctl/health/checks.py +533 -0
- labctl/health/daemon.py +397 -0
- labctl/mcp_server.py +5254 -0
- labctl/power/__init__.py +13 -0
- labctl/power/base.py +145 -0
- labctl/power/kasa.py +446 -0
- labctl/power/shelly.py +89 -0
- labctl/power/tasmota.py +95 -0
- labctl/sdwire/__init__.py +10 -0
- labctl/sdwire/controller.py +1116 -0
- labctl/sdwire/power_guard.py +190 -0
- labctl/serial/__init__.py +18 -0
- labctl/serial/boot_test.py +342 -0
- labctl/serial/capture.py +430 -0
- labctl/serial/keys.py +89 -0
- labctl/serial/proxy.py +802 -0
- labctl/serial/ser2net.py +338 -0
- labctl/serial/udev.py +273 -0
- labctl/services.py +210 -0
- labctl/web/__init__.py +9 -0
- labctl/web/activity_broadcaster.py +204 -0
- labctl/web/api.py +1003 -0
- labctl/web/app.py +202 -0
- labctl/web/auth.py +97 -0
- labctl/web/claims.py +232 -0
- labctl/web/static/css/style.css +477 -0
- labctl/web/static/js/app.js +76 -0
- labctl/web/templates/activity.html +202 -0
- labctl/web/templates/base.html +49 -0
- labctl/web/templates/claim_refused.html +38 -0
- labctl/web/templates/console.html +237 -0
- labctl/web/templates/dashboard.html +100 -0
- labctl/web/templates/login.html +25 -0
- labctl/web/templates/sbc_detail.html +306 -0
- labctl/web/templates/settings.html +78 -0
- labctl/web/templates/status_history.html +65 -0
- labctl/web/views.py +496 -0
- labctl/web/websocket.py +208 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ sbc.name }} - Lab Controller{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="page-header">
|
|
7
|
+
<a href="{{ url_for('views.index') }}" class="back-link">← Back to Dashboard</a>
|
|
8
|
+
<h1>{{ sbc.name }}</h1>
|
|
9
|
+
<span class="status-badge status-{{ sbc.status.value }}">{{ sbc.status.value }}</span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
{% if active_claim %}
|
|
13
|
+
<div class="claim-section">
|
|
14
|
+
<h3>Active Claim</h3>
|
|
15
|
+
<div class="info-row">
|
|
16
|
+
<span class="label">Held by:</span>
|
|
17
|
+
<span class="value">{{ active_claim.agent_name }} ({{ active_claim.session_kind }})</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="info-row">
|
|
20
|
+
<span class="label">Reason:</span>
|
|
21
|
+
<span class="value">{{ active_claim.reason }}</span>
|
|
22
|
+
</div>
|
|
23
|
+
{% if active_claim.time_remaining %}
|
|
24
|
+
{% set rem = active_claim.time_remaining.total_seconds()|int %}
|
|
25
|
+
<div class="info-row">
|
|
26
|
+
<span class="label">Time remaining:</span>
|
|
27
|
+
<span class="value">
|
|
28
|
+
{% if rem >= 3600 %}{{ rem // 3600 }}h {{ (rem % 3600) // 60 }}m
|
|
29
|
+
{% elif rem >= 60 %}{{ rem // 60 }}m {{ rem % 60 }}s
|
|
30
|
+
{% else %}{{ rem }}s{% endif %}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
{% endif %}
|
|
34
|
+
{% if active_claim.renewal_count %}
|
|
35
|
+
<div class="info-row">
|
|
36
|
+
<span class="label">Renewed:</span>
|
|
37
|
+
<span class="value">{{ active_claim.renewal_count }}x</span>
|
|
38
|
+
</div>
|
|
39
|
+
{% endif %}
|
|
40
|
+
{% for req in active_claim.pending_requests %}
|
|
41
|
+
<div class="claim-release-request">
|
|
42
|
+
Release requested by <strong>{{ req.requested_by }}</strong>: {{ req.reason }}
|
|
43
|
+
</div>
|
|
44
|
+
{% endfor %}
|
|
45
|
+
<form action="{{ url_for('views.sbc_force_release', name=sbc.name) }}" method="post" class="mt-2" style="display:inline;">
|
|
46
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
47
|
+
<input type="hidden" name="reason" value="force-released via web dashboard">
|
|
48
|
+
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Force-release this claim?')">Force Release</button>
|
|
49
|
+
</form>
|
|
50
|
+
</div>
|
|
51
|
+
{% endif %}
|
|
52
|
+
|
|
53
|
+
<div class="detail-grid">
|
|
54
|
+
<!-- Basic Info with Edit Form -->
|
|
55
|
+
<div class="detail-card">
|
|
56
|
+
<h3>Basic Information
|
|
57
|
+
<button class="btn btn-sm btn-secondary" onclick="toggleEdit('basic')" style="float: right;">Edit</button>
|
|
58
|
+
</h3>
|
|
59
|
+
|
|
60
|
+
<!-- Display view -->
|
|
61
|
+
<div id="basic-display">
|
|
62
|
+
<table class="detail-table">
|
|
63
|
+
<tr><th>Name</th><td>{{ sbc.name }}</td></tr>
|
|
64
|
+
<tr><th>Project</th><td>{{ sbc.project or '-' }}</td></tr>
|
|
65
|
+
<tr><th>Description</th><td>{{ sbc.description or '-' }}</td></tr>
|
|
66
|
+
<tr><th>SSH User</th><td>{{ sbc.ssh_user }}</td></tr>
|
|
67
|
+
<tr><th>Status</th><td>{{ sbc.status.value }}</td></tr>
|
|
68
|
+
</table>
|
|
69
|
+
<p class="mt-2">
|
|
70
|
+
<a href="{{ url_for('views.sbc_history', name=sbc.name) }}" class="btn btn-secondary btn-sm">View Status History</a>
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- Edit form (hidden by default) -->
|
|
75
|
+
<form id="basic-edit" action="{{ url_for('views.sbc_edit', name=sbc.name) }}" method="post" style="display: none;">
|
|
76
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
77
|
+
<div class="form-group">
|
|
78
|
+
<label>Name</label>
|
|
79
|
+
<input type="text" name="name" value="{{ sbc.name }}" class="form-control">
|
|
80
|
+
</div>
|
|
81
|
+
<div class="form-group">
|
|
82
|
+
<label>Project</label>
|
|
83
|
+
<input type="text" name="project" value="{{ sbc.project or '' }}" class="form-control">
|
|
84
|
+
</div>
|
|
85
|
+
<div class="form-group">
|
|
86
|
+
<label>Description</label>
|
|
87
|
+
<input type="text" name="description" value="{{ sbc.description or '' }}" class="form-control">
|
|
88
|
+
</div>
|
|
89
|
+
<div class="form-group">
|
|
90
|
+
<label>SSH User</label>
|
|
91
|
+
<input type="text" name="ssh_user" value="{{ sbc.ssh_user }}" class="form-control">
|
|
92
|
+
</div>
|
|
93
|
+
<div class="form-group">
|
|
94
|
+
<label>Status</label>
|
|
95
|
+
<select name="status" class="form-control">
|
|
96
|
+
{% for s in Status %}
|
|
97
|
+
<option value="{{ s.value }}" {% if s == sbc.status %}selected{% endif %}>{{ s.value }}</option>
|
|
98
|
+
{% endfor %}
|
|
99
|
+
</select>
|
|
100
|
+
</div>
|
|
101
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
|
102
|
+
<button type="button" class="btn btn-secondary" onclick="toggleEdit('basic')">Cancel</button>
|
|
103
|
+
</form>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Power Control -->
|
|
107
|
+
<div class="detail-card">
|
|
108
|
+
<h3>Power Control</h3>
|
|
109
|
+
{% if sbc.power_plug %}
|
|
110
|
+
<table class="detail-table">
|
|
111
|
+
<tr><th>Plug Type</th><td>{{ sbc.power_plug.plug_type.value }}</td></tr>
|
|
112
|
+
<tr><th>Address</th><td>{{ sbc.power_plug.address }}</td></tr>
|
|
113
|
+
<tr><th>Index</th><td>{{ sbc.power_plug.plug_index }}</td></tr>
|
|
114
|
+
<tr>
|
|
115
|
+
<th>State</th>
|
|
116
|
+
<td>
|
|
117
|
+
<span class="power-indicator power-{{ power_state.value if power_state else 'unknown' }}"></span>
|
|
118
|
+
{{ power_state.value.upper() if power_state else 'UNKNOWN' }}
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
</table>
|
|
122
|
+
<div class="power-actions-large">
|
|
123
|
+
<form action="{{ url_for('views.sbc_power_action', name=sbc.name, action='on') }}" method="post" class="inline-form">
|
|
124
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
125
|
+
<button type="submit" class="btn btn-success">Power ON</button>
|
|
126
|
+
</form>
|
|
127
|
+
<form action="{{ url_for('views.sbc_power_action', name=sbc.name, action='off') }}" method="post" class="inline-form">
|
|
128
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
129
|
+
<button type="submit" class="btn btn-danger">Power OFF</button>
|
|
130
|
+
</form>
|
|
131
|
+
<form action="{{ url_for('views.sbc_power_action', name=sbc.name, action='cycle') }}" method="post" class="inline-form">
|
|
132
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
133
|
+
<button type="submit" class="btn btn-warning">Power Cycle</button>
|
|
134
|
+
</form>
|
|
135
|
+
</div>
|
|
136
|
+
<form action="{{ url_for('views.sbc_plug_remove', name=sbc.name) }}" method="post" class="mt-2">
|
|
137
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
138
|
+
<button type="submit" class="btn btn-sm btn-secondary" onclick="return confirm('Remove power plug?')">Remove Plug</button>
|
|
139
|
+
</form>
|
|
140
|
+
{% else %}
|
|
141
|
+
<p class="text-muted">No power plug assigned.</p>
|
|
142
|
+
<button class="btn btn-sm btn-primary" onclick="toggleEdit('plug')">Assign Power Plug</button>
|
|
143
|
+
<form id="plug-edit" action="{{ url_for('views.sbc_plug_assign', name=sbc.name) }}" method="post" style="display: none; margin-top: 1rem;">
|
|
144
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
145
|
+
<div class="form-group">
|
|
146
|
+
<label>Plug Type</label>
|
|
147
|
+
<select name="plug_type" class="form-control" required>
|
|
148
|
+
{% for pt in PlugType %}
|
|
149
|
+
<option value="{{ pt.value }}">{{ pt.value }}</option>
|
|
150
|
+
{% endfor %}
|
|
151
|
+
</select>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="form-group">
|
|
154
|
+
<label>Address (IP)</label>
|
|
155
|
+
<input type="text" name="address" placeholder="192.168.1.100" class="form-control" required>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="form-group">
|
|
158
|
+
<label>Outlet Index</label>
|
|
159
|
+
<input type="number" name="plug_index" value="1" min="1" class="form-control">
|
|
160
|
+
</div>
|
|
161
|
+
<button type="submit" class="btn btn-primary">Assign</button>
|
|
162
|
+
<button type="button" class="btn btn-secondary" onclick="toggleEdit('plug')">Cancel</button>
|
|
163
|
+
</form>
|
|
164
|
+
{% endif %}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Serial Ports -->
|
|
168
|
+
<div class="detail-card">
|
|
169
|
+
<h3>Serial Ports
|
|
170
|
+
<button class="btn btn-sm btn-primary" onclick="toggleEdit('port')" style="float: right;">Add Port</button>
|
|
171
|
+
</h3>
|
|
172
|
+
{% if sbc.serial_ports %}
|
|
173
|
+
<table class="detail-table">
|
|
174
|
+
<tr><th>Type</th><th>Alias</th><th>Device</th><th>TCP Port</th><th>Baud</th><th></th></tr>
|
|
175
|
+
{% for port in sbc.serial_ports %}
|
|
176
|
+
<tr>
|
|
177
|
+
<td>{{ port.port_type.value }}</td>
|
|
178
|
+
<td>{{ port.alias or '-' }}</td>
|
|
179
|
+
<td><code>{{ port.device_path }}</code></td>
|
|
180
|
+
<td>{{ port.tcp_port or '-' }}</td>
|
|
181
|
+
<td>{{ port.baud_rate }}</td>
|
|
182
|
+
<td>
|
|
183
|
+
<form action="{{ url_for('views.sbc_port_remove', name=sbc.name, port_type=port.port_type.value) }}" method="post" class="inline-form">
|
|
184
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
185
|
+
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Remove this port?')">X</button>
|
|
186
|
+
</form>
|
|
187
|
+
</td>
|
|
188
|
+
</tr>
|
|
189
|
+
{% endfor %}
|
|
190
|
+
</table>
|
|
191
|
+
{% if sbc.console_port and sbc.console_port.tcp_port %}
|
|
192
|
+
<p class="mt-2">
|
|
193
|
+
Connect: <code>nc localhost {{ sbc.console_port.tcp_port }}</code>
|
|
194
|
+
<a href="{{ url_for('views.sbc_console', name=sbc.name) }}" class="btn btn-primary btn-sm" style="margin-left: 1rem;">Open Web Console</a>
|
|
195
|
+
</p>
|
|
196
|
+
{% endif %}
|
|
197
|
+
{% else %}
|
|
198
|
+
<p class="text-muted">No serial ports assigned.</p>
|
|
199
|
+
{% endif %}
|
|
200
|
+
|
|
201
|
+
<!-- Add port form (hidden by default) -->
|
|
202
|
+
<form id="port-edit" action="{{ url_for('views.sbc_port_assign', name=sbc.name) }}" method="post" style="display: none; margin-top: 1rem;">
|
|
203
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
204
|
+
<div class="form-group">
|
|
205
|
+
<label>Port Type</label>
|
|
206
|
+
<select name="port_type" class="form-control" required>
|
|
207
|
+
{% for pt in PortType %}
|
|
208
|
+
<option value="{{ pt.value }}">{{ pt.value }}</option>
|
|
209
|
+
{% endfor %}
|
|
210
|
+
</select>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="form-group">
|
|
213
|
+
<label>Alias (optional)</label>
|
|
214
|
+
<input type="text" name="alias" placeholder="e.g. jetson-console" class="form-control">
|
|
215
|
+
</div>
|
|
216
|
+
<div class="form-group">
|
|
217
|
+
<label>Device Path</label>
|
|
218
|
+
<input type="text" name="device" placeholder="/dev/lab/port-1" class="form-control" required>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="form-group">
|
|
221
|
+
<label>TCP Port (optional)</label>
|
|
222
|
+
<input type="number" name="tcp_port" placeholder="Auto-assign" class="form-control">
|
|
223
|
+
</div>
|
|
224
|
+
<div class="form-group">
|
|
225
|
+
<label>Baud Rate</label>
|
|
226
|
+
<input type="number" name="baud_rate" value="115200" class="form-control">
|
|
227
|
+
</div>
|
|
228
|
+
<button type="submit" class="btn btn-primary">Add Port</button>
|
|
229
|
+
<button type="button" class="btn btn-secondary" onclick="toggleEdit('port')">Cancel</button>
|
|
230
|
+
</form>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- Network Addresses -->
|
|
234
|
+
<div class="detail-card">
|
|
235
|
+
<h3>Network Addresses
|
|
236
|
+
<button class="btn btn-sm btn-primary" onclick="toggleEdit('network')" style="float: right;">Add Address</button>
|
|
237
|
+
</h3>
|
|
238
|
+
{% if sbc.network_addresses %}
|
|
239
|
+
<table class="detail-table">
|
|
240
|
+
<tr><th>Type</th><th>IP Address</th><th>MAC</th><th></th></tr>
|
|
241
|
+
{% for addr in sbc.network_addresses %}
|
|
242
|
+
<tr>
|
|
243
|
+
<td>{{ addr.address_type.value }}</td>
|
|
244
|
+
<td>{{ addr.ip_address }}</td>
|
|
245
|
+
<td>{{ addr.mac_address or '-' }}</td>
|
|
246
|
+
<td>
|
|
247
|
+
<form action="{{ url_for('views.sbc_network_remove', name=sbc.name, address_type=addr.address_type.value) }}" method="post" class="inline-form">
|
|
248
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
249
|
+
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Remove this address?')">X</button>
|
|
250
|
+
</form>
|
|
251
|
+
</td>
|
|
252
|
+
</tr>
|
|
253
|
+
{% endfor %}
|
|
254
|
+
</table>
|
|
255
|
+
{% if sbc.primary_ip %}
|
|
256
|
+
<p class="mt-2">
|
|
257
|
+
SSH: <code>ssh {{ sbc.ssh_user }}@{{ sbc.primary_ip }}</code>
|
|
258
|
+
</p>
|
|
259
|
+
{% endif %}
|
|
260
|
+
{% else %}
|
|
261
|
+
<p class="text-muted">No network addresses configured.</p>
|
|
262
|
+
{% endif %}
|
|
263
|
+
|
|
264
|
+
<!-- Add network form (hidden by default) -->
|
|
265
|
+
<form id="network-edit" action="{{ url_for('views.sbc_network_set', name=sbc.name) }}" method="post" style="display: none; margin-top: 1rem;">
|
|
266
|
+
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
|
267
|
+
<div class="form-group">
|
|
268
|
+
<label>Address Type</label>
|
|
269
|
+
<select name="address_type" class="form-control" required>
|
|
270
|
+
{% for at in AddressType %}
|
|
271
|
+
<option value="{{ at.value }}">{{ at.value }}</option>
|
|
272
|
+
{% endfor %}
|
|
273
|
+
</select>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="form-group">
|
|
276
|
+
<label>IP Address</label>
|
|
277
|
+
<input type="text" name="ip_address" placeholder="192.168.1.100" class="form-control" required>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="form-group">
|
|
280
|
+
<label>MAC Address (optional)</label>
|
|
281
|
+
<input type="text" name="mac_address" placeholder="aa:bb:cc:dd:ee:ff" class="form-control">
|
|
282
|
+
</div>
|
|
283
|
+
<div class="form-group">
|
|
284
|
+
<label>Hostname (optional)</label>
|
|
285
|
+
<input type="text" name="hostname" placeholder="sbc1.local" class="form-control">
|
|
286
|
+
</div>
|
|
287
|
+
<button type="submit" class="btn btn-primary">Add Address</button>
|
|
288
|
+
<button type="button" class="btn btn-secondary" onclick="toggleEdit('network')">Cancel</button>
|
|
289
|
+
</form>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<script>
|
|
294
|
+
function toggleEdit(section) {
|
|
295
|
+
var display = document.getElementById(section + '-display');
|
|
296
|
+
var edit = document.getElementById(section + '-edit');
|
|
297
|
+
|
|
298
|
+
if (display) {
|
|
299
|
+
display.style.display = display.style.display === 'none' ? 'block' : 'none';
|
|
300
|
+
}
|
|
301
|
+
if (edit) {
|
|
302
|
+
edit.style.display = edit.style.display === 'none' ? 'block' : 'none';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
</script>
|
|
306
|
+
{% endblock %}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Settings - Lab Controller{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="page-header">
|
|
7
|
+
<a href="{{ url_for('views.index') }}" class="back-link">← Back to Dashboard</a>
|
|
8
|
+
<h1>Settings</h1>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="detail-grid">
|
|
12
|
+
<!-- Database Settings -->
|
|
13
|
+
<div class="detail-card">
|
|
14
|
+
<h3>Database</h3>
|
|
15
|
+
<table class="detail-table">
|
|
16
|
+
<tr><th>Path</th><td><code>{{ config.database_path if config else 'Not configured' }}</code></td></tr>
|
|
17
|
+
</table>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Serial Settings -->
|
|
21
|
+
<div class="detail-card">
|
|
22
|
+
<h3>Serial</h3>
|
|
23
|
+
<table class="detail-table">
|
|
24
|
+
<tr><th>Device Directory</th><td><code>{{ config.serial.dev_dir if config else '/dev/lab' }}</code></td></tr>
|
|
25
|
+
<tr><th>Default Baud</th><td>{{ config.serial.default_baud if config else '115200' }}</td></tr>
|
|
26
|
+
</table>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- ser2net Settings -->
|
|
30
|
+
<div class="detail-card">
|
|
31
|
+
<h3>ser2net</h3>
|
|
32
|
+
<table class="detail-table">
|
|
33
|
+
<tr><th>Enabled</th><td>{{ 'Yes' if (config and config.ser2net.enabled) else 'No' }}</td></tr>
|
|
34
|
+
<tr><th>Config File</th><td><code>{{ config.ser2net.config_file if config else '/etc/ser2net.yaml' }}</code></td></tr>
|
|
35
|
+
<tr><th>Base TCP Port</th><td>{{ config.serial.base_tcp_port if config else '4000' }}</td></tr>
|
|
36
|
+
</table>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Proxy Settings -->
|
|
40
|
+
<div class="detail-card">
|
|
41
|
+
<h3>Proxy</h3>
|
|
42
|
+
<table class="detail-table">
|
|
43
|
+
<tr><th>Port Base</th><td>{{ config.proxy.port_base if config else '5500' }}</td></tr>
|
|
44
|
+
<tr><th>Port Range</th><td>{{ config.proxy.port_range if config else '100' }}</td></tr>
|
|
45
|
+
<tr><th>Write Policy</th><td>{{ config.proxy.write_policy if config else 'first' }}</td></tr>
|
|
46
|
+
<tr><th>Max Clients</th><td>{{ config.proxy.max_clients if config else '10' }}</td></tr>
|
|
47
|
+
<tr><th>Log Directory</th><td><code>{{ config.proxy.log_dir if config else '~/.local/share/labctl/logs' }}</code></td></tr>
|
|
48
|
+
</table>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- Health Settings -->
|
|
52
|
+
<div class="detail-card">
|
|
53
|
+
<h3>Health Monitoring</h3>
|
|
54
|
+
<table class="detail-table">
|
|
55
|
+
<tr><th>Check Interval</th><td>{{ config.health.check_interval if config else '10' }}s</td></tr>
|
|
56
|
+
<tr><th>Ping Timeout</th><td>{{ config.health.ping_timeout if config else '2.0' }}s</td></tr>
|
|
57
|
+
<tr><th>Serial Timeout</th><td>{{ config.health.serial_timeout if config else '2.0' }}s</td></tr>
|
|
58
|
+
{% set retention = config.health.status_retention_days if config else 30 %}
|
|
59
|
+
<tr><th>Status Retention</th><td>{{ 'keep all' if retention == 0 else retention ~ ' days' }}</td></tr>
|
|
60
|
+
<tr><th>Alert on Offline</th><td>{{ 'Yes' if (config and config.health.alert_on_offline) else 'No' }}</td></tr>
|
|
61
|
+
<tr><th>Alert on Power Change</th><td>{{ 'Yes' if (config and config.health.alert_on_power_change) else 'No' }}</td></tr>
|
|
62
|
+
<tr><th>Alert Log</th><td><code>{{ config.health.alert_log_path if config else '~/.local/share/labctl/alerts.log' }}</code></td></tr>
|
|
63
|
+
</table>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Configuration File -->
|
|
67
|
+
<div class="detail-card">
|
|
68
|
+
<h3>Configuration File</h3>
|
|
69
|
+
<p class="text-muted">Settings are loaded from <code>~/.config/labctl/config.yaml</code></p>
|
|
70
|
+
<p class="text-muted">Environment variables can override settings:</p>
|
|
71
|
+
<ul class="text-muted">
|
|
72
|
+
<li><code>LABCTL_DEV_DIR</code> - Device directory</li>
|
|
73
|
+
<li><code>LABCTL_BASE_TCP_PORT</code> - Base TCP port</li>
|
|
74
|
+
<li><code>LABCTL_DATABASE</code> - Database path</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{% endblock %}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ sbc.name }} History - Lab Controller{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div class="page-header">
|
|
7
|
+
<a href="{{ url_for('views.sbc_detail', name=sbc.name) }}" class="back-link">← Back to {{ sbc.name }}</a>
|
|
8
|
+
<h1>Status History: {{ sbc.name }}</h1>
|
|
9
|
+
<span class="status-badge status-{{ sbc.status.value }}">Current: {{ sbc.status.value }}</span>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="detail-card">
|
|
13
|
+
<h3>Status Timeline</h3>
|
|
14
|
+
|
|
15
|
+
{% if history %}
|
|
16
|
+
<table class="detail-table history-table">
|
|
17
|
+
<thead>
|
|
18
|
+
<tr>
|
|
19
|
+
<th>Timestamp</th>
|
|
20
|
+
<th>Status</th>
|
|
21
|
+
<th>Details</th>
|
|
22
|
+
</tr>
|
|
23
|
+
</thead>
|
|
24
|
+
<tbody>
|
|
25
|
+
{% for entry in history %}
|
|
26
|
+
<tr>
|
|
27
|
+
<td class="timestamp">{{ entry.logged_at }}</td>
|
|
28
|
+
<td>
|
|
29
|
+
<span class="status-badge status-{{ entry.status }}">{{ entry.status }}</span>
|
|
30
|
+
</td>
|
|
31
|
+
<td class="details">{{ entry.details or '-' }}</td>
|
|
32
|
+
</tr>
|
|
33
|
+
{% endfor %}
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
{% else %}
|
|
37
|
+
<p class="text-muted">No status history recorded yet.</p>
|
|
38
|
+
<p>Run <code>labctl health-check --update</code> or <code>labctl monitor</code> to start tracking status changes.</p>
|
|
39
|
+
{% endif %}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
.history-table {
|
|
44
|
+
width: 100%;
|
|
45
|
+
}
|
|
46
|
+
.history-table th {
|
|
47
|
+
text-align: left;
|
|
48
|
+
padding: 0.5rem;
|
|
49
|
+
border-bottom: 2px solid #ddd;
|
|
50
|
+
}
|
|
51
|
+
.history-table td {
|
|
52
|
+
padding: 0.5rem;
|
|
53
|
+
border-bottom: 1px solid #eee;
|
|
54
|
+
}
|
|
55
|
+
.history-table .timestamp {
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
font-family: monospace;
|
|
58
|
+
color: #666;
|
|
59
|
+
}
|
|
60
|
+
.history-table .details {
|
|
61
|
+
font-size: 0.9em;
|
|
62
|
+
color: #555;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
{% endblock %}
|