Adventure section: ingest, permalink, digest, emblems, noindex

Pete's side of the Adventure news feed. Receives structured game-event
facts from gogobee, templates them in Pete's warm-reporter voice, and
publishes to a new /adventure section + live Matrix posts.

- adventure.go: bearer ingest + fact-guard + 13 event templates;
  /adventure/{guid} permalink (story.html); per-event SVG emblems at
  /adventure/art/{type}.svg (card image + og:image); NoPush suppresses
  the live Matrix post (cold-start backfill).
- adventure_digest.go: daily BULLETIN roundup at DigestHour (UTC);
  unposted-in-48h = bulletins; marks them digested; per-day ?digest= URL
  avoids canonical dedup.
- config AdventureConfig (enabled/ingest_token/channel/digest_hour);
  web.New takes the seam + a priority poster; started in main.
- adventure theme colors; thumbURL passes through local emblem paths;
  adventure pages are noindex (player-named; gap #5).

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-11 00:53:59 -07:00
parent 0a723418ff
commit 4c671fb410
17 changed files with 986 additions and 18 deletions

View File

@@ -42,6 +42,7 @@ var channels = []Channel{
{Slug: "kids", Title: "Kids", Theme: "kids", Emoji: "🧒", Blurb: "World news written for younger readers — short, clear, and curious."},
{Slug: "finance", Title: "Finance", Theme: "finance", Emoji: "💰", Blurb: "Markets, money, and the business of the world. Read-only — these stories don't post to Matrix."},
{Slug: "lego", Title: "LEGO", Theme: "lego", Emoji: "🧱", Blurb: "Sets, releases, and the wider brick-building world."},
{Slug: "adventure", Title: "Adventure", Theme: "adventure", Emoji: "⚔️", Blurb: "Dispatches from the realm — deaths, first-clears, arrivals, and rivalries. Reporting from the field, this is Pete."},
}
// SourceInfo is the trimmed view of a configured feed for the settings panel.
@@ -61,6 +62,8 @@ type Server struct {
tts *ttsService // nil when server-side read-aloud is disabled
adminSubs map[string]bool // OIDC subjects allowed to view /status
pushHTTP *http.Client // SSRF-guarded client for Web Push delivery; built lazily by pushClient()
adv config.AdventureConfig // gogobee adventure-news seam
advPost PriorityPoster // posts priority adventure beats to Matrix; nil = web-only
// Daily-rotated salt for the privacy-preserving unique-visitor estimate.
// Guarded by metricsMu; never persisted (see metrics.go).
@@ -72,8 +75,8 @@ type Server struct {
// New builds the server. Templates are parsed once at startup. Each page
// gets its own template set sharing layout.html + _card.html, which avoids
// `main` define collisions between pages.
func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled bool) (*Server, error) {
pages := []string{"index", "channel", "weather", "bookmarks", "for-you", "status"}
func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled bool, adv config.AdventureConfig, advPost PriorityPoster) (*Server, error) {
pages := []string{"index", "channel", "weather", "bookmarks", "for-you", "status", "story"}
tpls := make(map[string]*template.Template, len(pages))
for _, p := range pages {
t, err := template.New("").Funcs(funcs).ParseFS(templateFS,
@@ -99,7 +102,7 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
adminSubs[sub] = true
}
}
s := &Server{cfg: cfg, sources: infos, postingEnabled: postingEnabled, tpls: tpls, adminSubs: adminSubs}
s := &Server{cfg: cfg, sources: infos, postingEnabled: postingEnabled, tpls: tpls, adminSubs: adminSubs, adv: adv, advPost: advPost}
// Optional OIDC sign-in (Authentik). Discovery is a network call; if the
// provider is unreachable at boot we log and serve anonymously rather than
@@ -165,6 +168,19 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
_, _ = w.Write([]byte("ok"))
})
// gogobee adventure-news ingest. Bearer-authed (not OIDC), so it lives
// outside the sign-in block. Self-gates on adv.Enabled (404 when off).
mux.HandleFunc("POST /api/ingest/adventure", s.handleAdventureIngest)
// Per-dispatch permalink (the article_url every ingested story points at).
// Public GET; self-gates on adv.Enabled. Distinct from GET /adventure (the
// channel listing, registered in the channels loop above).
mux.HandleFunc("GET /adventure/{guid}", s.handleAdventureStory)
// Themed per-event emblem (card + og:image). A distinct path depth from
// /adventure/{guid}, so the two patterns never overlap.
mux.HandleFunc("GET /adventure/art/{type}", s.handleAdventureArt)
if s.auth != nil {
mux.HandleFunc("GET /auth/login", s.auth.handleLogin)
mux.HandleFunc("GET /auth/callback", s.auth.handleCallback)