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:
Akili
2026-10-07 19:06:07 +02:00
parent a12c27b6bc
commit 86215f1eb1
13 changed files with 775 additions and 74 deletions
+13
View File
@@ -81,6 +81,7 @@ func (h *Handler) AdminOverview(c *okapi.Context) error {
"stats": stats,
"settings": settings,
"online": h.broker.Online(),
"visitors": h.visitorsActive(ctx),
"replicas": h.broker.Replicas(),
"debug": h.debug,
"runtime": okapi.M{
@@ -127,6 +128,18 @@ func (h *Handler) AdminListEntries(c *okapi.Context) error {
return c.OK(okapi.M{"entries": out, "total": total, "limit": limit, "offset": offset})
}
// AdminVisitors lists the most recently active visitor identities, so the
// console can show the cookies the app has issued and who is around now.
func (h *Handler) AdminVisitors(c *okapi.Context) error {
limit := atoiDefault(c.Query("limit"), 50)
visitors, err := h.store.AdminVisitors(c.Request().Context(), limit)
if err != nil {
return c.JSON(http.StatusInternalServerError, okapi.M{"error": "could not load visitors"})
}
fresh := time.Now().Add(-visitorFreshWindow)
return c.OK(okapi.M{"visitors": visitors, "active_since": fresh.UTC().Format(time.RFC3339)})
}
// AdminUpdateEntry pins/unpins or hides/unhides an entry and broadcasts it.
func (h *Handler) AdminUpdateEntry(c *okapi.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)