Web Dashboard (Experimental)

The web dashboard lets you monitor and interact with agent sessions from any browser, including your phone, tablet, or another computer. It runs as an embedded web server inside the aoe binary.

The web dashboard on desktop: workspace sidebar, live agent terminal, and diff panel

In this section

This page is the overview: how to run aoe serve, the access and auth modes, the security model, and PWA install. The rest of the surface has its own pages:

  • Dashboard & workspaces: the layout, status glyphs, the session-creation wizard, sidebar sort, triage (pin / archive / snooze), command palette, and the first-run tutorial.
  • Terminal view: the agent and paired terminals, reconnect behavior, the WebSocket close-code reference, and read-only mode.
  • Diff view: reviewing changed files, the flat / tree file list, per-session base override, and inline review comments.
  • Settings & profiles: the settings tabs, profile picker, connected-device tracking, and step-up elevation.

Mobile and touch behavior is documented inline on each page, next to the surface it applies to.

Availability

The web dashboard is included in all release binaries: GitHub Releases, the quick install script, and Homebrew (brew install aoe). No extra build steps needed, just run aoe serve.

If you build from source, the dashboard requires the serve Cargo feature (and Node.js to compile the embedded frontend); see Web Dashboard Development.

Starting the server

# Localhost only (safe, default)
aoe serve

# Remote access over HTTPS (Tailscale Funnel if available, else Cloudflare quick tunnel)
aoe serve --remote

# Accessible from other devices on your LAN/VPN (HTTP, requires VPN)
aoe serve --host 0.0.0.0

# Run in background
aoe serve --daemon

# Open the printed URL in the default browser once the server is ready
aoe serve --open

# Read-only monitoring (no terminal input)
aoe serve --remote --read-only

The server prints a URL with an auth token:

aoe web dashboard running at:
  http://localhost:8080/?token=a1b2c3...

Open this URL in any browser to access the dashboard. The token is set as a cookie on first visit so you don’t need to keep it in the URL.

--open is opt-in. It is suppressed when you also pass --daemon or --remote, when running over SSH (SSH_CONNECTION / SSH_TTY set), and on Linux/BSD with no DISPLAY / WAYLAND_DISPLAY.

Retrieving the live URL

In --remote mode the auth token rotates every 4 hours, so a URL captured at startup eventually stops working. Use aoe url to print the current dashboard URL of a running daemon:

# Print the primary URL with the live token
aoe url

# Print every labeled URL (Tailscale / LAN / localhost), tab-separated
aoe url --all

# Print only the auth token (useful for scripted login flows)
aoe url --token-only

aoe url exits non-zero if no daemon is running.

In --remote mode, a QR code is also printed for easy phone pairing.

Remote access

The --remote flag is the recommended way to access the dashboard from your phone or another device:

aoe serve --remote

aoe picks a transport automatically in this order:

1. Tailscale Funnel (preferred when available)

If tailscale is on the host’s PATH and the daemon is logged in, aoe runs tailscale funnel --bg --yes <port> (the Tailscale 1.52+ single-command Funnel syntax) and exposes the dashboard at your stable https://<machine>.<tailnet>.ts.net URL. No domain, no Cloudflare account, no rotating URLs. This is the only option where a PWA installed on your phone keeps working across server restarts (the URL is stable).

Setup (two one-time gates; aoe surfaces the fix if either is missing):

  1. Install Tailscale on the host (tailscale.com/download)
  2. tailscale up
  3. Enable the Funnel feature for your tailnet (tailnet-wide switch): login.tailscale.com/f/funnel. When this isn’t enabled, tailscale funnel prints a node-specific activation URL; aoe detects that URL in stderr and bails in seconds with the link instead of timing out.
  4. Grant the funnel nodeAttr to this node in your tailnet ACL: login.tailscale.com/admin/acls/file. A default rule like { "target": ["autogroup:member"], "attr": ["funnel"] } works for personal tailnets; if your node is tagged, target the tag instead (autogroup:member excludes tagged devices).
  5. aoe serve --remote

Caveat: aoe serve --remote runs tailscale funnel --bg --yes <port>, which configures port 443 to proxy to the dashboard. If you already have a non-loopback service on port 443 of this node’s Funnel config (your own webapp pointing at a tailnet IP, a remote service), aoe refuses to start rather than silently replace it. A stale loopback config from a prior aoe run is fine, aoe overwrites that cleanly. Clear any conflict with tailscale funnel reset (the Error dialog offers this as [R]) and re-run, or pass --no-tailscale to use Cloudflare instead.

