PUDA Manual UI
Build Streamlit operator UIs that drive machines through the PUDA CLI. The CLI remains the control plane; the UI only shells out to puda.
For Streamlit layout, widgets, and styling, also follow the developing-with-streamlit skill.
Required session controls
Every manual-control UI must include these three buttons:
| Button | Role |
|---|---|
| Start | Open a manual run on all relevant machines |
| Complete | Close the manual run on all relevant machines |
| Reset | Reset a machine via CLI |
UI state machine
- Idle (default): Enable Start and Reset. Disable all machine actions and Complete.
- Start must run from the PUDA project root:
Setpuda machine start <machine_ids> --run-id manualst.session_state.started = Trueonly if it succeeds. Then enable machine actions and Complete. - Complete must run the same machine set and run ID:
Return to idle only on success.puda machine complete <machine_ids> --run-id manual - Reset remains visible in every state and runs:
Return to idle after a successful reset.puda machine reset <machine_id>
Do not send machine action commands until Start has succeeded for the current session.
CLI backend
Always invoke puda from the project root (where puda.config / project.md live). Capture stdout/stderr and show failures in the UI.
If puda machine start or puda machine complete is missing / unknown, tell the user to update the PUDA CLI to version >= 0.0.36 (e.g. puda version, then puda update).
<machine_ids> may be comma-separated (same pattern as puda machine reset biologic,first). Start and Complete must hit the same set for a session. Resolve targets from project memory, user selection, or workflow skill; discover with puda machine list when needed.
Dispatch individual actions
Use the maintained CLI path for every action button:
puda machine run <machine_id> <command_name> --params '<json-object>' --run-id manual
Implement with subprocess.run(..., cwd=<PUDA project root>, capture_output=True, text=True, timeout=<per-command timeout>).
puda machine runqueues exactly one command and does not send START or COMPLETE; invoke it only after manual Start succeeds.- Always pass
--run-id manualso actions belong to the open manual session. - Serialize parameters as a JSON object with
--params; use--kwargs '<json-object>'only when a command requires keyword arguments. - Record the invoked CLI command, exit code, stdout, and stderr in the operator-facing command log.
- Surface failures without changing session state.
Prohibited legacy dispatch paths
Do not generate temporary protocol JSON/YAML files, invoke puda protocol run, or use a custom NATS-publishing helper for dashboard action buttons. Those paths duplicate maintained CLI dispatch, can add unintended lifecycle semantics, and leave stale protocol artifacts.
Implementation rules
- Backend = subprocess/
pudaonly. Do not open a parallel NATS client from the UI. - Keep
run_idhard-coded to"manual"for Start, Complete, and action dispatch. - Gate every non-Start/non-Reset control on session state (
started == True). - Surface CLI errors; do not flip UI state to “started” or “idle” if the corresponding Start/Complete call failed.
- Prefer a small helper module (e.g.
puda_cli.py) that wraps start / complete / reset / run / list so buttons stay thin.
Command log and fragment behavior
Keep the command log in st.session_state and show the invoked CLI command, exit code, stdout, and stderr after every action. If action controls run inside @st.fragment, render the command log inside that same fragment: clicking a fragment button reruns the fragment only, so a page-level log renderer will remain stale. Expand the log automatically after a command result is recorded.
Layout and livestream
Prefer a single-screen layout: livestream and all controls visible together. Do not hide core controls in tabs unless explicitly requested.
- Put Start / Complete / Reset at the top.
- Put compact machine controls on the left and the camera on the right when screen width allows.
When Tailnet MagicDNS is enabled, use the machine's MagicDNS name for cross-machine/player URLs rather than a changing Tailnet IP. Discover it with tailscale status --json (Self.DNSName locally, or the target peer's DNSName) and remove its trailing dot.
Use the short MagicDNS hostname when it resolves for the operator, otherwise the full MagicDNS name:
http://<machine-hostname>:<player-port>/<path>
http://<machine-hostname>.<tailnet>.ts.net:<player-port>/<path>
Embed the player URL in st.iframe(...) and optionally link its API endpoint. Verify the selected player URL returns HTTP 200 before sharing or embedding. A Tailnet IP is a fallback only when neither MagicDNS form is available.
Dashboard URL
For a deployed manual-control dashboard, share its reachable MagicDNS hostname and Streamlit port:
http://<machine-hostname>:<streamlit-port>/
Verify http://<machine-hostname>:<streamlit-port>/_stcore/health returns HTTP 200 before reporting it. Example only: the First dashboard currently uses http://first:8501/; do not hard-code First or port 8501 for another machine.
Reference implementation
See references/first-manual-app.py for a verified First/Qubot single-screen manual-jogging dashboard. It demonstrates the lifecycle state machine, direct puda machine run dispatch, semantic Complete/Reset buttons, in-fragment command logging, and the livestream layout. Copy and adapt it for another dashboard rather than importing it as a runtime dependency.
Scaffold checklist
When creating a new manual-control app:
- Streamlit entrypoint (e.g.
app.py/streamlit_app.py) - Session state: idle vs started; list of relevant
machine_ids - Start, Complete, Reset always present
- Idle: enable Start and Reset; disable machine actions and Complete
- Start →
puda machine start … --run-id manual - Complete →
puda machine complete … --run-id manual - Reset →
puda machine reset <machine_id> - Actions →
puda machine run … --run-id manual(no temp protocols / direct NATS) - Machine action buttons only enabled after Start
- Command log inside the same
@st.fragmentas action buttons (if fragments are used) - Single-screen layout with livestream when a camera is available
Verification
Before reporting a dashboard update as complete:
- Run the dashboard virtualenv's
python -m py_compile app.py. - Confirm the source contains the direct
machine runpath and no references to temp protocol creation,puda protocol run, or custom direct-NATS dispatch. - Confirm Streamlit is running.
- Check
http://127.0.0.1:<port>/_stcore/healthreturns HTTP 200. - Only provide a Tailnet URL after its health endpoint also succeeds.