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 ~/homelab

Both 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 dashboard is post-bootstrap only. It refuses to open a project that hasn’t been bootstrapped and prints “For pre-bootstrap setup, use psw 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.

PageURLWhat 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/runsEvery 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/appsEverything 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/addBrowse the app catalog with categories and search, pick apps, assign targets, submit
Targets/targetsEvery 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/historyThe git log of your user project — every commit you (or convergence) ever pushed
Remote Access/remoteRemote-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_started event 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_completed event carries the result and duration
  • A heartbeat every 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:

ActionWhat 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 agentFlips the systemd timer on the core target. You’d disable it while debugging or during maintenance windows
Trigger a manual runFires a convergence run right now instead of waiting for the 5-minute timer
Expose / unexpose an app remotelyToggles a Pangolin resource — see remote access
Setup Pangolin on a targetOne-click remote-access bootstrap for the VPS-side
Rotate certs / rotate VPSDestructive 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:

SourceWhen it’s usedHow it reads
LocalSourceDeployed dashboard on the core target; also psw dashboard --project ~/homelab on your workstationReads the project folder directly — it’s mounted read-only at /project inside the container
SSHSourceWorkstation mode with PSW_SSH_HOST setSSHes 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:

  1. Fires every 5 minutes (or on a git-push webhook, or when you click “Trigger Run”)
  2. Reads the user project from git
  3. Builds the execution plan and runs it
  4. Writes every step as a structured event to .psw/events/
  5. 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 onlypsw dashboard refuses to open an un-bootstrapped project; use psw wizard for 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 streamsSSE 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