2. Named Cloudflare tunnel

Stable hostname on your own Cloudflare-managed domain. Takes precedence over Tailscale auto-detection when you pass the flags:

# One-time setup
cloudflared tunnel create my-tunnel
# Add a CNAME record: aoe.example.com -> <tunnel-id>.cfargotunnel.com

# Run with stable URL
aoe serve --remote --tunnel-name my-tunnel --tunnel-url aoe.example.com

3. Cloudflare quick tunnel (fallback)

Zero-config but the URL rotates on every restart. Fine for one-off remote sessions, bad for installed PWAs: the home-screen app is bound to the URL it was installed from, so every restart costs you a delete-and-reinstall.

aoe serve --remote

Requires cloudflared on the host:

aoe prints a notice when it falls back to this path so you don’t accidentally install a PWA from a rotating URL.

Flags

FlagDefaultDescription
--port8080Port to listen on
--host127.0.0.1Bind address. Use 0.0.0.0 for LAN/VPN access
--authtokenAuth mode: token (URL token, default), passphrase (passphrase login wall only), none (no auth, loopback only unless --behind-proxy)
--passphrasePassphrase for the login wall. Valid with --auth=token (token + passphrase) and --auth=passphrase. Also reads AOE_SERVE_PASSPHRASE
--behind-proxyoffServer sits behind an external reverse proxy that terminates TLS. Sets cookies as ; Secure and trusts X-Forwarded-For / cf-connecting-ip from loopback peers; does NOT spawn a tunnel
--no-authoffAlias for --auth=none (kept for backwards compatibility)
--remoteoffExpose over HTTPS tunnel (Tailscale Funnel if available, else Cloudflare quick tunnel)
--tunnel-nameUse a named Cloudflare tunnel (requires --remote; overrides Tailscale auto-detection)
--no-tailscaleoffSkip Tailscale Funnel auto-detection and use Cloudflare (requires --remote)
--tunnel-urlHostname for a named tunnel (requires --tunnel-name)
--read-onlyoffView terminals but cannot send keystrokes
--daemonoffFork to background and detach from terminal
--stopStop a running daemon

Auth mode matrix

ModeToken URLPassphrase wallUse case
--auth=token (default)requiredoptional (--passphrase)Standard local / VPN / Tailscale deployments
--auth=passphrase --passphrase XnonerequiredReverse-proxy deployments where pasting a token URL on mobile is too high friction
--auth=none (alias --no-auth)nonenoneLocalhost-only quick testing

Notes:

  • --auth=passphrase and --auth=none on a non-loopback bind require --behind-proxy. The flag asserts that an upstream reverse proxy terminates TLS and forwards the client IP. Without it, reduced-auth modes refuse to bind to a routable address.
  • --auth=passphrase requires --passphrase <VALUE> (or AOE_SERVE_PASSPHRASE) since the passphrase becomes the only human gate.
  • --auth=none --passphrase X is rejected explicitly; the previous silent acceptance of --no-auth --passphrase was a footgun. Use --auth=passphrase if the passphrase wall is what you want.
  • --remote is incompatible with --auth=none and --auth=passphrase; the public tunnel mandates both token auth and a passphrase.

Behind a reverse proxy

When TLS is terminated by an external reverse proxy (Traefik, nginx, Caddy) that forwards traffic to aoe serve on loopback (often through an SSH reverse tunnel), use --behind-proxy so cookies carry ; Secure and the rate limiter keys requests by the real client IP:

# Loopback bind, passphrase login wall, TLS terminated upstream.
aoe serve \
  --host 127.0.0.1 --port 42041 \
  --auth=passphrase --passphrase "$AOE_PASSPHRASE" \
  --behind-proxy

The upstream proxy must set X-Forwarded-For (or cf-connecting-ip); aoe reads the last value as the client IP. The trust check fires only when the socket peer is loopback, so a misconfigured upstream that lets requests reach aoe directly cannot spoof the IP.

Security

The web dashboard exposes terminal access. Anyone who authenticates can send keystrokes to your agent sessions, which run as your user.

