mischief: a storefront where money buys a stranger some trouble
The web half of Mischief Makers M3. A signed-in buyer picks a mark off the anonymous roster board and pays for a monster to find them; gogobee does the real work and hands back a verdict Pete files against the order. - mischief_orders: intent in, verdict out, idempotent on a guid that is the end-to-end key gogobee passes to DebitIdem and stamps on the contract - user_euro + mischief_tiers: advisory balance and the live price list, pushed on the roster tick so the storefront never hardcodes a number that can drift - OIDC-gated buy API (target + tier + signed), bearer-authed poll/claim wire - roster board grows a 'send trouble' button, a tier picker, and a status panel Pete never touches money and never runs a game rule. It records what a buyer wants and what gogobee said happened.
This commit is contained in:
@@ -41,13 +41,26 @@ const (
|
||||
)
|
||||
|
||||
// rosterPush is the payload gogobee POSTs to /api/ingest/roster.
|
||||
//
|
||||
// Balances ride the same tick as the board but live in a separate keyspace: they
|
||||
// are keyed by localpart (a buyer's own sign-in name), not by the anonymous
|
||||
// roster token, and Pete only ever reads one back for the authenticated user
|
||||
// asking about themselves. So the board stays anonymous while the storefront can
|
||||
// still grey out tiers a buyer plainly can't afford.
|
||||
type rosterPush struct {
|
||||
SnapshotAt int64 `json:"snapshot_at"`
|
||||
Adventurers []storage.RosterEntry `json:"adventurers"`
|
||||
SnapshotAt int64 `json:"snapshot_at"`
|
||||
Adventurers []storage.RosterEntry `json:"adventurers"`
|
||||
Balances []storage.MischiefBalance `json:"balances,omitempty"`
|
||||
Tiers []storage.MischiefTier `json:"tiers,omitempty"`
|
||||
}
|
||||
|
||||
// RosterView is one row as the page renders it.
|
||||
//
|
||||
// Token is the mark's anonymous roster token, exposed so the storefront can name
|
||||
// a target without ever knowing their real handle. It is safe on a public page by
|
||||
// design — non-reversible, stable, and already how gogobee keys the board.
|
||||
type RosterView struct {
|
||||
Token string
|
||||
Name string
|
||||
Level int
|
||||
ClassRace string
|
||||
@@ -103,7 +116,21 @@ func (s *Server) handleRosterIngest(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
slog.Info("roster ingest: board replaced", "adventurers", len(push.Adventurers))
|
||||
// Advisory balances are best-effort: a board that landed is worth keeping
|
||||
// even if the balances behind it didn't, so a failure here is logged, not
|
||||
// fatal. Stale affordability only ever bounces an order, never miscounts money.
|
||||
if err := storage.ReplaceUserEuro(push.Balances, push.SnapshotAt); err != nil {
|
||||
slog.Error("roster ingest: balance replace failed", "err", err)
|
||||
}
|
||||
// The tier catalog is likewise best-effort and only refreshed when the push
|
||||
// actually carried one, so a gogobee build that predates the storefront (no
|
||||
// tiers field) can't wipe a catalog a newer one already established.
|
||||
if len(push.Tiers) > 0 {
|
||||
if err := storage.ReplaceMischiefTiers(push.Tiers); err != nil {
|
||||
slog.Error("roster ingest: tier replace failed", "err", err)
|
||||
}
|
||||
}
|
||||
slog.Info("roster ingest: board replaced", "adventurers", len(push.Adventurers), "balances", len(push.Balances))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -142,6 +169,7 @@ func (s *Server) roster() (views []RosterView, stale bool, snapshotAt int64) {
|
||||
|
||||
func toRosterView(e storage.RosterEntry) RosterView {
|
||||
v := RosterView{
|
||||
Token: e.Token,
|
||||
Name: e.Name,
|
||||
Level: e.Level,
|
||||
ClassRace: e.ClassRace,
|
||||
|
||||
Reference in New Issue
Block a user