pixie-lab 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.
- pixie/__init__.py +20 -0
- pixie/_util.py +38 -0
- pixie/catalog/__init__.py +25 -0
- pixie/catalog/_fetcher.py +513 -0
- pixie/catalog/_routes.py +392 -0
- pixie/catalog/_schema.py +263 -0
- pixie/catalog/_store.py +234 -0
- pixie/deploy/__init__.py +26 -0
- pixie/deploy/_main.py +321 -0
- pixie/deploy/_templates.py +153 -0
- pixie/disks.py +85 -0
- pixie/events/__init__.py +28 -0
- pixie/events/_kinds.py +203 -0
- pixie/events/_log.py +210 -0
- pixie/events/_routes.py +42 -0
- pixie/exports/__init__.py +17 -0
- pixie/exports/_routes.py +211 -0
- pixie/exports/_store.py +156 -0
- pixie/exports/_supervisor.py +240 -0
- pixie/flash.py +1971 -0
- pixie/images.py +473 -0
- pixie/machines/__init__.py +16 -0
- pixie/machines/_routes.py +190 -0
- pixie/machines/_store.py +623 -0
- pixie/oras.py +547 -0
- pixie/pivot/__init__.py +180 -0
- pixie/pivot/nbdboot +348 -0
- pixie/pxe/__init__.py +19 -0
- pixie/pxe/_renderer.py +244 -0
- pixie/pxe/_routes.py +386 -0
- pixie/tftp/__init__.py +21 -0
- pixie/tftp/_supervisor.py +129 -0
- pixie/tui/__init__.py +185 -0
- pixie/tui/_app.py +2219 -0
- pixie/tui_catalog.py +657 -0
- pixie/web/__init__.py +6 -0
- pixie/web/_auth.py +70 -0
- pixie/web/_settings_store.py +211 -0
- pixie/web/_static/.gitkeep +0 -0
- pixie/web/_static/bootstrap-icons.min.css +5 -0
- pixie/web/_static/bootstrap.min.css +12 -0
- pixie/web/_static/fonts/bootstrap-icons.woff +0 -0
- pixie/web/_static/fonts/bootstrap-icons.woff2 +0 -0
- pixie/web/_static/htmx.min.js +1 -0
- pixie/web/_static/pixie-favicon.png +0 -0
- pixie/web/_static/sse.js +290 -0
- pixie/web/_table_state.py +261 -0
- pixie/web/_templates/_partials/page_description.html +22 -0
- pixie/web/_templates/_partials/recent_events.html +47 -0
- pixie/web/_templates/_partials/table_helpers.html +189 -0
- pixie/web/_templates/catalog.html +364 -0
- pixie/web/_templates/catalog_detail.html +386 -0
- pixie/web/_templates/dashboard.html +256 -0
- pixie/web/_templates/events.html +125 -0
- pixie/web/_templates/ipxe/bootstrap.j2 +12 -0
- pixie/web/_templates/ipxe/exit.j2 +10 -0
- pixie/web/_templates/ipxe/nbdboot.j2 +57 -0
- pixie/web/_templates/ipxe/pixie-live-env.j2 +55 -0
- pixie/web/_templates/ipxe/unavailable.j2 +14 -0
- pixie/web/_templates/layout.html +345 -0
- pixie/web/_templates/login.html +40 -0
- pixie/web/_templates/machine_detail.html +786 -0
- pixie/web/_templates/machines.html +186 -0
- pixie/web/_templates/settings.html +265 -0
- pixie/web/main.py +1910 -0
- pixie_lab-0.1.0.dist-info/METADATA +107 -0
- pixie_lab-0.1.0.dist-info/RECORD +70 -0
- pixie_lab-0.1.0.dist-info/WHEEL +4 -0
- pixie_lab-0.1.0.dist-info/entry_points.txt +3 -0
- pixie_lab-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
{% from "_partials/table_helpers.html" import search_input, sort_header, pagination_inline %}
|
|
3
|
+
{% from "_partials/page_description.html" import page_description %}
|
|
4
|
+
{% block title %}Events - pixie{% endblock %}
|
|
5
|
+
{% block subnav %}
|
|
6
|
+
<nav class="navbar subnav-strip" aria-label="Section sub-navigation">
|
|
7
|
+
<div class="container">
|
|
8
|
+
{# LEFT: anchor links. Don't repeat the top-nav ("Events");
|
|
9
|
+
use section-specific verbs. #}
|
|
10
|
+
<span class="subnav-jumps d-flex align-items-center flex-wrap">
|
|
11
|
+
<a href="#log">Log</a>
|
|
12
|
+
</span>
|
|
13
|
+
{# The kind + related-to filter dropdowns used to live here.
|
|
14
|
+
They moved into the table's card-header row alongside the
|
|
15
|
+
freeform filter + pagination so an operator's eye lands on
|
|
16
|
+
"what am I filtering", "what am I searching for", "how
|
|
17
|
+
many are there" in one horizontal sweep -- matching the
|
|
18
|
+
layout intent documented in table_helpers.html. #}
|
|
19
|
+
</div>
|
|
20
|
+
</nav>
|
|
21
|
+
{% endblock %}
|
|
22
|
+
{% block content %}
|
|
23
|
+
{{ page_description('clock-history', 'Chronological log of every state change pixie has emitted: machines binding + status POSTs, catalog images fetching, exports going up + down, admin logins. Filter by kind or by the subject it\'s about; freeform search narrows the summary column. The tail is truncated to the newest 2000 events by default.') }}
|
|
24
|
+
<div class="card" id="log">
|
|
25
|
+
<div class="card-header bg-light fw-semibold d-flex flex-wrap align-items-center gap-2 py-2">
|
|
26
|
+
<span class="text-nowrap"><i class="bi bi-clock-history me-1"></i>Events</span>
|
|
27
|
+
{{ search_input('/ui/events', q, preserved, 'kind / related to / summary') }}
|
|
28
|
+
{# Strict-equality narrowings applied BEFORE the freeform ``q``
|
|
29
|
+
filter. The two selects are dropped-label + icon-tooltip
|
|
30
|
+
only so they don't push pagination onto a second row on
|
|
31
|
+
medium viewports. Each is its own tiny form that submits on
|
|
32
|
+
``change``; hidden inputs preserve the OTHER filter + q so
|
|
33
|
+
``page`` + ``sort`` survive a filter flip. #}
|
|
34
|
+
<form method="get" action="/ui/events" class="d-flex align-items-center m-0">
|
|
35
|
+
{% if preserved.q %}<input type="hidden" name="q" value="{{ preserved.q }}">{% endif %}
|
|
36
|
+
{% if subject_kind_selected %}
|
|
37
|
+
<input type="hidden" name="subject_kind" value="{{ subject_kind_selected }}">
|
|
38
|
+
{% endif %}
|
|
39
|
+
<label for="filter-kind" class="text-muted small mb-0 me-1"
|
|
40
|
+
title="Filter by kind">
|
|
41
|
+
<i class="bi bi-tag"></i>
|
|
42
|
+
</label>
|
|
43
|
+
<select id="filter-kind" name="kind" onchange="this.form.submit()"
|
|
44
|
+
class="form-select form-select-sm" style="max-width: 14rem;"
|
|
45
|
+
aria-label="Filter by kind">
|
|
46
|
+
<option value="">kind: any</option>
|
|
47
|
+
{% for k in kind_choices %}
|
|
48
|
+
<option value="{{ k }}" {% if k == kind_selected %}selected{% endif %}>{{ k }}</option>
|
|
49
|
+
{% endfor %}
|
|
50
|
+
</select>
|
|
51
|
+
</form>
|
|
52
|
+
<form method="get" action="/ui/events" class="d-flex align-items-center m-0">
|
|
53
|
+
{% if preserved.q %}<input type="hidden" name="q" value="{{ preserved.q }}">{% endif %}
|
|
54
|
+
{% if kind_selected %}
|
|
55
|
+
<input type="hidden" name="kind" value="{{ kind_selected }}">
|
|
56
|
+
{% endif %}
|
|
57
|
+
<label for="filter-subject" class="text-muted small mb-0 me-1"
|
|
58
|
+
title="Filter by related-to">
|
|
59
|
+
<i class="bi bi-link-45deg"></i>
|
|
60
|
+
</label>
|
|
61
|
+
<select id="filter-subject" name="subject_kind" onchange="this.form.submit()"
|
|
62
|
+
class="form-select form-select-sm" style="max-width: 10rem;"
|
|
63
|
+
aria-label="Filter by related-to">
|
|
64
|
+
<option value="">related: any</option>
|
|
65
|
+
{% for s in subject_kind_choices %}
|
|
66
|
+
<option value="{{ s }}" {% if s == subject_kind_selected %}selected{% endif %}>{{ s }}</option>
|
|
67
|
+
{% endfor %}
|
|
68
|
+
</select>
|
|
69
|
+
</form>
|
|
70
|
+
{% if kind_selected or subject_kind_selected %}
|
|
71
|
+
<a href="/ui/events{% if preserved.q %}?q={{ preserved.q }}{% endif %}"
|
|
72
|
+
class="btn btn-sm btn-link text-nowrap p-0 text-decoration-none"
|
|
73
|
+
title="Clear kind + related-to filters">
|
|
74
|
+
<i class="bi bi-x-circle"></i>
|
|
75
|
+
</a>
|
|
76
|
+
{% endif %}
|
|
77
|
+
{{ pagination_inline(page_state, preserved, label='events') }}
|
|
78
|
+
</div>
|
|
79
|
+
{% if events %}
|
|
80
|
+
<div class="table-responsive">
|
|
81
|
+
<table class="table table-hover align-middle mb-0">
|
|
82
|
+
<thead class="table-light">
|
|
83
|
+
<tr>
|
|
84
|
+
{{ sort_header('When', 'ts', sort, preserved) }}
|
|
85
|
+
{{ sort_header('Kind', 'kind', sort, preserved) }}
|
|
86
|
+
{{ sort_header('Related to', 'subject_id', sort, preserved) }}
|
|
87
|
+
<th scope="col">Summary</th>
|
|
88
|
+
</tr>
|
|
89
|
+
</thead>
|
|
90
|
+
<tbody>
|
|
91
|
+
{% for e in events %}
|
|
92
|
+
<tr>
|
|
93
|
+
<td class="small text-muted text-nowrap">{{ e.ts | fmt_ts }}</td>
|
|
94
|
+
<td><code>{{ e.kind }}</code></td>
|
|
95
|
+
<td>
|
|
96
|
+
{# ``Related to`` reads as ``<what>: <which>``:
|
|
97
|
+
e.subject_kind is the category (entry / machine /
|
|
98
|
+
export / auth), e.subject_id is the specific
|
|
99
|
+
name / MAC / export name. Presenting both with a
|
|
100
|
+
category label makes the row self-describing
|
|
101
|
+
without an operator having to memorise which
|
|
102
|
+
bare token means which. #}
|
|
103
|
+
{% if e.subject_kind %}
|
|
104
|
+
<span class="badge text-bg-light border me-1">{{ e.subject_kind }}</span>
|
|
105
|
+
<code class="small">{{ e.subject_id or '' }}</code>
|
|
106
|
+
{% else %}
|
|
107
|
+
<span class="text-muted small">-</span>
|
|
108
|
+
{% endif %}
|
|
109
|
+
</td>
|
|
110
|
+
<td>{{ e.summary or "" }}</td>
|
|
111
|
+
</tr>
|
|
112
|
+
{% endfor %}
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
</div>
|
|
116
|
+
{% else %}
|
|
117
|
+
<div class="card-body">
|
|
118
|
+
<p class="text-muted mb-0">
|
|
119
|
+
<i class="bi bi-inbox me-1"></i>
|
|
120
|
+
{% if q %}No events match <code>{{ q }}</code>.{% else %}No events yet.{% endif %}
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
{% endif %}
|
|
124
|
+
</div>
|
|
125
|
+
{% endblock %}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!ipxe
|
|
2
|
+
# pixie iPXE bootstrap. TFTP + HTTP consumers land here from DHCP;
|
|
3
|
+
# the chain-load below re-routes each MAC to its target-specific
|
|
4
|
+
# plan at /pxe/<mac>.
|
|
5
|
+
#
|
|
6
|
+
# ``${net0/mac}`` is iPXE's own view of the primary NIC, which
|
|
7
|
+
# matches what the target announces on DHCP + what pixie's
|
|
8
|
+
# machines table keys on. We deliberately don't pass ``?ip=...``:
|
|
9
|
+
# operator inventory lives in bty-inventory (a later PR).
|
|
10
|
+
|
|
11
|
+
set pixie-base http://{{ host }}:{{ port }}
|
|
12
|
+
chain ${pixie-base}/pxe/${net0/mac}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!ipxe
|
|
2
|
+
# pixie plan for {{ mac }} - boot_mode=ipxe-exit
|
|
3
|
+
#
|
|
4
|
+
# The default for a discovered-but-not-yet-bound machine, and for
|
|
5
|
+
# machines the operator has explicitly told pixie to leave alone.
|
|
6
|
+
# iPXE's ``exit`` unloads itself; the firmware's next boot entry
|
|
7
|
+
# fires -- typically the local disk when the operator has DHCP
|
|
8
|
+
# pointing at pixie only for imaging bring-up.
|
|
9
|
+
|
|
10
|
+
exit
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!ipxe
|
|
2
|
+
# pixie plan for {{ mac }} - boot_mode=nbdboot
|
|
3
|
+
#
|
|
4
|
+
# Image-native nbdboot: the target fetches the image's own kernel +
|
|
5
|
+
# initrd from pixie's artifacts directory (content-addressed to the
|
|
6
|
+
# tar.gz bundle sha) and attaches its root over NBD from the
|
|
7
|
+
# per-machine export that pixie spawns for the disk image blob
|
|
8
|
+
# (content-addressed to the disk-image sha).
|
|
9
|
+
#
|
|
10
|
+
# Kernel cmdline carries ``boot=nbdboot``. Debian live-boot in the
|
|
11
|
+
# initrd dispatches to ``/scripts/<x>`` based on ``boot=<x>``; the
|
|
12
|
+
# script itself comes from a supplementary initramfs overlay pixie
|
|
13
|
+
# serves at ``/pivot/nbdboot.cpio.gz``, loaded via the second
|
|
14
|
+
# ``initrd`` line below. Linux concatenates cpio streams during
|
|
15
|
+
# initramfs unpack + later-overlay-wins on same-path entries, so
|
|
16
|
+
# ``/scripts/nbdboot`` from pixie's overlay shadows any legacy
|
|
17
|
+
# ``/scripts/ramboot`` in nosi's baked initrd -- and nosi is free
|
|
18
|
+
# to publish a generic kernel + initrd (no pixie-flavour pivot
|
|
19
|
+
# script) once it rebuilds without the pixie-media hook.
|
|
20
|
+
#
|
|
21
|
+
# Design departures from the trio's older ipxe_ramboot template:
|
|
22
|
+
#
|
|
23
|
+
# - URLs are content-addressed. A rename of the catalog entry does
|
|
24
|
+
# not silently point the target at different bytes; a redeploy of
|
|
25
|
+
# pixie serves the same URL for the same content.
|
|
26
|
+
# - No fallback to a bty-media-baked kernel. If the netboot bundle
|
|
27
|
+
# is missing or corrupt the target lands on the pixie-media
|
|
28
|
+
# diagnostic screen (a later PR); the plan renderer refuses to
|
|
29
|
+
# render an image-mismatched fallback that would kernel-panic
|
|
30
|
+
# on a bad module.
|
|
31
|
+
#
|
|
32
|
+
# Console + modprobe blacklist encode hardware lessons pulled from
|
|
33
|
+
# bty/nbdmux 2026-06/07 field runs; keep them as-is unless the
|
|
34
|
+
# behaviour is being explicitly re-litigated.
|
|
35
|
+
|
|
36
|
+
set pixie-base http://{{ host }}:{{ port }}
|
|
37
|
+
set nbd-host {{ nbd_host }}
|
|
38
|
+
set nbd-port {{ nbd_port }}
|
|
39
|
+
set nbd-name {{ nbd_name }}
|
|
40
|
+
|
|
41
|
+
# The kernel + initrd come out of the extracted netboot bundle at
|
|
42
|
+
# pixie's content-addressed artifacts path. iPXE fetches them over
|
|
43
|
+
# plain HTTP; no auth on that route.
|
|
44
|
+
#
|
|
45
|
+
# Cmdline carries BOTH ``pixie.*`` and ``bty.*`` prefixes: nosi
|
|
46
|
+
# netboot bundles published before the pixie rename read ``bty.*``
|
|
47
|
+
# in their initrd's nbdboot script, pixie's own netboot-pc bundle
|
|
48
|
+
# reads ``pixie.*``. Emitting both lets a single pixie deploy chain
|
|
49
|
+
# either bundle without a re-bake. Verified live 2026-07-14 on
|
|
50
|
+
# 10.20.30.10 booting nosi debian-13-headless (2026.W28) whose
|
|
51
|
+
# initrd greps for ``bty.server`` / ``bty.mac`` / ``bty.nbd`` /
|
|
52
|
+
# ``bty.image``; a ``pixie.*``-only cmdline made the nbdboot
|
|
53
|
+
# script fall through and never issue nbd-client.
|
|
54
|
+
kernel ${pixie-base}/artifacts/{{ bundle_sha }}/vmlinuz?mac={{ mac }} boot=nbdboot ip=dhcp rd.neednet=1 nbd.nbds_max=1 nbd.max_part=16 systemd.default_device_timeout_sec=10 systemd.default_timeout_start_sec=15 console=tty0 console=ttyS1,115200 console=ttyS0,115200 earlyprintk=ttyS0,115200 plymouth.enable=0 modprobe.blacklist=nouveau,rndis_host nouveau.modeset=0 systemd.gpt_auto=0 pixie.nbd=tcp://${nbd-host}:${nbd-port} pixie.image=${nbd-name} pixie.overlay_size={{ overlay_size }} pixie.server=${pixie-base} pixie.mac={{ mac }} bty.nbd=tcp://${nbd-host}:${nbd-port} bty.image=${nbd-name} bty.overlay_size={{ overlay_size }} bty.server=${pixie-base} bty.mac={{ mac }}{% if extra_cmdline %} {{ extra_cmdline }}{% endif %}
|
|
55
|
+
initrd ${pixie-base}/artifacts/{{ bundle_sha }}/initrd?mac={{ mac }}
|
|
56
|
+
initrd ${pixie-base}/pivot/nbdboot.cpio.gz
|
|
57
|
+
boot
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!ipxe
|
|
2
|
+
# pixie plan for {{ mac }} - boot_mode={{ boot_mode }}
|
|
3
|
+
#
|
|
4
|
+
# Boot pixie's own live env. The netboot-pc bake produced vmlinuz +
|
|
5
|
+
# initrd + squashfs artifacts; the pixie deploy staged them under
|
|
6
|
+
# ``$PIXIE_LIVE_ENV_DIR`` and this HTTP process exposes them at
|
|
7
|
+
# ``/boot/pixie-live-env/``. Debian live-boot cmdline shape:
|
|
8
|
+
#
|
|
9
|
+
# boot=live components fetch=<squashfs URL>
|
|
10
|
+
#
|
|
11
|
+
# handles the initrd -> squashfs pivot. ``pixie.server`` +
|
|
12
|
+
# ``pixie.mac`` are the two knobs pixie-on-tty1.service reads on
|
|
13
|
+
# every live-env boot; the pixie CLI then GETs
|
|
14
|
+
# ``<server>/pxe/<mac>/plan`` and dispatches (auto-flash,
|
|
15
|
+
# inventory + reboot, interactive wizard, or exit).
|
|
16
|
+
#
|
|
17
|
+
# ``ip=dhcp`` mirrors the nbdboot template's cmdline: the initrd's
|
|
18
|
+
# network module (Debian live-boot, in this case) needs an explicit
|
|
19
|
+
# DHCP directive to bring the primary NIC up before pixie CLI tries
|
|
20
|
+
# to reach ``$pixie-server``. ``console=`` chain matches nbdboot too
|
|
21
|
+
# so a target with a serial console reads output on both tty0 and
|
|
22
|
+
# ttyS0/1 -- most bench-side debugging happens on serial.
|
|
23
|
+
|
|
24
|
+
set pixie-base http://{{ host }}:{{ port }}
|
|
25
|
+
|
|
26
|
+
# Cmdline matches bty's proven-working live-env kernel line
|
|
27
|
+
# (bty/src/bty/web/_templates/ipxe_flash.j2 + ipxe_tui.j2). Deltas
|
|
28
|
+
# from an earlier pixie shape that had drifted:
|
|
29
|
+
#
|
|
30
|
+
# - ``ip=dhcp`` REMOVED. Debian live-boot's ``fetch=`` handler
|
|
31
|
+
# configures the network itself via its own DHCP client; keeping
|
|
32
|
+
# both racing produced silent hangs on GIGABYTE MC12-LE0 hardware
|
|
33
|
+
# where live-boot's ipconfig would return with no NIC configured
|
|
34
|
+
# despite iPXE having successfully DHCP'd from the same interface.
|
|
35
|
+
# bty never had it; verified live on 10.20.30.61 (matx-bmc) with
|
|
36
|
+
# nothing else changing.
|
|
37
|
+
# - ``plymouth.enable=0 modprobe.blacklist=nouveau,rndis_host
|
|
38
|
+
# nouveau.modeset=0 systemd.gpt_auto=0`` ADDED. Same set bty
|
|
39
|
+
# ships. plymouth stays off so the operator sees the full boot
|
|
40
|
+
# stream (a wedge between two ``[ OK ] Started X`` lines is
|
|
41
|
+
# immediately diagnostic); nouveau blacklist keeps NVIDIA
|
|
42
|
+
# GPU quirks from stalling non-graphics targets; rndis_host
|
|
43
|
+
# blacklist stops the kernel from claiming a USB-console-emulated
|
|
44
|
+
# NIC before the real one appears; systemd.gpt_auto=0 keeps
|
|
45
|
+
# systemd from auto-mounting random GPT partitions the flasher
|
|
46
|
+
# is about to overwrite.
|
|
47
|
+
#
|
|
48
|
+
# ``console=`` chain matches bty too; LAST-wins for /dev/console.
|
|
49
|
+
# The ttyS0 tail lands on most bench-side USB-serial adapters;
|
|
50
|
+
# operators driving from a BMC's SoL on ttyS1 append their own
|
|
51
|
+
# ``console=ttyS1,115200`` via PIXIE_LIVE_ENV_EXTRA_CMDLINE.
|
|
52
|
+
|
|
53
|
+
kernel ${pixie-base}/boot/pixie-live-env/vmlinuz boot=live fetch=${pixie-base}/boot/pixie-live-env/live.squashfs components console=tty0 console=ttyS1,115200 console=ttyS0,115200 earlyprintk=ttyS0,115200 plymouth.enable=0 modprobe.blacklist=nouveau,rndis_host nouveau.modeset=0 systemd.gpt_auto=0 pixie.server=${pixie-base} pixie.mac={{ mac }} bty.server=${pixie-base} bty.mac={{ mac }}{% if extra_cmdline %} {{ extra_cmdline }}{% endif %}
|
|
54
|
+
initrd ${pixie-base}/boot/pixie-live-env/initrd
|
|
55
|
+
boot
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!ipxe
|
|
2
|
+
# pixie plan for {{ mac }} - {{ reason_slug }}
|
|
3
|
+
#
|
|
4
|
+
# The operator asked for boot_mode=nbdboot but the plan can't be
|
|
5
|
+
# rendered right now. Reason:
|
|
6
|
+
#
|
|
7
|
+
# {{ reason }}
|
|
8
|
+
#
|
|
9
|
+
# We deliberately do NOT chain-load anything else here (no bty-media
|
|
10
|
+
# fallback, no generic "keep trying" loop). ``exit`` unloads iPXE so
|
|
11
|
+
# the firmware's next boot device fires; the operator sees the reason
|
|
12
|
+
# in ``/ui/machines/<mac>`` and on pixie's event log.
|
|
13
|
+
|
|
14
|
+
exit
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{% block title %}pixie{% endblock %}</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="/static/pixie-favicon.png">
|
|
8
|
+
<link rel="stylesheet" href="/static/bootstrap.min.css">
|
|
9
|
+
<link rel="stylesheet" href="/static/bootstrap-icons.min.css">
|
|
10
|
+
<style>
|
|
11
|
+
/* Palette anchor shared with bty / withcache / nbdmux (the three
|
|
12
|
+
services that pixie folds into one process). Bootstrap 5
|
|
13
|
+
exposes ``--bs-primary`` + a matching -rgb triplet used by
|
|
14
|
+
translucent variants (alerts, focus rings, .bg-*-subtle).
|
|
15
|
+
Overriding both here re-tints every stock component without
|
|
16
|
+
patching bootstrap.min.css. Navy stays as the primary tint
|
|
17
|
+
because pixie inherits bty's operator surface -- operators
|
|
18
|
+
coming from bty read the same colour as "the appliance
|
|
19
|
+
surface", not "a new product". */
|
|
20
|
+
:root {
|
|
21
|
+
--bs-primary: #0d6efd;
|
|
22
|
+
--bs-primary-rgb: 13, 110, 253;
|
|
23
|
+
--bs-link-color: #0d6efd;
|
|
24
|
+
--bs-link-hover-color: #0a58ca;
|
|
25
|
+
}
|
|
26
|
+
.btn-primary {
|
|
27
|
+
--bs-btn-bg: #0d6efd;
|
|
28
|
+
--bs-btn-border-color: #0d6efd;
|
|
29
|
+
--bs-btn-hover-bg: #0a58ca;
|
|
30
|
+
--bs-btn-hover-border-color: #0a58ca;
|
|
31
|
+
--bs-btn-active-bg: #0a58ca;
|
|
32
|
+
--bs-btn-active-border-color: #0a58ca;
|
|
33
|
+
}
|
|
34
|
+
.bg-primary { --bs-bg-opacity: 1; background-color: #0d6efd !important; }
|
|
35
|
+
.text-primary { --bs-text-opacity: 1; color: #0d6efd !important; }
|
|
36
|
+
.border-primary { --bs-border-opacity: 1; border-color: #0d6efd !important; }
|
|
37
|
+
/* Brand strip: a thin gradient accent above the navbar.
|
|
38
|
+
Same navy -> dark-magenta -> magenta gradient as bty /
|
|
39
|
+
withcache / nbdmux so pixie reads as the fourth
|
|
40
|
+
surface in the same product family; when the trio is
|
|
41
|
+
folded into pixie the family strip stays as their
|
|
42
|
+
shared visual signature. */
|
|
43
|
+
.brand-accent {
|
|
44
|
+
height: 3px;
|
|
45
|
+
background: linear-gradient(90deg, #0d6efd 0%, #6610f2 50%, #d63384 100%);
|
|
46
|
+
}
|
|
47
|
+
.sticky-header {
|
|
48
|
+
position: sticky;
|
|
49
|
+
top: 0;
|
|
50
|
+
z-index: 1030;
|
|
51
|
+
}
|
|
52
|
+
html {
|
|
53
|
+
scroll-padding-top: 6.5rem;
|
|
54
|
+
scroll-behavior: smooth;
|
|
55
|
+
}
|
|
56
|
+
.navbar-brand {
|
|
57
|
+
border-radius: 0.5rem;
|
|
58
|
+
padding-left: 0.6rem;
|
|
59
|
+
padding-right: 0.6rem;
|
|
60
|
+
margin-right: 0.25rem;
|
|
61
|
+
transition: background-color 0.15s;
|
|
62
|
+
}
|
|
63
|
+
.navbar-brand.brand-active {
|
|
64
|
+
background-color: rgba(13, 110, 253, 0.85);
|
|
65
|
+
}
|
|
66
|
+
.navbar-brand:hover {
|
|
67
|
+
background-color: rgba(255, 255, 255, 0.06);
|
|
68
|
+
}
|
|
69
|
+
.navbar-brand.brand-active:hover {
|
|
70
|
+
background-color: rgba(13, 110, 253, 0.95);
|
|
71
|
+
}
|
|
72
|
+
.navbar-version {
|
|
73
|
+
color: rgba(255, 255, 255, 0.55);
|
|
74
|
+
font-weight: 400;
|
|
75
|
+
font-size: 0.85rem;
|
|
76
|
+
align-self: center;
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
}
|
|
79
|
+
.navbar-brand .brand-icon {
|
|
80
|
+
height: 1.05rem;
|
|
81
|
+
width: auto;
|
|
82
|
+
vertical-align: -0.15rem;
|
|
83
|
+
}
|
|
84
|
+
.navbar .nav-btn {
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.4rem;
|
|
88
|
+
padding: 0.4rem 0.8rem;
|
|
89
|
+
margin-right: 0.25rem;
|
|
90
|
+
border-radius: 0.5rem;
|
|
91
|
+
color: rgba(255, 255, 255, 0.85);
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
transition: background-color 0.15s;
|
|
94
|
+
}
|
|
95
|
+
.navbar .nav-btn:hover {
|
|
96
|
+
background-color: rgba(255, 255, 255, 0.10);
|
|
97
|
+
color: #fff;
|
|
98
|
+
}
|
|
99
|
+
.navbar .nav-btn.active {
|
|
100
|
+
background-color: #0d6efd;
|
|
101
|
+
color: #fff;
|
|
102
|
+
box-shadow: 0 0 0 1px rgba(13, 110, 253, 0.6);
|
|
103
|
+
}
|
|
104
|
+
.navbar .nav-btn i {
|
|
105
|
+
font-size: 1.05rem;
|
|
106
|
+
}
|
|
107
|
+
/* User-bar: a single pill; pixie has no per-operator account
|
|
108
|
+
surface yet so this is just a logout button styled to match
|
|
109
|
+
bty's user-bar. Extend later when accounts land. */
|
|
110
|
+
.user-bar {
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
align-items: stretch;
|
|
113
|
+
border-radius: 999px;
|
|
114
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
115
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
font-size: 0.85rem;
|
|
118
|
+
}
|
|
119
|
+
.user-bar-action {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
padding: 0.35rem 0.7rem;
|
|
123
|
+
background: transparent;
|
|
124
|
+
border: none;
|
|
125
|
+
color: rgba(255, 255, 255, 0.85);
|
|
126
|
+
text-decoration: none;
|
|
127
|
+
transition: background-color 0.15s, color 0.15s;
|
|
128
|
+
}
|
|
129
|
+
.user-bar-action:hover,
|
|
130
|
+
.user-bar-action:focus {
|
|
131
|
+
background-color: rgba(255, 255, 255, 0.10);
|
|
132
|
+
color: #fff;
|
|
133
|
+
outline: none;
|
|
134
|
+
}
|
|
135
|
+
.user-bar-logout:hover,
|
|
136
|
+
.user-bar-logout:focus {
|
|
137
|
+
background-color: rgba(220, 53, 69, 0.65);
|
|
138
|
+
color: #fff;
|
|
139
|
+
}
|
|
140
|
+
/* Sub-nav strip. Same layout contract as bty: pages with
|
|
141
|
+
per-section actions render their pill nav here so it
|
|
142
|
+
visually attaches to the main navbar above. */
|
|
143
|
+
.subnav-strip {
|
|
144
|
+
background-color: #495057;
|
|
145
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
|
|
146
|
+
padding-top: 0.25rem;
|
|
147
|
+
padding-bottom: 0.25rem;
|
|
148
|
+
font-size: 0.85rem;
|
|
149
|
+
line-height: 1.4;
|
|
150
|
+
}
|
|
151
|
+
.subnav-strip > .container {
|
|
152
|
+
min-height: 2rem;
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 0.6rem;
|
|
156
|
+
}
|
|
157
|
+
.subnav-strip .form-control-sm,
|
|
158
|
+
.subnav-strip .btn-sm,
|
|
159
|
+
.subnav-strip a,
|
|
160
|
+
.subnav-strip label,
|
|
161
|
+
.subnav-strip .small,
|
|
162
|
+
.subnav-strip small {
|
|
163
|
+
font-size: inherit;
|
|
164
|
+
line-height: inherit;
|
|
165
|
+
}
|
|
166
|
+
.subnav-strip .form-control-sm,
|
|
167
|
+
.subnav-strip .btn-sm {
|
|
168
|
+
padding-top: 0.15rem;
|
|
169
|
+
padding-bottom: 0.15rem;
|
|
170
|
+
}
|
|
171
|
+
.subnav-strip .nav-pills {
|
|
172
|
+
gap: 0.25rem;
|
|
173
|
+
}
|
|
174
|
+
.subnav-strip .nav-pills .nav-link {
|
|
175
|
+
color: rgba(255, 255, 255, 0.78);
|
|
176
|
+
padding: 0.15rem 0.55rem;
|
|
177
|
+
}
|
|
178
|
+
.subnav-strip .nav-pills .nav-link:hover {
|
|
179
|
+
color: #fff;
|
|
180
|
+
background-color: rgba(255, 255, 255, 0.10);
|
|
181
|
+
}
|
|
182
|
+
.subnav-strip .nav-pills .nav-link.active {
|
|
183
|
+
color: #fff;
|
|
184
|
+
background-color: #0d6efd;
|
|
185
|
+
}
|
|
186
|
+
.subnav-strip .text-muted {
|
|
187
|
+
color: rgba(255, 255, 255, 0.55) !important;
|
|
188
|
+
}
|
|
189
|
+
.subnav-strip code {
|
|
190
|
+
color: rgba(255, 255, 255, 0.85);
|
|
191
|
+
background-color: transparent;
|
|
192
|
+
}
|
|
193
|
+
.subnav-strip a {
|
|
194
|
+
color: rgba(255, 255, 255, 0.78);
|
|
195
|
+
}
|
|
196
|
+
.subnav-strip a:hover {
|
|
197
|
+
color: #fff;
|
|
198
|
+
}
|
|
199
|
+
/* Jump links (subnav-jumps) sit next to each other with a
|
|
200
|
+
visible gap so an operator scanning the strip sees them as
|
|
201
|
+
distinct anchors instead of one run-together phrase. The
|
|
202
|
+
parent flex sets gap=0.6rem for TOP-LEVEL children of the
|
|
203
|
+
strip, but the anchors nest inside a single .subnav-jumps
|
|
204
|
+
span; give that span its own gap. */
|
|
205
|
+
.subnav-strip .subnav-jumps {
|
|
206
|
+
gap: 1.1rem;
|
|
207
|
+
}
|
|
208
|
+
.subnav-strip .subnav-jumps > a {
|
|
209
|
+
padding: 0.1rem 0.15rem;
|
|
210
|
+
border-radius: 0.2rem;
|
|
211
|
+
}
|
|
212
|
+
.subnav-strip .subnav-jumps > a:hover {
|
|
213
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
214
|
+
}
|
|
215
|
+
/* Surface shading ladder: card-header > thead > table body, each
|
|
216
|
+
step lighter going down. Same three-tone recipe bty uses so a
|
|
217
|
+
pixie machines / catalog table reads with the same depth cues
|
|
218
|
+
an operator learned on bty. */
|
|
219
|
+
.table {
|
|
220
|
+
--bs-table-bg: #eff2f6;
|
|
221
|
+
}
|
|
222
|
+
.table > thead {
|
|
223
|
+
--bs-table-bg: #e1e7ef;
|
|
224
|
+
}
|
|
225
|
+
.card > .card-header.bg-light {
|
|
226
|
+
background-color: #d2dbe6 !important;
|
|
227
|
+
}
|
|
228
|
+
/* Bootstrap's default .alert-warning is very-pale-yellow with a
|
|
229
|
+
faint amber text -- readable on print but low-contrast on
|
|
230
|
+
screen, especially for the small-text hints pixie uses on the
|
|
231
|
+
bind form + inventory "no data yet" callouts. Darken the text
|
|
232
|
+
and firm up the border so warnings actually catch the eye. */
|
|
233
|
+
.alert-warning {
|
|
234
|
+
color: #664200;
|
|
235
|
+
background-color: #fff3cd;
|
|
236
|
+
border-color: #ffd35a;
|
|
237
|
+
}
|
|
238
|
+
.alert-warning code {
|
|
239
|
+
color: #664200;
|
|
240
|
+
}
|
|
241
|
+
.alert-warning a {
|
|
242
|
+
color: #664200;
|
|
243
|
+
text-decoration: underline;
|
|
244
|
+
}
|
|
245
|
+
/* Page description strip: a one-paragraph orientation shown
|
|
246
|
+
at the top of every operator page (dashboard, catalog,
|
|
247
|
+
machines, events, settings, machine detail). Left accent
|
|
248
|
+
border marks it as "meta" content, distinct from the cards
|
|
249
|
+
below. Font size is smaller than the card body prose so a
|
|
250
|
+
returning operator can skim past it. */
|
|
251
|
+
.page-description {
|
|
252
|
+
padding: 0.5rem 0.85rem;
|
|
253
|
+
border-left: 3px solid var(--bs-primary);
|
|
254
|
+
background-color: rgba(13, 110, 253, 0.04);
|
|
255
|
+
border-radius: 0 0.2rem 0.2rem 0;
|
|
256
|
+
font-size: 0.9rem;
|
|
257
|
+
line-height: 1.4;
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
260
|
+
<script src="/static/htmx.min.js"></script>
|
|
261
|
+
<script src="/static/sse.js"></script>
|
|
262
|
+
</head>
|
|
263
|
+
<body class="bg-light">
|
|
264
|
+
{# Sticky header: the accent strip, the main navbar, and the sub-nav
|
|
265
|
+
travel together and pin to the top so per-section nav stays
|
|
266
|
+
reachable while scrolling a long page. #}
|
|
267
|
+
<div class="sticky-header">
|
|
268
|
+
<div class="brand-accent"></div>
|
|
269
|
+
<nav class="navbar navbar-expand-md bg-dark navbar-dark py-2">
|
|
270
|
+
<div class="container">
|
|
271
|
+
{# Brand pill doubles as the dashboard link. The active state
|
|
272
|
+
on ``page == 'dashboard'`` is the "you are on the dashboard"
|
|
273
|
+
cue; a separate ``Dashboard`` nav-btn would just duplicate
|
|
274
|
+
the brand's click target. #}
|
|
275
|
+
<a class="navbar-brand fw-semibold {% if page == 'dashboard' %}brand-active{% endif %}"
|
|
276
|
+
href="/ui/">
|
|
277
|
+
<img src="/static/pixie-favicon.png" alt="pixie mascot" class="brand-icon me-1">PIXIE
|
|
278
|
+
</a>
|
|
279
|
+
{% if authed %}
|
|
280
|
+
<div class="d-flex flex-grow-1 align-items-center flex-wrap">
|
|
281
|
+
<div class="me-auto d-flex flex-wrap">
|
|
282
|
+
<a class="nav-btn {% if page == 'machines' %}active{% endif %}" href="/ui/machines">
|
|
283
|
+
<i class="bi bi-pc-display"></i>Machines
|
|
284
|
+
</a>
|
|
285
|
+
<a class="nav-btn {% if page == 'catalog' %}active{% endif %}" href="/ui/catalog">
|
|
286
|
+
<i class="bi bi-collection"></i>Catalog
|
|
287
|
+
</a>
|
|
288
|
+
<a class="nav-btn {% if page == 'events' %}active{% endif %}" href="/ui/events">
|
|
289
|
+
<i class="bi bi-clock-history"></i>Events
|
|
290
|
+
</a>
|
|
291
|
+
<a class="nav-btn {% if page == 'settings' %}active{% endif %}" href="/ui/settings">
|
|
292
|
+
<i class="bi bi-gear"></i>Settings
|
|
293
|
+
</a>
|
|
294
|
+
</div>
|
|
295
|
+
<span class="navbar-version me-2">v{{ version }}</span>
|
|
296
|
+
<div class="user-bar mt-2 mt-md-0"
|
|
297
|
+
title="Session (session cookie, one operator).">
|
|
298
|
+
<span class="user-bar-action">
|
|
299
|
+
<i class="bi bi-person-circle me-1"></i>admin
|
|
300
|
+
</span>
|
|
301
|
+
<form action="/ui/logout" method="post" class="m-0 d-inline-flex">
|
|
302
|
+
<button type="submit" class="user-bar-action user-bar-logout"
|
|
303
|
+
title="Sign out">
|
|
304
|
+
<i class="bi bi-box-arrow-right"></i>
|
|
305
|
+
</button>
|
|
306
|
+
</form>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
{% endif %}
|
|
310
|
+
</div>
|
|
311
|
+
</nav>
|
|
312
|
+
|
|
313
|
+
{# Sub-nav strip. Pages with per-section actions render their pill
|
|
314
|
+
nav in the ``subnav`` block; pages without leave it empty and
|
|
315
|
+
the main content flows straight up to the main navbar. #}
|
|
316
|
+
{% block subnav %}{% endblock %}
|
|
317
|
+
</div>{# /.sticky-header #}
|
|
318
|
+
|
|
319
|
+
<main class="container py-4">
|
|
320
|
+
{% if flash %}
|
|
321
|
+
<div class="alert alert-{{ flash_kind or 'info' }}">{{ flash }}</div>
|
|
322
|
+
{% endif %}
|
|
323
|
+
{% block intro %}{% endblock %}
|
|
324
|
+
{% block content %}{% endblock %}
|
|
325
|
+
</main>
|
|
326
|
+
|
|
327
|
+
<script>
|
|
328
|
+
// Keep ``scroll-padding-top`` in lockstep with the sticky header's
|
|
329
|
+
// actual height so in-page ``#anchor`` jumps land BELOW the header
|
|
330
|
+
// rather than under it. Same pattern bty uses; measure on load and
|
|
331
|
+
// on resize so window shrinks that wrap the subnav onto two lines
|
|
332
|
+
// also stay correct.
|
|
333
|
+
(function () {
|
|
334
|
+
function syncScrollPadding() {
|
|
335
|
+
var header = document.querySelector(".sticky-header");
|
|
336
|
+
if (!header) return;
|
|
337
|
+
document.documentElement.style.scrollPaddingTop = (header.offsetHeight + 6) + "px";
|
|
338
|
+
}
|
|
339
|
+
syncScrollPadding();
|
|
340
|
+
window.addEventListener("resize", syncScrollPadding);
|
|
341
|
+
document.body.addEventListener("htmx:afterSwap", syncScrollPadding);
|
|
342
|
+
})();
|
|
343
|
+
</script>
|
|
344
|
+
</body>
|
|
345
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
{% block title %}Log in - pixie{% endblock %}
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="row justify-content-center">
|
|
5
|
+
<div class="col-md-6">
|
|
6
|
+
<div class="card">
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<h1 class="h4 mb-3"><i class="bi bi-shield-lock me-2"></i>Log in</h1>
|
|
9
|
+
<p class="text-muted small">
|
|
10
|
+
Enter the admin password. The default is
|
|
11
|
+
<code>pixie</code>; override via
|
|
12
|
+
<code>PIXIE_ADMIN_PASSWORD</code> on the server.
|
|
13
|
+
</p>
|
|
14
|
+
{% if using_default_password %}
|
|
15
|
+
<div class="alert alert-warning small mb-3">
|
|
16
|
+
<i class="bi bi-exclamation-triangle me-1"></i>
|
|
17
|
+
This server still uses the well-known default
|
|
18
|
+
password <code>pixie</code>. Set
|
|
19
|
+
<code>PIXIE_ADMIN_PASSWORD</code> on the host before
|
|
20
|
+
exposing it beyond localhost.
|
|
21
|
+
</div>
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% if error %}
|
|
24
|
+
<div class="alert alert-danger">{{ error }}</div>
|
|
25
|
+
{% endif %}
|
|
26
|
+
<form action="/ui/login" method="post">
|
|
27
|
+
<div class="mb-3">
|
|
28
|
+
<label for="password" class="form-label">Password</label>
|
|
29
|
+
<input type="password" class="form-control" id="password"
|
|
30
|
+
name="password" autocomplete="current-password" autofocus required>
|
|
31
|
+
</div>
|
|
32
|
+
<button type="submit" class="btn btn-primary w-100">
|
|
33
|
+
<i class="bi bi-box-arrow-in-right me-1"></i>Log in
|
|
34
|
+
</button>
|
|
35
|
+
</form>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
{% endblock %}
|