Authentication

  • Token auth (--auth=token, default): A random 256-bit token is generated on startup and stored at ~/.config/agent-of-empires/serve.token (Linux) or ~/.agent-of-empires/serve.token (macOS). The token is passed via URL on first visit, then stored as an HttpOnly; SameSite=Strict cookie.
  • Passphrase wall (--auth=passphrase, or combined with token via --passphrase): An argon2-hashed passphrase gates /login. Sessions are bound to a per-device secret stored in the client’s localStorage; a leaked session cookie alone is insufficient.
  • Rate limiting: 5 failed login attempts from an IP trigger a 15-minute lockout. Uses Cf-Connecting-IP / X-Forwarded-For from loopback peers (covers --remote tunnel mode and --behind-proxy reverse-proxy mode) to prevent IP spoofing.
  • Token rotation: In --remote mode, the token rotates every 4 hours with a 5-minute grace period for active sessions.
  • Device tracking: Connected devices (IP, browser, last seen) are visible in Settings > Security.
  • Step-up elevation: A “Confirm passphrase” prompt appears on writes whose payload can plant code for the next session spawn: the sandbox and worktree sections. Confirmation lasts 15 minutes. User-preference writes (theme, sound, updates, notification toggles, logging filter, profile description, and safe session fields like yolo_mode_default) save without the prompt; saving a theme should not feel like signing in again.
  • Local-only fields: The agent-command surface (session.agent_command_override, session.agent_extra_args, session.custom_agents, session.agent_detect_as) and the status-hook shell commands (status_hooks.on_*) map names to arbitrary host commands, so the dashboard never writes them: the server rejects a PATCH that touches them and they are editable only in the TUI on the host. The per-field policy is derived from the settings schema (#1692), so this list stays in sync automatically.

Security headers

The server sets X-Frame-Options: DENY (prevents clickjacking), X-Content-Type-Options: nosniff, and Referrer-Policy: no-referrer (prevents token leaking via Referer headers).

Safe usage patterns

  • Localhost (aoe serve): Same security as the TUI. Fine.
  • Remote via tunnel (aoe serve --remote): Encrypted via HTTPS. Recommended for phone access.
  • Over Tailscale/WireGuard (aoe serve --host 0.0.0.0): The VPN encrypts traffic.
  • Behind a reverse proxy (aoe serve --auth=passphrase --passphrase ... --behind-proxy): TLS terminated upstream by Traefik / nginx / Caddy. Passphrase is the only human gate.
  • Read-only (aoe serve --remote --read-only): Monitor sessions without input capability.

Dangerous

  • aoe serve --host 0.0.0.0 on public WiFi without a VPN: traffic is unencrypted HTTP
  • aoe serve --auth=none --host 0.0.0.0 (or alias --no-auth --host 0.0.0.0): blocked (refuses to start without --behind-proxy)
  • aoe serve --auth=none --remote or --auth=passphrase --remote: blocked (refuses to start)

Installing as a PWA

The dashboard supports Progressive Web App (PWA) installation for an app-like experience:

macOS (Chrome): Three-dot menu > “Install Agent of Empires” — creates a standalone window with a Dock icon.

macOS (Safari): File > Add to Dock.

iOS: Share > Add to Home Screen.

Android: Chrome will prompt “Add to Home Screen” or show an install banner.

The PWA requires the server to be running. Use --daemon to keep it running in the background:

aoe serve --daemon
# Server runs in background, prints PID
# Stop with: aoe serve --stop

Shutdown behavior

Ctrl-C on a foreground aoe serve, and aoe serve --stop against a daemon, both exit within ~5 seconds even with open dashboard tabs. Live structured view and terminal WebSocket clients receive a close frame with code 1001 (“going away”) so the browser logs a clean reason and skips its transient-error reconnect backoff for one cycle. The reconnect resumes normally once a fresh aoe serve is running. A 5-second hard cap acts as a safety net: if any handler fails to honor the shutdown signal, the process still exits and emits WARN shutdown: graceful shutdown exceeded grace window, forcing exit.

How it works

aoe serve runs an embedded server inside the aoe process; your browser connects to it to list sessions, stream terminal output, and (when write access is enabled) send input. Each terminal is backed by a real tmux session, so your work survives browser crashes, network drops, and reconnects.

The terminal disconnect banner surfaces a WebSocket close code when it can’t reach a working pane. The decoder ring for those codes lives on the Terminal view page.

For build, architecture, and frontend-development details, see Web Dashboard Development.