feat: add emoji reactions, light/dark themes and rollout observability

Add a set of guest-facing and operator-facing features that make the
deployment-strategy demos (recreate, rolling, canary) more visual:

- Emoji reactions (heart/tada/thumbsup/smile): visitors react without an
  account, a first-party cookie keeps counts honest, the tally streams
  live to every tab over SSE. Backed by a new reactions table with an
  idempotent upsert; whitelisted emoji so the endpoint can't be abused.
- Light & dark themes: the wall follows the OS preference, visitors can
  toggle it (persisted locally), and a moderator can force a theme for
  everyone from the admin console, pushed live over SSE.
- Per-replica request counter: a middleware tallies requests answered by
  each replica; surfaced in /healthz, /api/info and the admin replica
  table. Under load-balancing the counts diverge, and a rolling update
  resets them one container at a time — the rollout made visible.
- Rolling-update wave in the Chaos lab: bounces replicas one at a time
  (via the crash endpoint) while polling the replica table, so a training
  session can watch each replica reset and rejoin — what a rolling update
  looks like from the inside.
- Admin console: reactions tile, per-replica Requests column, reaction
  tallies in moderation, and a wall-theme setting.
- Favicon + /favicon.ico redirect.

Tests cover reactions (idempotency, tallies, whitelist) and the theme
setting round-trip.

Akili-Task: tsk_xgg6sfuzkzymyc6gstr4
Akili-Agent: goldfish
This commit is contained in:
Akili
2026-10-05 21:36:23 +02:00
parent 886f7cdc14
commit 3a68f64eb6
14 changed files with 726 additions and 82 deletions
+19 -13
View File
@@ -28,6 +28,9 @@ type Handler struct {
unhealthy atomic.Bool
chaos chaosState
// requests counts HTTP requests answered by this replica since boot. A
// rolling update resets it one container at a time; see countRequests.
requests atomic.Int64
}
// serverTime is the shape of the live clock payload (v2 feature). It carries
@@ -71,12 +74,13 @@ func (h *Handler) Health(c *okapi.Context) error {
})
}
return c.OK(okapi.M{
"status": "ok",
"db": "up",
"redis": h.broker.RedisStatus(c.Request().Context()),
"app": h.appName,
"version": h.version,
"host": h.host,
"status": "ok",
"db": "up",
"redis": h.broker.RedisStatus(c.Request().Context()),
"app": h.appName,
"version": h.version,
"host": h.host,
"requests": h.requests.Load(),
})
}
@@ -85,13 +89,15 @@ func (h *Handler) Health(c *okapi.Context) error {
// are currently connected.
func (h *Handler) Info(c *okapi.Context) error {
return c.OK(okapi.M{
"app": h.appName,
"version": h.version,
"online": h.broker.Online(),
"host": h.host,
"broker": h.broker.Mode(),
"replicas": h.broker.Replicas(),
"admin": h.auth.Enabled(),
"app": h.appName,
"version": h.version,
"online": h.broker.Online(),
"host": h.host,
"broker": h.broker.Mode(),
"replicas": h.broker.Replicas(),
"requests": h.requests.Load(),
"admin": h.auth.Enabled(),
"reactions": ReactionEmojis(),
})
}