The console (GUI)
plesty-server gui opens the Qt console on the bench — the same Bench
the command line drives, with a window on it. It is deliberately local:
from another machine the bench is driven with plesty-bench,
which needs no GUI. It needs the gui extra
(uv tool install "plesty-server[gui]"), which brings PySide6 through
plesty-lib[gui].
What is plesty-lib’s, what is ours
The console takes plesty-lib’s theme — plesty.lib.ui.theme.Theme: the
plesty.net palette (palette.json), the fonts and the widget chrome
vocabulary — so the lab GUI, the website and the reports stay one visual
system. It does not take the docking Shell/Panel layout: a bench
console is not a monitor wall. The layout is designed here from scratch and
lives in one stylesheet template, plesty/server/assets/console.qss, whose
{{token}} placeholders are filled from the lib palette at start-up
(view.app.stylesheet()). No colour is written in Python; no CSS is written
in Python either.
Layout
┌ File Edit Catalogue Device View Help ─────────────────────────────────────────────┐
│ ┌ Devices ──────────────── [Declare…] [Fleet up] [Fleet down] [Refresh] ┐ │
│ │ ☑ NAME SOURCE STATE PORT PID STARTED│ │
│ │ ☑ pm100d plesty-pm100d==0.2.1 running 5551 4242 10:21 │ │
│ │ ☐ k10cr1 plesty-k10cr1==0.3.0.dev4 stopped 5552 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ┌ pm100d ──── [Start] [Stop] [Restart] [Install] [Update] [Uninstall] [Field test…] ┐ │
│ │ [Log] [Jobs] [Reports] │ │
│ │ === plesty-server start … :: …/bin/python -m plesty.pm100d … │ │
│ │ [SERVER] Running at 127.0.0.1:5551 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ ~/.plesty/server · fleet.yaml · 2 device(s) · refreshed 10:21:05 │
└───────────────────────────────────────────────────────────────────────┘
The working fleet — the first column is a check box: checked devices are the working fleet, the set
Fleet up/Fleet down/fleet runact on andFile ▸ Save working fleet as…writes. It is persisted as the declaration’sautostart, so the headless bench and the console mean the same thing by it; a fleet may hold many declared devices and a working fleet of one.Buttons — the Devices card header carries the fleet actions (Declare…, Fleet up, Fleet down, Refresh); the detail card header carries the device actions for the selected row (Start · Stop · Restart · Install · Edit… · Update · Field Test · Uninstall · Remove, on their own row), enabled by state: Start only when the server is down, Stop only when it is up (or to clear an
exitedrecord), Field Test only when the installed module is a checkout with atests/field_test.py— hovering the disabled button says why.Menu bar
File — New fleet… (an empty fleet file, made current), Open fleet… (replace the declarations with another fleet file; the home stays), Save working fleet as… (the checked devices to a new, portable fleet file), Quit.
Edit — Device config… (the highlighted device: its declaration and its
.envfile —<home>/run/<name>/.env, read by the server and folded into field-test runs), Fleet file… (the whole fleet as YAML, validated before it is written), Add device… (an instance of an installed module — the package is a drop-down of what the bench has; with nothing installed the user is sent to the catalogue), Remove from fleet.Catalogue — Search & install… (the hub catalogue’s quantum device modules by name/vendor with their check and field-test status, and what this bench already holds of each). It is a market place: Install module stocks the bench and declares nothing — the fleet is not touched and none is needed. The source is preselected from what the module offers: its PyPI release when it has one (no git involved), else its repository at the latest tag.
Device — Start, Stop, Restart · Install, Update, Uninstall · Field test… · Fleet up, Fleet down — also the table’s context menu.
View (Refresh, Jobs), Help (About).
Main view — the Devices card over the detail card for the selected device with three tabs: the server Log tail, its Jobs (double-click opens the job), its published field-test Reports. Refreshes every 3 s.
The declaration form (Add device / Edit) — the source is one of Release (PyPI) with a version drop-down or Repository (git) with URL and ref drop-downs; the other is disabled. The drop-downs are filled from what the bench knows (
Bench.source_options: installed versions and checkouts, the catalogue once fetched,main/exp) and stay editable..envseeding — on the first install of an instance its.envis a verbatim copy of the module’s.env.example(a checkout’s own; for a release install it is fetched from the module’s repository via the catalogue, the generic one shipped with plesty-server when offline). A later install of another version/ref appends the keys the new example has and the file lacks, never overwriting values.Failures — reported in the window’s status bar with a Dismiss button, not in a modal. What fails here fails asynchronously (an install runs for minutes, and the operator is often elsewhere), and a modal blocks Qt’s event loop: the table stops refreshing, an open job log stops tailing, and the console is frozen until somebody clicks it away. The message stays until it is dismissed or the next action succeeds. A job’s own log dialog is a different thing and opens as before — it is what the operator asked for.
Pop-ups —
DeclareDialog,DeviceConfigDialog,FleetFileDialog,CatalogueDialog,FieldTestDialog,JobDialog, and the confirm/alert/about message boxes.
Closing the console
Closing the window stops what it manages — every running server and job —
after a confirmation that lists them, so nothing is left unmanaged behind a
window that is gone (and an accidental close cannot take a bench down
silently). A bench that must keep its servers up without a window runs
plesty-server fleet run instead.
How the view stays a view
The window owns a
Bench(presenter) and aWorker(a small thread pool). Every menu action is one presenter call on the worker;doneandfailedarrive as Qt signals on the GUI thread, which repaints or alerts. Nothing inview/imports a service or touches a process.Dialogs turn form fields into model objects (
to_spec(),to_run()) and report aFleetErrorinline instead of closing — the presenter gets a valid object or nothing.Long work (install, update, field test) comes back as a
Job; the window opens aJobDialogon it and refreshes when it ends. Closing the dialog does not stop the job — it is detached, like everything else.
Testing
tests/test_view.py runs the whole thing offscreen
(QT_QPA_PLATFORM=offscreen): builds the window on a bench with the fake
server, starts and stops a device through the menu actions, checks the
table, the log tab, the jobs and reports tabs, the dialogs’ conversions and
the job dialog following a real job. Skipped when PySide6 is absent.