# 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`](remote.md), 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 ```text ┌ 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 — `/run//.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-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 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.