devicectl-core 0.1.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 (47) hide show
  1. devicectl/__init__.py +18 -0
  2. devicectl/cli/__init__.py +1 -0
  3. devicectl/cli/command.py +95 -0
  4. devicectl/cli/exits.py +32 -0
  5. devicectl/cli/fanout.py +142 -0
  6. devicectl/cli/main.py +69 -0
  7. devicectl/cli/output.py +299 -0
  8. devicectl/cli/parser.py +80 -0
  9. devicectl/cli/report.py +86 -0
  10. devicectl/cli/target.py +26 -0
  11. devicectl/clock.py +57 -0
  12. devicectl/devtools/__init__.py +6 -0
  13. devicectl/devtools/frontlint.py +935 -0
  14. devicectl/devtools/htmcheck.py +396 -0
  15. devicectl/devtools/rendercheck.py +384 -0
  16. devicectl/doctor.py +112 -0
  17. devicectl/errors.py +68 -0
  18. devicectl/fields.py +564 -0
  19. devicectl/meta.py +64 -0
  20. devicectl/paths.py +40 -0
  21. devicectl/progress.py +77 -0
  22. devicectl/report.py +67 -0
  23. devicectl/testing.py +199 -0
  24. devicectl/trace.py +333 -0
  25. devicectl/web/__init__.py +1 -0
  26. devicectl/web/agents.py +94 -0
  27. devicectl/web/events.py +171 -0
  28. devicectl/web/http.py +243 -0
  29. devicectl/web/progress.py +101 -0
  30. devicectl/web/server.py +1013 -0
  31. devicectl/web/static/core.css +3034 -0
  32. devicectl/web/static/js/api.js +198 -0
  33. devicectl/web/static/js/band.js +640 -0
  34. devicectl/web/static/js/chart.js +400 -0
  35. devicectl/web/static/js/drafts.js +312 -0
  36. devicectl/web/static/js/notify.js +272 -0
  37. devicectl/web/static/js/panels.js +432 -0
  38. devicectl/web/static/js/shell.js +672 -0
  39. devicectl/web/static/js/trace.js +133 -0
  40. devicectl/web/static/js/ui.js +1139 -0
  41. devicectl/web/static/vendor/preact-htm.module.js +27 -0
  42. devicectl/web/worker.py +697 -0
  43. devicectl_core-0.1.0.dist-info/METADATA +131 -0
  44. devicectl_core-0.1.0.dist-info/RECORD +47 -0
  45. devicectl_core-0.1.0.dist-info/WHEEL +4 -0
  46. devicectl_core-0.1.0.dist-info/licenses/LICENSE +287 -0
  47. devicectl_core-0.1.0.dist-info/licenses/NOTICE +13 -0
