feat: add admin console, Redis broker and chaos lab for training demo
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Miabi Guestbook — example app
|
||||
|
||||
[](https://miabi.io?ref=badge)
|
||||
|
||||
A tiny, self-contained web app used as a **reference for deploying to
|
||||
[Miabi](https://github.com/miabi-io/miabi)**. It is a classic guestbook:
|
||||
visitors leave a name and a message, and the wall shows every signature.
|
||||
@@ -28,6 +30,19 @@ miniature:
|
||||
- **`AutoMigrate` on startup** with retry (tolerates the DB coming up a beat later)
|
||||
- **First-run seeder** — populates the wall when the table is empty (`SEED=false` to skip)
|
||||
- **`/healthz` readiness probe** that returns `503` until the database is reachable
|
||||
- **Scales horizontally** — attach a Redis and live updates + presence fan out
|
||||
across every replica via pub/sub; without it, the in-process broker makes the
|
||||
classic "works on one replica" bug easy to demonstrate
|
||||
- **Admin console** at `/admin`, guarded by a single `ADMIN_TOKEN` secret:
|
||||
overview (stats, replicas, runtime), moderation (pin / hide / delete, signer
|
||||
IP, CSV export), wall settings (pause signing, announcement banner) — all pushed live
|
||||
- **Chaos lab** (`DEBUG_ENDPOINTS=true`): crash, fail health checks, burn CPU,
|
||||
hold memory, burst logs, inject latency/5xx, plus an in-browser traffic probe
|
||||
that shows load-balancing and canary splits
|
||||
- **Scheduled-job friendly CLI** — `guestbook cleanup` (purge/expire) and
|
||||
`guestbook migrate` subcommands for Miabi cron jobs and one-off jobs
|
||||
|
||||
**Presenting it?** [`DEMO.md`](DEMO.md) is a step-by-step training runbook.
|
||||
- **Single static binary** — the whole UI is embedded with `go:embed`, so the
|
||||
image is one distroless layer with **no Node build step**
|
||||
|
||||
@@ -45,14 +60,44 @@ and keeps the container tiny.
|
||||
|
||||
| Method | Path | Description |
|
||||
|----------|----------------------|------------------------------------|
|
||||
| `GET` | `/healthz` | Liveness/readiness (checks the DB), reports `version` |
|
||||
| `GET` | `/api/info` | App name, serving `version`, `host`, connected-client count |
|
||||
| `GET` | `/healthz` | Liveness/readiness (checks the DB), reports `version`, `host`, `redis` |
|
||||
| `GET` | `/api/info` | App name, serving `version`, `host`, online count, `broker` mode, `replicas` |
|
||||
| `GET` | `/api/time` | Current server time + `version` + `host` (v2 — canary probe) |
|
||||
| `GET` | `/api/entries` | List entries + total; paginated via `?limit=&offset=` |
|
||||
| `POST` | `/api/entries` | Create `{ "name", "message" }` (broadcast live) |
|
||||
| `DELETE` | `/api/entries/{id}` | Delete an entry (broadcast live) |
|
||||
| `GET` | `/api/stream` | **SSE** stream: `welcome`, `created`, `deleted`, `presence`, `tick` events |
|
||||
| `GET` | `/` · `/all` | The web UI (home · all-signatures page) |
|
||||
| `GET` | `/api/settings` | Public wall settings: `signing_paused`, `banner` |
|
||||
| `GET` | `/api/entries` | Visible entries (pinned first) + total; paginated via `?limit=&offset=` |
|
||||
| `POST` | `/api/entries` | Create `{ "name", "message" }` (broadcast live; `503` while paused) |
|
||||
| `GET` | `/api/stream` | **SSE** stream: `welcome`, `created`, `updated`, `deleted`, `settings`, `presence`, `tick` events |
|
||||
| `GET` | `/` · `/all` · `/admin` | The web UI (home · all signatures · admin console) |
|
||||
|
||||
### Admin API
|
||||
|
||||
Authenticated with the session cookie from `POST /api/admin/login {"token"}`
|
||||
or an `Authorization: Bearer <ADMIN_TOKEN>` header. Disabled (`404`) when
|
||||
`ADMIN_TOKEN` is unset.
|
||||
|
||||
| Method | Path | Description |
|
||||
|----------|-------------------------------|-------------|
|
||||
| `GET` | `/api/admin/overview` | Stats (14-day histogram), replicas, runtime of the answering replica |
|
||||
| `GET` | `/api/admin/entries` | All entries; `?status=visible\|hidden\|pinned&q=&limit=&offset=` |
|
||||
| `PATCH` | `/api/admin/entries/{id}` | `{ "pinned"?: bool, "hidden"?: bool }` |
|
||||
| `DELETE` | `/api/admin/entries/{id}` | Soft-delete an entry |
|
||||
| `GET`·`PUT` | `/api/admin/settings` | `{ "signing_paused": bool, "banner": string }` |
|
||||
| `GET` | `/api/admin/export.csv` | Every entry as CSV |
|
||||
|
||||
### Debug API (chaos)
|
||||
|
||||
Requires admin auth **and** `DEBUG_ENDPOINTS=true`. Each call affects only the
|
||||
replica that receives it and echoes its `host`.
|
||||
|
||||
| Method | Path | Effect |
|
||||
|--------|------|--------|
|
||||
| `POST` | `/api/debug/crash` | Exit with status 1 |
|
||||
| `POST` | `/api/debug/health` | `{ "healthy": false }` makes `/healthz` return 503 |
|
||||
| `GET` | `/api/debug/slow?ms=2000` | Sleep before answering (max 30 s) |
|
||||
| `GET` | `/api/debug/error?status=500` | Answer with a 4xx/5xx |
|
||||
| `POST` | `/api/debug/cpu?seconds=30` | Saturate all cores (max 120 s) |
|
||||
| `POST` | `/api/debug/memory?mb=256&seconds=30` | Hold resident memory (max 2048 MiB) |
|
||||
| `POST` | `/api/debug/logs?count=20&level=error` | Write a burst of log lines |
|
||||
|
||||
### Live updates (SSE)
|
||||
|
||||
@@ -64,6 +109,11 @@ the live server time (with `version` + `host`). The UI uses these to update the
|
||||
wall in real time, show a live **“N online”** indicator, and drive the
|
||||
**server-time card** — all without polling.
|
||||
|
||||
With `REDIS_URL` set, `created`/`updated`/`deleted`/`settings` events go through
|
||||
a Redis pub/sub channel so every replica delivers them, and each replica
|
||||
advertises its client count under a short-TTL key so presence is cluster-wide.
|
||||
`tick` events stay per-replica on purpose: the clock shows who served you.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
@@ -81,6 +131,10 @@ wall in real time, show a live **“N online”** indicator, and drive the
|
||||
| `DB_PASSWORD` | `postgres` | Database password |
|
||||
| `DB_NAME` | `guestbook` | Database name |
|
||||
| `DB_SSL_MODE` | `disable` | Postgres `sslmode` |
|
||||
| `REDIS_URL` | — | Redis URL (`redis://:pass@host:6379`). Enables cross-replica live updates. Falls back to `REDIS_DATABASE_URL`, which is what Miabi injects when you attach a managed Redis with the `REDIS` prefix. |
|
||||
| `ADMIN_TOKEN` | — | Enables `/admin`. Store it as a **secret**. |
|
||||
| `TRUSTED_PROXIES` | — | Comma-separated CIDRs whose `X-Forwarded-For` is trusted when recording a signer's IP (e.g. `10.0.0.0/8`). Unset: the header is taken as-is — fine behind the Miabi gateway, spoofable if the app is exposed directly. |
|
||||
| `DEBUG_ENDPOINTS` | `false` | `true` enables the `/api/debug/*` chaos endpoints (still admin-only). |
|
||||
|
||||
> **On Miabi:** attach a managed database to the app and Miabi injects these
|
||||
> variables automatically — `DATABASE_URL` for the full DSN, plus the discrete
|
||||
@@ -102,6 +156,8 @@ SQLite file, so this just works:
|
||||
go run . # runs the default "server" command
|
||||
go run . server -p 9000 # okapicli flags: choose the port
|
||||
go run . --help # list commands and flags
|
||||
go run . migrate # apply migrations and exit
|
||||
go run . cleanup --purge-after 168h --max-age 720h --dry-run
|
||||
```
|
||||
|
||||
To point it at your own Postgres, set `DATABASE_URL` (or the `DB_*` vars) first:
|
||||
@@ -159,5 +215,7 @@ docker build --build-arg VERSION=2.0.0 -t miabi/guestbook:2.0.0 .
|
||||
3. **Attach the database** — Miabi injects the connection as `DATABASE_URL`.
|
||||
4. **Set the health check** to `GET /healthz` and the port to `8080`.
|
||||
5. **Connect a domain** — Miabi issues automatic SSL and routes traffic.
|
||||
6. **Optional:** add `ADMIN_TOKEN` as a secret, scale replicas, and attach a
|
||||
managed Redis with the `REDIS` prefix. See [`DEMO.md`](DEMO.md).
|
||||
|
||||
The app migrates its schema on first boot, so there is no separate release step.
|
||||
|
||||
Reference in New Issue
Block a user