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 themeplesty.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 run act on and File Save working fleet as… writes. It is persisted as the declaration’s autostart, 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 exited record), Field Test only when the installed module is a checkout with a tests/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 .env file — <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.

  • .env seeding — on the first install of an instance its .env is 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-upsDeclareDialog, 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 a Worker (a small thread pool). Every menu action is one presenter call on the worker; done and failed arrive as Qt signals on the GUI thread, which repaints or alerts. Nothing in view/ imports a service or touches a process.

  • Dialogs turn form fields into model objects (to_spec(), to_run()) and report a FleetError inline 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 a JobDialog on 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.