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

@@ -13,11 +13,29 @@ import (
var envBracketRe = regexp.MustCompile(`\$\{([^}]+)\}`)
type Config struct {
Matrix MatrixConfig `toml:"matrix"`
Posting PostingConfig `toml:"posting"`
Storage StorageConfig `toml:"storage"`
Web WebConfig `toml:"web"`
Sources []SourceConfig `toml:"sources"`
Matrix MatrixConfig `toml:"matrix"`
Posting PostingConfig `toml:"posting"`
Storage StorageConfig `toml:"storage"`
Web WebConfig `toml:"web"`
Adventure AdventureConfig `toml:"adventure"`
Sources []SourceConfig `toml:"sources"`
}
// AdventureConfig wires the gogobee adventure-news seam: gogobee POSTs
// game-event facts to Pete's ingest endpoint, Pete templates them into stories
// on the /adventure section and posts PRIORITY beats live to Matrix. Disabled by
// default — the endpoint 404s and no adventure channel appears until enabled.
type AdventureConfig struct {
Enabled bool `toml:"enabled"`
// IngestToken is the shared bearer secret gogobee presents on
// POST /api/ingest/adventure. Required when enabled. Use ${ENV_VAR}.
IngestToken string `toml:"ingest_token"`
// Channel is the Matrix channel name (a key in [matrix.channels]) that
// PRIORITY adventure beats post to live. If it isn't a configured Matrix
// channel, adventure runs website-only (stories still appear on /adventure).
Channel string `toml:"channel"`
// DigestHour is the UTC hour [0,23] the daily bulletin digest posts. Default 17.
DigestHour int `toml:"digest_hour"`
}
// WebConfig controls the read-only HTTP interface (news.parodia.dev style).
@@ -228,6 +246,21 @@ func (c *Config) validate() error {
}
}
if c.Adventure.Enabled {
if c.Adventure.IngestToken == "" {
return fmt.Errorf("adventure.ingest_token is required when adventure is enabled (the bearer secret gogobee presents)")
}
if c.Adventure.DigestHour < 0 || c.Adventure.DigestHour > 23 {
return fmt.Errorf("adventure.digest_hour must be 0-23")
}
if c.Adventure.Channel != "" {
if _, ok := c.Matrix.Channels[c.Adventure.Channel]; !ok {
slog.Warn("adventure.channel is not a configured Matrix channel — adventure runs website-only",
"channel", c.Adventure.Channel)
}
}
}
for i, s := range c.Sources {
if s.Name == "" {
return fmt.Errorf("sources[%d].name is required", i)
@@ -299,6 +332,9 @@ func (c *Config) applyDefaults() {
if c.Web.Push.MinStories == 0 {
c.Web.Push.MinStories = 3
}
if c.Adventure.DigestHour == 0 {
c.Adventure.DigestHour = 17
}
for i := range c.Sources {
if c.Sources[i].PollIntervalMinutes == 0 {
c.Sources[i].PollIntervalMinutes = 20