@@ -0,0 +1,432 @@
1
+ /* Shared plumbing for the panel tabs: fetch-once state, edit-and-apply
2
+ * cards, and the small pieces every panel reuses.
3
+ *
4
+ * A panel tab is not a dashboard: it is opened to *do* something, so what
5
+ * it reads is kept here -- once per device, across tab switches -- rather
6
+ * than following the dashboard's reload-on-connect flow. What a panel
7
+ * has changed and not sent is in drafts.js.
8
+ */
9
+
10
+ import { Card, DASH, Help, Row, Select } from '/core/js/ui.js';
11
+ import { html, useEffect, useState } from '/core/vendor/preact-htm.module.js';
12
+
13
+ /* What every panel has read, keyed by the endpoint rather than by the
14
+ * component that asked for it.
15
+ *
16
+ * Two things wanted this out of the components' own `useState`. A panel
17
+ * that unmounts -- which is what switching tabs used to do -- took its
18
+ * document with it, so coming back re-read a device that had not changed.
19
+ * And two cards can want the same document: two halves of one settings
20
+ * page each used to fetch the same endpoint for itself, which is two round
21
+ * trips over the one connection for one answer, and two copies that could
22
+ * disagree. One entry per key fixes both: the second caller finds the
23
+ * first one's document, or waits on the fetch already in flight.
24
+ *
25
+ * It is module state on purpose. It belongs to the device, not to the
26
+ * tree, and the program empties it when the device changes.
27
+ */
28
+ const cache = new Map(); // key -> { doc, loading, error, promise }
29
+ const listeners = new Set();
30
+
31
+ const EMPTY_ENTRY = { doc: null, loading: false, error: '', promise: null };
32
+
33
+ function announce() {
34
+ for (const listener of [...listeners]) listener();
35
+ }
36
+
37
+ function entryOf(key) {
38
+ return cache.get(key) || EMPTY_ENTRY;
39
+ }
40
+
41
+ /* Forget everything: another device, another set of answers. */
42
+ export function resetPanels() {
43
+ cache.clear();
44
+ announce();
45
+ }
46
+
47
+ /* Replace what a key holds -- for a write whose reply is the new document,
48
+ * so the card that wrote it and the card beside it both move at once. */
49
+ export function putPanel(key, doc) {
50
+ cache.set(key, { doc, loading: false, error: '', promise: null });
51
+ announce();
52
+ }
53
+
54
+ /* Read a panel's document once per device. Returns [doc, loading, error,
55
+ * read], the same tuple every caller destructures.
56
+ *
57
+ * `eager` is what a panel turns off when its read is not something to do
58
+ * on arrival -- a listing fetched from the internet, a test that drives
59
+ * the device -- so the read waits for the button that asks for it and the
60
+ * answer is still kept, across tab switches, like every other panel's. */
61
+ export function usePanel(key, load, { eager = true } = {}) {
62
+ const [, bump] = useState(0);
63
+
64
+ useEffect(() => {
65
+ const wake = () => bump((n) => n + 1);
66
+ listeners.add(wake);
67
+ return () => {
68
+ listeners.delete(wake);
69
+ };
70
+ }, []);
71
+
72
+ const read = () => {
73
+ const running = cache.get(key)?.promise;
74
+ if (running) return running; // someone else is already asking
75
+ const held = entryOf(key).doc;
76
+ const promise = Promise.resolve()
77
+ .then(load)
78
+ .then((doc) => {
79
+ cache.set(key, { doc, loading: false, error: '', promise: null });
80
+ announce();
81
+ return doc;
82
+ })
83
+ .catch((err) => {
84
+ /* Keep whatever was read before: a failed refresh should not blank
85
+ * a card that was showing something a moment ago. */
86
+ cache.set(key, {
87
+ doc: held,
88
+ loading: false,
89
+ error: err.message,
90
+ promise: null,
91
+ });
92
+ announce();
93
+ });
94
+ cache.set(key, { doc: held, loading: true, error: '', promise });
95
+ announce();
96
+ return promise;
97
+ };
98
+
99
+ useEffect(() => {
100
+ if (eager && !cache.has(key)) read();
101
+ }, [key]);
102
+
103
+ const entry = entryOf(key);
104
+ return [entry.doc, entry.loading, entry.error, read];
105
+ }
106
+
107
+ /* A whole card that could not be read: said once, with the way to try
108
+ * again, rather than a page of em dashes that each shrug.
109
+ *
110
+ * `title` keeps it inside the card it belongs to. Without one, a panel
111
+ * being read and a panel that failed were both a bare line of grey text
112
+ * where a card should be, so every card arriving changed the shape of the
113
+ * grid under the ones already there. */
114
+ export function PanelError({ error, loading, onRetry, title }) {
115
+ if (!error) return null;
116
+ const said = html`<div class="empty">
117
+ ${error}
118
+ <div class="actions centred">
119
+ <button class="btn" disabled=${loading} onClick=${onRetry}>Try again</button>
120
+ </div>
121
+ </div>`;
122
+ return title
123
+ ? html`<${Card} title=${title} badge=${html`<span class="badge bad">unread</span>`}>
124
+ ${said}
125
+ <//>`
126
+ : said;
127
+ }
128
+
129
+ /* A card that is being read. It holds its place in the grid, and shows
130
+ * the shape of what is coming rather than the word "loading" -- so a page
131
+ * of panels arriving settles into itself instead of jumping. */
132
+ export function Loading({ loading, what, title, rows }) {
133
+ if (!loading) return null;
134
+ const bones = html`<div class="skeleton" aria-label=${what || 'Reading...'}>
135
+ ${Array.from({ length: rows || 4 }, (_, i) => html`<span class="bone" key=${i}></span>`)}
136
+ </div>`;
137
+ return title
138
+ ? html`<${Card} title=${title}>${bones}<//>`
139
+ : html`<div class="empty">${what || 'Reading...'}</div>`;
140
+ }
141
+
142
+ /* What a panel shows instead of itself, while it has nothing to show.
143
+ *
144
+ * Twelve panels wrote this pair of early returns out by hand, with the
145
+ * card's title typed three times at each site -- and three of them had
146
+ * drifted into returning `null` on an error, so a card that failed to read
147
+ * simply was not there and nothing said why. Here it is once: hand it the
148
+ * tuple `usePanel` returned, and render what comes back if anything does.
149
+ *
150
+ * const state = usePanel('auth', onLoad);
151
+ * const [doc] = state;
152
+ * const waiting = panelWait(state, { title: 'Authorization' });
153
+ * if (waiting !== undefined) return waiting;
154
+ *
155
+ * `undefined` -- not `null` -- is the answer that means "carry on": a card
156
+ * may legitimately want to render nothing, which is what `null` is, and
157
+ * `quiet` is when. That is the second and third card behind one read: the
158
+ * first of them reports the failure, and one endpoint saying it three
159
+ * times is one endpoint shouting. They still wait for themselves, though,
160
+ * because a card that renders nothing while a read is in flight is a card
161
+ * the grid does not know is coming -- three panels behind one read put up
162
+ * one skeleton and then landed as three, and everything below them jumped
163
+ * a row. A skeleton is a card's way of saying it will be here.
164
+ */
165
+ export function panelWait(state, { title, what, rows, quiet = false } = {}) {
166
+ const [doc, loading, error, read] = state;
167
+ if (error) {
168
+ if (quiet) return null;
169
+ return html`<${PanelError}
170
+ error=${error}
171
+ loading=${loading}
172
+ onRetry=${read}
173
+ title=${title}
174
+ />`;
175
+ }
176
+ if (!doc) {
177
+ return html`<${Loading} loading what=${what} title=${title} rows=${rows} />`;
178
+ }
179
+ return undefined;
180
+ }
181
+
182
+ /* A select for an enumerated register, offered as the device's own
183
+ * dropdown: value to send, title to show, in table order. */
184
+ function EnumSelect({ value, table, onChange, disabled, includeBlank, pending }) {
185
+ const entries = Object.entries(table || {}).map(([v, title]) => ({
186
+ value: String(v),
187
+ title,
188
+ }));
189
+ if (includeBlank) entries.unshift({ value: '', title: '—' });
190
+ return html`<${Select}
191
+ value=${value === null || value === undefined ? '' : String(value)}
192
+ entries=${entries}
193
+ onChange=${onChange}
194
+ disabled=${disabled}
195
+ pending=${pending}
196
+ />`;
197
+ }
198
+
199
+ /* One number, as a small input with its unit, pending-coloured while it
200
+ * differs from what the device holds. */
201
+ function NumInput({ value, live, min, max, step, unit, onChange, disabled, pending: held }) {
202
+ const pending = held ?? Number(value) !== Number(live);
203
+ return html`<span class="set num">
204
+ <input
205
+ type="number"
206
+ class=${pending ? 'pending' : ''}
207
+ min=${min}
208
+ max=${max}
209
+ step=${step || 1}
210
+ value=${value ?? ''}
211
+ disabled=${disabled}
212
+ onInput=${(e) => onChange(e.target.value === '' ? null : Number(e.target.value))}
213
+ />
214
+ ${unit && html`<span class=${`unit${pending ? ' pending' : ''}`}>${unit}</span>`}
215
+ </span>`;
216
+ }
217
+
218
+ /* One text input, edged in amber while it differs from what the device
219
+ * holds -- the same "this is a draft, nothing has been written" cue a
220
+ * number or a slider gets. It was computed here and then thrown away, so
221
+ * a typed-over URL looked exactly like the one the device is using. */
222
+ function TextInput({ value, live, onChange, disabled, placeholder, width, pending: held }) {
223
+ const pending = held ?? (value ?? '') !== (live ?? '');
224
+ return html`<input
225
+ type="text"
226
+ class=${pending ? 'pending' : ''}
227
+ style=${width ? `width:${width}` : ''}
228
+ value=${value ?? ''}
229
+
230
+ placeholder=${placeholder || ''}
231
+ disabled=${disabled}
232
+ onInput=${(e) => onChange(e.target.value)}
233
+ />`;
234
+ }
235
+
236
+ /* --- one setting, as a row -------------------------------------------------
237
+ *
238
+ * Six tabs were each writing the same three shapes out by hand: a `Row`
239
+ * whose value is a ternary on `readOnly`, the reading spelled one way on
240
+ * one side of it and an input on the other. Which is why the same setting
241
+ * could show "16 A" on one tab and "16" on the next, why a missing value
242
+ * was a hand-typed em dash here and the muted one `Row` draws there, and
243
+ * why every enumerated field repeated the same
244
+ * `e.target.value === '' ? null : Number(...)` decode with its own chance
245
+ * of getting it wrong. These three say it once.
246
+ */
247
+
248
+ /* What a select hands back, in the type the device's register wants: the
249
+ * blank option is "unset" in every case, and the rest is the caller's
250
+ * `kind` -- most registers are numbers, a couple are strings, and the
251
+ * measurement source is a boolean the app writes as 0 or 1. */
252
+ function decode(kind, raw) {
253
+ if (raw === '') return null;
254
+ if (kind === 'boolean') return raw === '1' || raw === 'true';
255
+ if (kind === 'text') return raw;
256
+ return Number(raw);
257
+ }
258
+
259
+ /* ... and the option that value is, on the way back in.
260
+ *
261
+ * A boolean register is stored as 0 or 1 and named by a table keyed on
262
+ * those, but what the device is written with is a real boolean -- so a
263
+ * draft holds `true`, whose option is "1". Without this the select fell
264
+ * back to its first option the moment you changed it, which is an enumerated
265
+ * setting silently reading as its own opposite. */
266
+ function encode(kind, value) {
267
+ if (value === null || value === undefined) return '';
268
+ if (kind === 'boolean') return value ? '1' : '0';
269
+ return String(value);
270
+ }
271
+
272
+ /* A number the device holds, with its unit. Read-only, the unit is part
273
+ * of the reading; editable, it is the label beside the input.
274
+ *
275
+ * `pending` says the row holds an edit nobody has applied; left out, it is
276
+ * worked out from `value` against `live`. Every row below takes it, so a
277
+ * card's changed fields are the same colour whatever kind of field they
278
+ * are -- see `Row`. */
279
+ export function NumRow({
280
+ k,
281
+ title,
282
+ readOnly,
283
+ value,
284
+ live,
285
+ unit,
286
+ min,
287
+ max,
288
+ step,
289
+ onChange,
290
+ disabled,
291
+ pending,
292
+ hint,
293
+ }) {
294
+ const said =
295
+ value === null || value === undefined ? undefined : `${value}${unit ? ` ${unit}` : ''}`;
296
+ const held = !readOnly && (pending ?? (live !== undefined && Number(value) !== Number(live)));
297
+ /* What it may be set to, on the value: the name's tooltip is for what it
298
+ * is (`hint`). */
299
+ const allowed =
300
+ !readOnly && min !== undefined && min !== null && max !== undefined && max !== null
301
+ ? `allowed ${min}${unit ? ` ${unit}` : ''} to ${max}${unit ? ` ${unit}` : ''}`
302
+ : undefined;
303
+ return html`<${Row}
304
+ k=${k}
305
+ title=${title ?? allowed}
306
+ hint=${hint}
307
+ pending=${held}
308
+ v=${readOnly
309
+ ? said
310
+ : html`<${NumInput}
311
+ value=${value}
312
+ live=${live}
313
+ min=${min}
314
+ max=${max}
315
+ step=${step}
316
+ unit=${unit}
317
+ onChange=${onChange}
318
+ disabled=${disabled}
319
+ pending=${held}
320
+ />`}
321
+ />`;
322
+ }
323
+
324
+ /* An enumerated register, shown as the catalog's word for it and edited as
325
+ * a dropdown. */
326
+ export function EnumRow({
327
+ k,
328
+ title,
329
+ readOnly,
330
+ value,
331
+ table,
332
+ kind,
333
+ onChange,
334
+ disabled,
335
+ includeBlank,
336
+ pending,
337
+ hint,
338
+ }) {
339
+ const held = !readOnly && Boolean(pending);
340
+ return html`<${Row}
341
+ k=${k}
342
+ title=${title}
343
+ hint=${hint}
344
+ pending=${held}
345
+ v=${readOnly
346
+ ? table?.[encode(kind, value)]
347
+ : html`<${EnumSelect}
348
+ value=${encode(kind, value)}
349
+ table=${table}
350
+ onChange=${(value) => onChange(decode(kind, value))}
351
+ disabled=${disabled}
352
+ includeBlank=${includeBlank}
353
+ pending=${held}
354
+ />`}
355
+ />`;
356
+ }
357
+
358
+ /* A string register -- a URL, a path, a name. */
359
+ export function TextRow({
360
+ k,
361
+ title,
362
+ readOnly,
363
+ value,
364
+ live,
365
+ placeholder,
366
+ width,
367
+ onChange,
368
+ disabled,
369
+ pending,
370
+ hint,
371
+ }) {
372
+ const held = !readOnly && (pending ?? (live !== undefined && (value ?? '') !== (live ?? '')));
373
+ return html`<${Row}
374
+ k=${k}
375
+ title=${title}
376
+ hint=${hint}
377
+ pending=${held}
378
+ v=${readOnly
379
+ ? value || undefined
380
+ : html`<${TextInput}
381
+ value=${value ?? ''}
382
+ live=${live}
383
+ placeholder=${placeholder}
384
+ width=${width}
385
+ onChange=${onChange}
386
+ disabled=${disabled}
387
+ pending=${held}
388
+ />`}
389
+ />`;
390
+ }
391
+
392
+ /* A row whose value side is a toggle. */
393
+ export function ToggleRow({ k, value, onChange, disabled, label, title, hint, pending }) {
394
+ return html`<${Row}
395
+ k=${k}
396
+ title=${title}
397
+ hint=${hint}
398
+ pending=${pending}
399
+ v=${html`<label class=${pending ? 'toggle pending' : 'toggle'}>
400
+ <input
401
+ type="checkbox"
402
+ checked=${Boolean(value)}
403
+ disabled=${disabled}
404
+ onChange=${(e) => onChange(e.target.checked)}
405
+ />
406
+ ${label || (value ? 'on' : 'off')}
407
+ </label>`}
408
+ />`;
409
+ }
410
+
411
+ /* Rows rendered from a backend [label, value] table (a network panel, a health check). */
412
+ function Rows({ rows }) {
413
+ if (!rows?.length) return html`<div class="empty">Nothing reported.</div>`;
414
+ return html`<div class="rows">
415
+ ${rows.map((r) => html`<${Row} key=${r.label} k=${r.label} v=${r.value || DASH} />`)}
416
+ </div>`;
417
+ }
418
+
419
+ /* A plain card of rows, for read-only panels.
420
+ *
421
+ * The note is a line *about* the card, so it goes above what it is about
422
+ * and folds -- the same `Help` every other card's explanation uses. It
423
+ * used to be a paragraph after the last row, which is where a card puts
424
+ * its footnotes: a sentence explaining what the rows are, printed
425
+ * underneath them, read last by anyone who read it at all. `more` is the
426
+ * rest of it, and without one the note is simply a line of prose. */
427
+ export function PanelCard({ title, rows, note, more, badge }) {
428
+ return html`<${Card} title=${title} actions=${badge}>
429
+ ${note && html`<${Help} summary=${note}>${more}<//>`}
430
+ <${Rows} rows=${rows} />
431
+ <//>`;
432
+ }