Operations Mode#
What Is It?#
Everything up to and including the Setup Wizard is a one-time experience — you run it once, bare metal to live platform, and never again. Operations mode is what comes after: the dashboard you open every day to see what’s running, add new apps , watch convergence runs, and check whether anything is on fire.
The wizard was the installer. Operations mode is the control panel.
Where Do You Find It?#
Two doors to the same room:
# In your browser, after bootstrap:
https://dashboard.<your-domain>
# From your laptop, using the CLI:
psw dashboard --project ~/homelabBoth open the exact same UI — it’s the same code (psw_dashboard) either way. The browser URL talks to the dashboard running on your core target
, protected by Authelia
SSO
. The psw dashboard CLI launches a local copy against your project folder on disk — handy when you’re offline, scripting, or want to poke at a project without a browser.
psw dashboardis post-bootstrap only. It refuses to open a project that hasn’t been bootstrapped and prints “For pre-bootstrap setup, usepsw wizard.” The two CLIs are strictly separated: wizard for installing, dashboard for operating.
Logging In#
The deployed dashboard sits behind Authelia’s forward-auth proxy (sso_type: proxy in its meta.yml) — so before you see anything, Authelia asks for your SSO username and password. The default admin credentials are created during bootstrap
and shown on the wizard’s Launch completion screen (write them down!). All PSW apps that opt into SSO share this same login — one set of credentials for the whole platform.
When you run psw dashboard locally there’s no Authelia in front of it (it’s binding to 127.0.0.1 by default), so no login prompt — the local instance is implicitly trusted.
What’s on Each Page?#
The dashboard has seven main pages plus a handful of partial endpoints that power live updates. Every one is a view; the dashboard never SSHes into a server or runs psw commands on your behalf — it reads from the project files and a .psw/events/ directory that convergence
writes to as it works.
| Page | URL | What it shows |
|---|---|---|
| Overview | / | The home. Sync status (desired commit vs. deployed commit), agent status (is convergence enabled? when did it last run?), run stats (success/failed counts), and a list of your hosts, targets, and apps |
| Runs | /runs | Every convergence run in reverse chronological order. Filter by type or result, paginate through history |
| Run detail | /runs/<id> | One specific run: every stage, every task event, duration, result. Live-updating while it’s in flight |
| Apps | /apps | Everything you’ve deployed: name, target, sync status, convergence health. Feature-owned apps (pangolin + newt
) hidden by default; add ?all=1 to show them |
| Add apps | /apps/add | Browse the app catalog with categories and search, pick apps, assign targets, submit |
| Targets | /targets | Every target
in network.yml with its IP, node, and which apps are on it |
| Hosts | /hosts + /hosts/<name> | Every Proxmox node . The drilldown shows what targets live on it and their state |
| History | /history | The git log of your user project — every commit you (or convergence) ever pushed |
| Remote Access | /remote | Remote-access controls: which apps are exposed, VPS health, rotate certs or the VPS itself |
Live Updates: How It Stays Current#
The dashboard uses Server-Sent Events
over the /sse/events endpoint — the same pattern the wizard uses, but for a different kind of stream:
- When a convergence
run starts anywhere (timer, git-push webhook, or manual button), a
run_startedevent fires and the Overview page lights up - While it’s running, every task event (deploy this app, run that reconciler, sync that aggregator) streams in real time as a
task_event— you watch the execution plan unfold line-by-line - When it finishes, a
run_completedevent carries the result and duration - A
heartbeatevery 15 seconds keeps the connection alive
No polling the UI, no manual refresh. The events come from a .psw/events/ directory of JSONL files that convergence appends to as it works — the dashboard watches it with a 0.5-second poll internally and fans the events out to every connected browser.
What You Can Do (Not Just See)#
The dashboard is mostly read-only by design — the source of truth is your git repo , and changes flow through commits and convergence , not through button clicks. But a handful of actions are exposed as buttons because they don’t bypass that model:
| Action | What it does |
|---|---|
Add an app (/apps/add) | Writes a new services/<app>/service.yml, commits it, pushes. Convergence deploys it on the next tick |
| Enable / disable the convergence agent | Flips the systemd timer on the core target. You’d disable it while debugging or during maintenance windows |
| Trigger a manual run | Fires a convergence run right now instead of waiting for the 5-minute timer |
| Expose / unexpose an app remotely | Toggles a Pangolin resource — see remote access |
| Setup Pangolin on a target | One-click remote-access bootstrap for the VPS-side |
| Rotate certs / rotate VPS | Destructive remote-access operations, guarded by a confirm — see remote access |
Things you don’t do in the dashboard:
- Remove an app — still a CLI action (
psw app remove <name>), since it’s a destructive git change you want to review in a diff before pushing - Edit
network.yml/ add a target — same reasoning, edit the file and commit - Reconfigure providers — done once in the wizard; if you really need to change it, use
psw provider configure - Run the Setup Wizard again — that’s
psw wizard; the dashboard is strictly post-bootstrap
The rule of thumb: if an action modifies infrastructure state, do it in git (locally or via CLI). If it’s observation or a transient toggle, it’s a button.
Where the Data Comes From#
The dashboard has a source abstraction with two implementations:
| Source | When it’s used | How it reads |
|---|---|---|
LocalSource | Deployed dashboard on the core target; also psw dashboard --project ~/homelab on your workstation | Reads the project folder directly — it’s mounted read-only at /project inside the container |
SSHSource | Workstation mode with PSW_SSH_HOST set | SSHes to a remote project and reads everything over the wire |
Both implementations back the same DashboardSource protocol, so the UI code never knows or cares which it’s talking to. The dashboard is purely file-based — it has no database of its own. Run history comes from .psw/converge/state.yml, live events come from .psw/events/ JSONL files, and project shape comes from the network.yml / services/ / secrets/ files in your project folder. Infrastructure state stays in git; the dashboard just renders it.
How It Relates to Convergence#
The dashboard doesn’t do convergence — it watches it. Convergence is a separate systemd service on the core target that:
- Fires every 5 minutes (or on a git-push webhook, or when you click “Trigger Run”)
- Reads the user project from git
- Builds the execution plan and runs it
- Writes every step as a structured event to
.psw/events/ - Finalizes the run into
.psw/converge/state.yml
The dashboard reads .psw/events/ for live streaming and .psw/converge/state.yml for historical runs. Disable the dashboard tomorrow and convergence keeps working fine; disable convergence and the dashboard just has nothing new to show.
Key Ideas#
- Post-bootstrap only —
psw dashboardrefuses to open an un-bootstrapped project; usepsw wizardfor setup - Same URL, two doors — browser (
https://dashboard.<domain>, behind SSO) or CLI (psw dashboard --project, local) - Read-first — most of the UI is observation, because your git repo is the source of truth
- Live streams — SSE drives every “something changed” update, no manual refresh
- No direct infra access — the dashboard never SSHes into servers or shells out to
psw; it reads project files and event streams - Feature-owned apps hidden — apps like pangolin/newt managed by a feature don’t clutter the app list; see the feature’s own page