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,8 +19,10 @@ import (
|
||||
// - "deleted" → ID is set (a removed signature)
|
||||
// - "reaction" → ID/Emoji/Count are set (an entry's tally changed)
|
||||
// - "settings" → Settings is set (signing paused, banner changed, theme forced)
|
||||
// - "presence" → Online/Replicas are set (connected clients changed)
|
||||
// - "tick" → Time/Version/Host are set (live server clock; new in v2)
|
||||
// - "presence" → Online/Replicas/Visitors are set (connected clients or active
|
||||
// visitor identities changed)
|
||||
// - "tick" → Time/Version/Host are set (live server clock; new in v2),
|
||||
// plus Visitors (active identities; new in v3)
|
||||
type Event struct {
|
||||
Type string `json:"type"`
|
||||
Entry *Entry `json:"entry,omitempty"`
|
||||
@@ -30,6 +32,7 @@ type Event struct {
|
||||
Settings *Settings `json:"settings,omitempty"`
|
||||
Online int `json:"online,omitempty"`
|
||||
Replicas int `json:"replicas,omitempty"`
|
||||
Visitors int64 `json:"visitors,omitempty"`
|
||||
Time string `json:"time,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
@@ -69,6 +72,9 @@ type Broker struct {
|
||||
rdb *redis.Client
|
||||
dirty chan struct{}
|
||||
requests func() int64
|
||||
// visitors reports how many visitor identities were recently active (from
|
||||
// the database, so it is cluster-wide by construction); may be nil.
|
||||
visitors func() int64
|
||||
|
||||
cmu sync.RWMutex
|
||||
replicas []Replica
|
||||
@@ -229,7 +235,7 @@ func (b *Broker) localCount() int {
|
||||
|
||||
func (b *Broker) presenceChanged() {
|
||||
if b.rdb == nil {
|
||||
b.emit(Event{Type: "presence", Online: b.localCount(), Replicas: 1})
|
||||
b.emit(Event{Type: "presence", Online: b.localCount(), Replicas: 1, Visitors: b.visitorsActive()})
|
||||
return
|
||||
}
|
||||
select {
|
||||
@@ -238,6 +244,15 @@ func (b *Broker) presenceChanged() {
|
||||
}
|
||||
}
|
||||
|
||||
// visitorsActive returns the recent-visitor count, or 0 when no reporter is
|
||||
// registered.
|
||||
func (b *Broker) visitorsActive() int64 {
|
||||
if b.visitors == nil {
|
||||
return 0
|
||||
}
|
||||
return b.visitors()
|
||||
}
|
||||
|
||||
func (b *Broker) publishRaw(ctx context.Context, ev Event) error {
|
||||
payload, err := json.Marshal(ev)
|
||||
if err != nil {
|
||||
@@ -321,7 +336,7 @@ func (b *Broker) refresh(ctx context.Context) {
|
||||
b.cmu.Unlock()
|
||||
|
||||
if changed {
|
||||
b.emit(Event{Type: "presence", Online: total, Replicas: len(replicas)})
|
||||
b.emit(Event{Type: "presence", Online: total, Replicas: len(replicas), Visitors: b.visitorsActive()})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user