|
| 1 | +# Web Platform (`rosbackup-web`) |
| 2 | + |
| 3 | +A self-hosted web UI + HTTP API for rosbackup-ng: see the fleet, run backups (whole |
| 4 | +fleet / group / tag / single target) with live progress, browse and download backup |
| 5 | +history, and (optionally) watch per-target latency. It embeds the same console-free |
| 6 | +backup engine the CLI uses — the web layer is presentation around the existing core, |
| 7 | +not a fork of the backup logic. |
| 8 | + |
| 9 | +> **Scope (first iteration).** Read & run: dashboard, targets, run-now with a live |
| 10 | +> view, run history, and a backup browser/download over the **local** destination. |
| 11 | +> Schedules, API tokens, remote-destination browsing, and push-restore are later |
| 12 | +> phases (see `docs/`/the design spec). RBAC and capability toggles are future work. |
| 13 | +
|
| 14 | +## Install |
| 15 | + |
| 16 | +The web platform is an optional extra so the core CLI keeps its lean dependency set: |
| 17 | + |
| 18 | +```bash |
| 19 | +pip install "rosbackup-ng[web]" |
| 20 | +# from a checkout: |
| 21 | +pip install -e ".[web]" |
| 22 | +``` |
| 23 | + |
| 24 | +This adds the `rosbackup-web` console script (FastAPI + uvicorn + Jinja2; htmx is |
| 25 | +vendored in-tree — no Node, no CDN). A source checkout can also run it directly with |
| 26 | +`python rosbackup-web.py …`. |
| 27 | + |
| 28 | +## First run |
| 29 | + |
| 30 | +The web app reads the **same config directory** as the CLI (`-c`), and the YAML files |
| 31 | +stay the single source of truth. Runtime state (run history, sessions, audit log) |
| 32 | +lives in a SQLite database at `<config-dir>/web.db` (0600). |
| 33 | + |
| 34 | +```bash |
| 35 | +# 1) set the admin password (out-of-band; never exposed over HTTP) |
| 36 | +rosbackup-web -c /etc/rosbackup --set-password |
| 37 | + |
| 38 | +# 2) start the server (binds 127.0.0.1:8474 by default) |
| 39 | +rosbackup-web -c /etc/rosbackup |
| 40 | +``` |
| 41 | + |
| 42 | +Open <http://127.0.0.1:8474>. Until a password is set, every page returns the |
| 43 | +"set the password from the shell" notice — there is no network-exposed setup wizard. |
| 44 | + |
| 45 | +### Options |
| 46 | + |
| 47 | +| Flag | Default | Meaning | |
| 48 | +|---|---|---| |
| 49 | +| `-c`, `--config-dir` | `config` | Config directory (same as the CLI) | |
| 50 | +| `--host` | `127.0.0.1` | Bind address. Anything other than localhost prints a warning — terminate TLS at a reverse proxy | |
| 51 | +| `--port` | `8474` | Bind port | |
| 52 | +| `--db` | `<config-dir>/web.db` | Web state database path | |
| 53 | +| `--set-password` | — | Set the admin password and exit (does not start the server) | |
| 54 | +| `--latency` | off | Enable optional ICMP latency monitoring of target hosts | |
| 55 | +| `--latency-interval` | `30` | Latency poll interval in seconds (implies `--latency`) | |
| 56 | + |
| 57 | +## Security model |
| 58 | + |
| 59 | +- **Single admin account.** Password stored as a stdlib `scrypt` hash in `web.db`; |
| 60 | + set only via `--set-password`. |
| 61 | +- **Sessions:** HMAC-signed cookie (`HttpOnly`, `SameSite=Lax`, `Secure` behind TLS), |
| 62 | + 30-minute idle / 12-hour absolute timeout. |
| 63 | +- **CSRF:** every mutating request needs the per-session token (htmx sends it as a |
| 64 | + header; plain forms include it as a hidden field). |
| 65 | +- **Login lockout:** 5 failures → 60-second lockout. |
| 66 | +- **Headers:** self-only Content-Security-Policy (htmx/CSS are vendored), `DENY` |
| 67 | + framing, `nosniff`. |
| 68 | +- **Bind localhost; let a reverse proxy own TLS** (see below). |
| 69 | +- Secrets (passwords, storage credentials) are never rendered. |
| 70 | + |
| 71 | +## Behind a reverse proxy (TLS via Caddy + deSEC DNS-01) |
| 72 | + |
| 73 | +For internal-only names or hosts without inbound :80/:443, the DNS-01 ACME challenge |
| 74 | +issues real certificates without exposing the box. Example with [deSEC](https://desec.io): |
| 75 | + |
| 76 | +See [`examples/Caddyfile`](examples/Caddyfile). In short: |
| 77 | + |
| 78 | +``` |
| 79 | +rosbackup.example.com { |
| 80 | + reverse_proxy 127.0.0.1:8474 |
| 81 | + tls { |
| 82 | + dns desec {env.DESEC_TOKEN} |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +(Build Caddy with the deSEC DNS provider plugin: `xcaddy build --with |
| 88 | +github.com/caddy-dns/desec`, or use a Caddy build that bundles it.) Caddy sets |
| 89 | +`X-Forwarded-Proto: https`, so the session cookie is issued with `Secure`. |
| 90 | + |
| 91 | +## Run as a service (systemd) |
| 92 | + |
| 93 | +See [`examples/rosbackup-web.service`](examples/rosbackup-web.service). It runs the |
| 94 | +server as an unprivileged user bound to localhost, with the config directory and |
| 95 | +`web.db` on persistent storage. |
| 96 | + |
| 97 | +## HTTP API (`/api/v1`) |
| 98 | + |
| 99 | +Session cookie (UI) auth today; bearer tokens arrive in a later phase. Selected |
| 100 | +endpoints: |
| 101 | + |
| 102 | +``` |
| 103 | +GET /api/v1/health liveness (unauthenticated): version, db ok |
| 104 | +GET /api/v1/targets[?group=&tag=] inventory |
| 105 | +GET /api/v1/groups | /api/v1/tags names + device counts (the --list data) |
| 106 | +POST /api/v1/runs {scope_kind, scope_value, dry_run} -> 202 {run_id, started} |
| 107 | +GET /api/v1/runs[?limit=&offset=] run history |
| 108 | +GET /api/v1/runs/{id} run + per-target results (incl. storage destinations) |
| 109 | +GET /events/runs/{id} Server-Sent-Events live stream |
| 110 | +GET /api/v1/backups/{target} local backup sets for a target |
| 111 | +GET /api/v1/backups/{target}/download?path=<dir>/<file> download one backup file |
| 112 | +``` |
| 113 | + |
| 114 | +`scope_kind` is `all` | `group` | `tag` | `target`; non-`all` scopes require |
| 115 | +`scope_value`. Only **one backup run executes at a time** (a global lock); a request |
| 116 | +made while a run is active returns that run's id with `started: false`. |
| 117 | + |
| 118 | +## UI & design system |
| 119 | + |
| 120 | +The UI is a standard admin shell: a fixed **left sidebar** (brand + nav + sign-out) |
| 121 | +and a fluid content area with a header bar (`page_title` + optional `head_actions`). |
| 122 | +It is intentionally dependency-free — no CSS framework, no build step. |
| 123 | + |
| 124 | +- **Layout:** `templates/base.html` is the authenticated shell; pages `{% extends %}` |
| 125 | + it and fill `page_title`, `head_actions`, and `content`. Pre-auth pages |
| 126 | + (`login`, `setup`) extend the standalone `_auth_base.html` (centered card). |
| 127 | +- **Tokens & components:** all styling lives in `static/style.css` via CSS custom |
| 128 | + properties (`--side-bg`, `--accent`, `--ok/--bad/--warn`, `--radius`, …). Reusable |
| 129 | + component classes: `.card` / `.card-head`, the `.cards`+`.stat` summary grid, |
| 130 | + `table` (+ `.grid`), `.chip` (+ `.ok/.bad/.running/.muted/.group`), `.banner` |
| 131 | + (`.info`/`.banner-warn`/`.banner-ok`), `.btn`/`.btn.primary`, and the `.filters` |
| 132 | + / `.run-form` form helpers. Add new screens by composing these, not new CSS. |
| 133 | +- **Theme:** dark sidebar, light content. To restyle, change the tokens at the top of |
| 134 | + `style.css`; components follow. |
| 135 | + |
| 136 | +## Latency monitoring (optional) |
| 137 | + |
| 138 | +`--latency` starts a background thread that pings each target host on an interval and |
| 139 | +shows reachability + round-trip time in the Targets table. It shells out to the system |
| 140 | +`ping` (no root, no extra dependency); hosts that do not answer show as `down`. It is |
| 141 | +purely observational — it never affects backups. |
0 commit comments