news: tell Pete who's out there right now, not just who died

We only ever told Pete about outcomes. Nothing emitted when an expedition
*started*, which is why the two bored adventurers walked into dungeons and the
news feed said nothing at all — it wasn't broken, it had nothing to say.

Two halves:

A roster snapshot, pushed every 2 minutes. Deliberately NOT on the durable fact
queue: a fact is history and losing it loses it forever, so it retries. A
snapshot is a photograph of the present, and a retried one is a lie — by the
time it lands, she's moved. The next tick carries the truth. That's also what
lets Pete's staleness timer work: if we stay down, nothing arrives, and the
board stops claiming to be live instead of insisting forever that Josie is
still in holymachina.

And a "departure" bulletin when a bored adventurer lets itself out.

The snapshot omits opted-out players rather than anonymizing them, and carries a
board token distinct from every event token, so a standing row can't become the
key that links a player's dispatches back together.

The player_meta scan folds last_player_action_at/created_at in Go instead of
COALESCE()ing in SQL — modernc rebuilds time.Time from the declared column type
and COALESCE erases it. A failed scan here would publish an empty board and
every adventurer would vanish from the page.
This commit is contained in:
prosolis
2026-07-13 18:05:49 -07:00
parent 1cbd68a718
commit ced75786b9
6 changed files with 360 additions and 1 deletions

View File

@@ -268,6 +268,35 @@ func claimRealmFirst(kind, target string) bool {
// zone_first would just echo TwinBee back at the same people. Bulletin still
// gets the story onto the site and into the daily digest, where it reads as a
// roundup rather than a repeat.
// emitBoredomDeparture announces that an adventurer got restless and let itself
// out. The one thing gogobee never used to tell Pete was that an expedition
// *started* — every dispatch was an outcome — which is why the two live boredom
// runs produced no news at all.
//
// The event_type must be one Pete already knows: an unknown type is a 400, which
// retries and then parks the bulletin forever. Deploy Pete first.
func emitBoredomDeparture(userID id.UserID, zone ZoneDefinition, level int) {
if !peteclient.Enabled() || !newsEmissionOn() {
return
}
name := charName(userID)
if name == "" {
return
}
ts := nowUnix()
disc := fmt.Sprintf("departure:%s:%d", zone.ID, ts)
emitFact(peteclient.Fact{
GUID: fmt.Sprintf("departure:%s:%s:%d", eventToken(userID, disc), zone.ID, ts),
EventType: "departure",
Tier: "bulletin",
Subject: name,
Zone: zone.Display,
Level: level,
Outcome: "departed",
OccurredAt: ts,
}, userID, "")
}
func emitZoneClearNews(userID id.UserID, exp *Expedition) {
if !peteclient.Enabled() || !newsEmissionOn() {
return