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:
+27
-3
@@ -98,9 +98,21 @@ func (h *Handler) Info(c *okapi.Context) error {
|
||||
"requests": h.requests.Load(),
|
||||
"admin": h.auth.Enabled(),
|
||||
"reactions": ReactionEmojis(),
|
||||
"visitors": h.visitorsActive(c.Request().Context()),
|
||||
})
|
||||
}
|
||||
|
||||
// visitorsActive counts identities seen in the last few minutes — the
|
||||
// "currently visiting" number. It degrades to 0 on a database hiccup; the
|
||||
// SSE presence event simply carries whatever the database last said.
|
||||
func (h *Handler) visitorsActive(ctx context.Context) int64 {
|
||||
n, err := h.store.VisitorsSeenAfter(ctx, time.Now().Add(-visitorFreshWindow))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// PublicSettings returns the wall state visitors need: paused flag and banner.
|
||||
func (h *Handler) PublicSettings(c *okapi.Context) error {
|
||||
st, err := h.store.Settings(c.Request().Context())
|
||||
@@ -172,9 +184,6 @@ func (h *Handler) CreateEntry(c *okapi.Context) error {
|
||||
name := strings.TrimSpace(req.Name)
|
||||
message := strings.TrimSpace(req.Message)
|
||||
|
||||
if name == "" {
|
||||
name = "Anonymous"
|
||||
}
|
||||
if r := []rune(name); len(r) > maxNameLen {
|
||||
name = string(r[:maxNameLen])
|
||||
}
|
||||
@@ -187,10 +196,24 @@ func (h *Handler) CreateEntry(c *okapi.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// The visitor identity ties the signature to a browser: an unsigned post
|
||||
// keeps the name the visitor used before, and a signed post is remembered
|
||||
// for next time.
|
||||
visitor := visitorFromContext(c)
|
||||
if name == "" {
|
||||
name = visitorName(visitor)
|
||||
}
|
||||
if name == "" {
|
||||
name = "Anonymous"
|
||||
}
|
||||
|
||||
entry, err := h.store.Create(ctx, name, message, h.host, clientIP(c))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, okapi.M{"error": "could not save entry"})
|
||||
}
|
||||
if !visitor.IsZero() {
|
||||
h.store.VisitorSigned(ctx, visitor.ID, name)
|
||||
}
|
||||
|
||||
h.broker.Publish(Event{Type: "created", Entry: &entry})
|
||||
return c.Created(entry)
|
||||
@@ -233,6 +256,7 @@ func (h *Handler) Stream(c *okapi.Context) error {
|
||||
"online": h.broker.Online(),
|
||||
"replicas": len(h.broker.Replicas()),
|
||||
"host": h.host,
|
||||
"visitors": h.visitorsActive(ctx),
|
||||
}}
|
||||
select {
|
||||
case msgs <- welcome:
|
||||
|
||||
Reference in New Issue
Block a user