fuzzyworld-tools / files / docs / admin.md
admin.md
bb7e1f57 2026-11-06 Pavel Safronov | 1 | # Admin panel (`fw admin`) |
| 2 | ||
| 3 | ```bash | |
| 4 | fw admin [--port N] [--no-open] [--code-dir DIR] | |
| 5 | ``` | |
| 6 | ||
| 7 | `lib/admin.py` (Python standard library only) runs **on the laptop**. It starts a small web page on `127.0.0.1` for | |
| 8 | bug reports, blog comments and what is deployed where, prints a one-time login URL and opens it in the browser. | |
| 9 | ||
| 10 | - The pages are server-rendered HTML with plain `POST` forms and **no JavaScript**. | |
| 11 | - Every button runs the matching `~/bin/fw ...` command on the server over ssh, exactly what you would type (`FW_HOST`, default `dh`). | |
| 12 | - The panel keeps no state and never shows a password. | |
| 13 | - It is local-only on purpose: it is never hosted on DreamHost. | |
| 14 | ||
| 15 | `FW_CODE_DIR` (or `--code-dir`, default `~/code`) is the folder holding the apps' working copies. | |
| 16 | ||
| 17 | The server-side `fw` must know `issues list --json`, `issues show --json`, `issues record`, `comments list --json` | |
| 18 | and `deploys --json`, so deploy the server scripts first (`npm run deploy`). | |
| 19 | ||
| 20 | ## Security model | |
| 21 | ||
| 22 | | Threat | Defence | | |
| 23 | |---|---| | |
| 24 | | Other machines | binds to `127.0.0.1` only | | |
| 25 | | Strangers on this machine | the login URL carries a random key that works once and sets a session cookie `fwadmin-<port>` (`HttpOnly`, `SameSite=Strict`); any other request without the cookie gets 403 | | |
| 26 | | DNS rebinding | the `Host` header must be exactly `127.0.0.1:<port>`, else 421 | | |
| 27 | | Cross-site requests | every `POST` needs a random per-launch token in its form (a separate secret from the cookie), an `Origin` (if sent) equal to the page's own, and `Sec-Fetch-Site` (if sent) `same-origin` or `none`; otherwise 403 and no command runs | | |
| 28 | | Script injection | strict `Content-Security-Policy` (no scripts, forms only to itself, no framing), `nosniff`, no referrer, no caching | | |
| 29 | | Command injection | mail text never reaches a command line: only a validated repo name, issue number, comment id and a fixed set of action words, each checked again before ssh is run | | |
| 30 | | Mistakes | hide, reject, untrust, remove and every mode change ask for confirmation first | | |
| 31 | | Leaks | query strings are never logged | | |
| 32 | ||
| 33 | ## Pages | |
| 34 | ||
| 35 | ### Reports (`/reports`) | |
| 36 | ||
| 37 | One table across repos: repo, number, title, state, public, mode, record, messages shown/total, age. Filters: | |
| 38 | **Waiting** (the default when anything waits), **Has replay** and **All**. | |
| 39 | ||
| 40 | ### A report (`/reports/<repo>/<n>`) | |
| 41 | ||
| 42 | - The details table and every message in full, **including reports that are not published**, with who sent it (Reporter A, B, ...), whether it passed authentication and whether it is shown. | |
| 43 | - Buttons: Approve, Hide, Close / Reopen. | |
| 44 | - Radio buttons for the repo's mode (public, reviewed, private) with Set. | |
| 45 | - Previous / next links, and a link to the public page when there is one. | |
| 46 | ||
| 47 | ### Watch (on a report with a run record) | |
| 48 | ||
| 49 | - A link to the deployed replay viewer, when the record is public. | |
| 50 | - Always "Download record" (`issues record`). | |
| 51 | - When `<code dir>/<repo>/package.json` has a `report-replay` script, "Prepare dev replay": | |
| 52 | 1. it saves the record to a fresh temporary folder, | |
| 53 | 2. runs `npm run --silent report-replay -- <record.json>` in `<code dir>/<repo>` (with `--rules <commit>` when the report's details have a commit), 60 s at most, | |
| 54 | 3. shows the output, with the `http://localhost:5173/?replay=...` address as a link. | |
| 55 | ||
| 56 | It works only while `npm run dev` runs in that repo. The record folder is kept, because the dev server needs it: | |
| 57 | one `fw-admin-<repo>-<n>-...` folder per replay under the system temp dir, mode 700. Delete them when you like. | |
| 58 | ||
| 59 | ### Comments (`/comments`) | |
| 60 | ||
| 61 | Pending comments in full, with the post link, author, date and source. Buttons: Approve, Approve + trust (email | |
| 62 | comments only), Reject, and Untrust (trusted senders only). Below, "Recently approved" (the 20 newest) with a Remove | |
| 63 | button each. | |
| 64 | ||
| 65 | ### Deploy (`/deploy`, read-only for now) | |
| 66 | ||
| 67 | Uses `fw deploys --json`. There is no button yet: deploy with `npm run deploy`. | |
| 68 | ||
| 69 | - **Tools:** per repo that declares deploy targets (in `package.json` under `fuzzyworld.deploy`), each target's state (up to date, pending, never deployed), the commit and time it was deployed from, and the commits on the default branch since that touch its folders; per laptop for `perMachine` targets. | |
| 70 | - **Apps:** every deployed app against its repo: state, deployed commit, time, and the commits since. | |
| 71 | - Commits that only touch Markdown, CI config, the licence, `.gitignore`, `docs/` or `screenshots/` are ignored, so a repo-wide CI-file rename does not mark every app pending. | |
| 72 | - The state is "unknown" when the repo or the commit is not on the server. | |
| 73 | ||
| 74 | ## Behaviour | |
| 75 | ||
| 76 | The server handles one request at a time (an idle connection cannot stall it). After each action the page reloads | |
| 77 | with the command's output (or its error) at the top. These messages are server-wide, not per browser tab. |