feat: add visitor identity cookie and database reset job
Three connected features for the deployment-strategy demo: 1. Visitor identity cookie (v3). A new visitors table backs a first-party gb_visitor cookie: middleware on /api checks the cookie's UUID against the database on every request and regenerates + persists it when the id is missing, malformed, or unknown (fresh browser, pruned identity, or a reset database). The identity remembers the name a visitor signed with (pre-filled next time, used for unsigned posts), replaces the standalone reaction cookie for reaction de-dupe, and feeds a live "N visiting" counter (identities seen in the last 5 minutes) shown on the wall, in /api/info, the admin overview, and the presence/tick SSE events. The admin console gains Visitors/Visiting tiles and a visitors table, plus a GET /api/me endpoint and GET /api/admin/visitors. 2. guestbook reset command. Wipes entries (including soft-deleted rows), reactions and visitor identities under the schema advisory lock, with --dry-run, --hard (also reset wall settings) and --reseed flags; meant to run as a one-off Miabi Job. After a reset every outstanding cookie is invalidated, so browsers get a fresh identity on their next request. 3. cleanup learns --prune-visitors (default 30 days): the cron job now also forgets idle identities together with their reactions. Docs (README, DEMO) updated: act 8 covers reset, the visitor-identity talking point is in act 3, and the API tables list the new endpoints. Akili-Task: tsk_ptdi5ufndrzwlljk4v5g Akili-Agent: goldfish
This commit is contained in:
@@ -19,9 +19,15 @@ miniature:
|
||||
- **Live updates over SSE** — new/removed signatures stream to every open tab,
|
||||
with a real-time **connected-clients** ("N online") indicator
|
||||
- **Emoji reactions** — visitors cheer a signature with ❤️ 🎉 👍 😄 (no
|
||||
account; a cookie keeps counts honest) and the tally streams live to every
|
||||
tab. Each reaction counts against a whitelist, so the endpoint can't be
|
||||
used to spam arbitrary emoji
|
||||
account; the visitor identity keeps counts honest) and the tally streams
|
||||
live to every tab. Each reaction counts against a whitelist, so the
|
||||
endpoint can't be used to spam arbitrary emoji
|
||||
- **Visitor identity cookie** (new in v3) — every browser gets a `gb_visitor`
|
||||
cookie whose UUID the backend checks **against the database** on each API
|
||||
request: unknown ids (first visit, pruned identity, or a wiped database) are
|
||||
discarded and regenerated. The identity remembers the name you signed with,
|
||||
drives the reaction de-dupe, and feeds a live **“N visiting”** counter
|
||||
(distinct from connected SSE clients)
|
||||
- **Light & dark themes** — the wall follows the OS preference, visitors can
|
||||
toggle it, and a moderator can **force a theme for everyone** from the admin
|
||||
console (pushed live over SSE)
|
||||
@@ -53,8 +59,10 @@ miniature:
|
||||
hold memory, burst logs, inject latency/5xx, a **rolling-update wave** that
|
||||
bounces replicas one at a time while the replica table shows each one reset,
|
||||
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
|
||||
- **Scheduled-job friendly CLI** — `guestbook cleanup` (purge/expire/prune
|
||||
visitors) and `guestbook migrate` for Miabi cron jobs, plus
|
||||
**`guestbook reset`** to wipe the wall (entries, reactions, identities;
|
||||
`--reseed`, `--hard`) as a one-off job
|
||||
|
||||
**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
|
||||
@@ -75,9 +83,10 @@ and keeps the container tiny.
|
||||
| Method | Path | Description |
|
||||
|----------|----------------------|------------------------------------|
|
||||
| `GET` | `/healthz` | Liveness/readiness (checks the DB), reports `version`, `host`, `redis`, `requests` |
|
||||
| `GET` | `/api/info` | App name, serving `version`, `host`, online count, `broker` mode, `replicas` (with per-replica request counts), `requests`, reaction whitelist |
|
||||
| `GET` | `/api/info` | App name, serving `version`, `host`, online count, active `visitors`, `broker` mode, `replicas` (with per-replica request counts), `requests`, reaction whitelist |
|
||||
| `GET` | `/api/time` | Current server time + `version` + `host` (v2 — canary probe) |
|
||||
| `GET` | `/api/settings` | Public wall settings: `signing_paused`, `banner`, `theme` |
|
||||
| `GET` | `/api/me` | The caller's visitor identity: id, remembered name, signature count, first seen |
|
||||
| `GET` | `/api/entries` | Visible entries (pinned first) with reaction tallies + total; paginated via `?limit=&offset=` |
|
||||
| `POST` | `/api/entries` | Create `{ "name", "message" }` (broadcast live; `503` while paused) |
|
||||
| `POST` | `/api/entries/{id}/reactions?emoji=` | Add this browser's reaction (`heart`·`tada`·`thumbsup`·`smile`); idempotent, broadcasts the new tally |
|
||||
@@ -93,7 +102,8 @@ or an `Authorization: Bearer <ADMIN_TOKEN>` header. Disabled (`404`) when
|
||||
|
||||
| Method | Path | Description |
|
||||
|----------|-------------------------------|-------------|
|
||||
| `GET` | `/api/admin/overview` | Stats (14-day histogram, reaction totals), replicas (with per-replica request counts), runtime of the answering replica |
|
||||
| `GET` | `/api/admin/overview` | Stats (14-day histogram, reaction + visitor totals), replicas (with per-replica request counts), runtime of the answering replica |
|
||||
| `GET` | `/api/admin/visitors` | Recent visitor identities (name remembered, signature count, last seen; `?limit=`) |
|
||||
| `GET` | `/api/admin/entries` | All entries with reaction tallies; `?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 |
|
||||
@@ -133,6 +143,26 @@ each replica advertises its client count **and request count** under a
|
||||
short-TTL key so presence is cluster-wide. `tick` events stay per-replica on
|
||||
purpose: the clock shows who served you.
|
||||
|
||||
### Visitor identity
|
||||
|
||||
Every API request passes through middleware that resolves the caller's
|
||||
**visitor identity**: if the `gb_visitor` cookie is missing, malformed, or its
|
||||
id is **not found in the `visitors` table**, the backend mints a new UUID,
|
||||
persists it and rewrites the cookie. Known ids are kept and stamped
|
||||
(`last_seen`). The identity:
|
||||
|
||||
- remembers the **name** you signed with (pre-filled next time, used for
|
||||
unsigned posts),
|
||||
- keys **reaction** de-duplication (replacing the old standalone reaction
|
||||
cookie),
|
||||
- feeds the **“N visiting”** counter on the wall, in `/api/info`, the admin
|
||||
overview and the `tick`/`presence` SSE events (identities seen in the last
|
||||
5 minutes — read from the database, so it is cluster-wide with no Redis).
|
||||
|
||||
Because validity lives in the database, a `guestbook reset` (or pruning by the
|
||||
`cleanup` job) invalidates every outstanding cookie: browsers silently get a
|
||||
fresh identity on their next request.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
@@ -176,7 +206,8 @@ 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
|
||||
go run . cleanup --purge-after 168h --max-age 720h --prune-visitors 720h --dry-run
|
||||
go run . reset --dry-run # preview a full wipe; add --reseed to refill the wall
|
||||
```
|
||||
|
||||
To point it at your own Postgres, set `DATABASE_URL` (or the `DB_*` vars) first:
|
||||
|
||||
Reference in New Issue
Block a user