adventure: gogobee pushes each adventurer's sheet to Pete's detail page

The board on Pete shows flat rows; this hands it two more channels so a name
can become a page. Public stats + equipped gear ride the roster snapshot
(RosterDetail on each entry, keyed by the anonymous token, no handle). The
private self-view — inventory, vault, house, pets — rides its own push keyed
by localpart, so Pete only ever serves it back to the one signed-in owner it
belongs to; the board token rides along so the ownership check is a join, never
a reversal of the one-way token. The private set skips no one for opt-out (that
governs the public board only) and skips the dead (no live page to own).
This commit is contained in:
prosolis
2026-07-14 22:22:39 -07:00
parent ba306f0b59
commit 11bfce780c
3 changed files with 492 additions and 0 deletions

View File

@@ -245,6 +245,39 @@ type RosterEntry struct {
Region string `json:"region,omitempty"`
Day int `json:"day,omitempty"`
IdleHours int `json:"idle_hours,omitempty"`
// Detail is the public, expanded sheet — stats and equipped gear — shown on
// the click-through detail page. Nil for an entry we couldn't fully load.
// Public-tier: no Matrix handle, keyed only by the anonymous Token, same as
// the summary fields above.
Detail *RosterDetail `json:"detail,omitempty"`
}
// RosterDetail is everything a visitor may see on an adventurer's detail page:
// the current combat sheet and equipped gear, plus live expedition context. It
// rides the roster push because it is small and shares the board's semantics —
// a photograph of the present, fine to drop and refresh, never a handle.
type RosterDetail struct {
HPCurrent int `json:"hp_current"`
HPMax int `json:"hp_max"`
TempHP int `json:"temp_hp,omitempty"`
ArmorClass int `json:"armor_class"`
Abilities [6]int `json:"abilities"` // STR, DEX, CON, INT, WIS, CHA scores
Modifiers [6]int `json:"modifiers"` // matching ability modifiers
Gear []GearItem `json:"gear,omitempty"`
// Expedition context, present only while on a run.
Supplies int `json:"supplies,omitempty"`
ThreatLevel int `json:"threat_level,omitempty"`
Room string `json:"room,omitempty"`
}
// GearItem is one equipped piece for the armor/gear panel.
type GearItem struct {
Slot string `json:"slot"` // weapon | armor | helmet | boots | tool
Name string `json:"name"`
Tier int `json:"tier"`
Condition int `json:"condition"`
Masterwork bool `json:"masterwork,omitempty"`
}
// MischiefBalance is one buyer's advisory euro balance, ridden along with the
@@ -303,6 +336,71 @@ func PushRoster(ctx context.Context, snap RosterSnapshot) error {
return std.post(ctx, "/api/ingest/roster", payload)
}
// PlayerDetail is the private, owner-only expansion for one player: inventory,
// vault, house, and pets. Like MischiefBalance it is keyed by localpart (the
// sign-in name), in its own keyspace on Pete — Pete only ever serves it back to
// the one authenticated user it belongs to, never on the public board. It
// carries the player's current board Token too, so Pete can answer "is the
// signed-in viewer the owner of the adventurer on this page?" by a join, without
// ever having to reverse the one-way token.
type PlayerDetail struct {
Localpart string `json:"localpart"`
Token string `json:"token"`
Inventory []ItemView `json:"inventory,omitempty"`
Vault []ItemView `json:"vault,omitempty"`
House HouseView `json:"house"`
Pets []PetView `json:"pets,omitempty"`
}
// ItemView is one backpack or vault item for the private inventory panel.
type ItemView struct {
Name string `json:"name"`
Type string `json:"type"`
Tier int `json:"tier"`
Value int64 `json:"value"`
Temper int `json:"temper,omitempty"`
}
// HouseView is the owner's housing summary.
type HouseView struct {
Tier int `json:"tier"`
LoanBalance int `json:"loan_balance,omitempty"`
Autopay bool `json:"autopay,omitempty"`
Rate float64 `json:"rate,omitempty"`
}
// PetView is one pet slot.
type PetView struct {
Type string `json:"type"`
Name string `json:"name"`
Level int `json:"level"`
XP int `json:"xp,omitempty"`
ArmorTier int `json:"armor_tier,omitempty"`
}
// DetailSnapshot is the complete private-detail set, pushed whole and replacing
// Pete's copy — same complete-snapshot contract as the roster, so a player who
// drops out of the game stops having a stale self-view on Pete.
type DetailSnapshot struct {
SnapshotAt int64 `json:"snapshot_at"`
Players []PlayerDetail `json:"players"`
}
// PushDetails sends the private self-detail set to Pete, best-effort. Like the
// roster it is dropped on failure — the next tick carries a fresher copy — and
// it rides its own endpoint (not the roster body) so a fat inventory can't blow
// the roster push's size budget or its drop-the-lie semantics.
func PushDetails(ctx context.Context, snap DetailSnapshot) error {
if !Enabled() {
return nil
}
payload, err := json.Marshal(snap)
if err != nil {
return err
}
return std.post(ctx, "/api/ingest/detail", payload)
}
// post sends one payload to a Pete endpoint with bearer auth. Mirrors the
// bearer-POST pattern in email_nag.go:sendCode.
func (c *Client) post(ctx context.Context, path string, payload []byte) error {