adventure: let a player back out from the web, not only in Matrix

Three verbs to match gogobee's: call off an expedition, turn back out of
somebody else's party, send the pet sitter home. Which one this page
offers is derived here rather than pushed — leadership is already legible
in the party seats and the sitter's standing is already in the babysit
offer, so nothing new crosses the wire.

Two things running it turned up that no test would have. An applied
abandon left "Pull out of the run" sitting under a verdict saying the
expedition was over, so an applied verb now also hides the other verbs it
just made untrue. And a party member was being offered that same button
in the first place, beside the one that actually works — Pete knows from
the seat it just read that gogobee would refuse it, so it is withheld.

Also: heal the Matrix handle onto push rows stored before the column
existed, on its own endpoint rather than through the subscribe upsert,
which resets both watermarks and would have silenced the digest for
anybody who reads the site regularly. And stack the board row below sm —
four flex columns that wrapped to six lines on a phone, pre-existing.
This commit is contained in:
prosolis
2026-07-24 21:42:59 -07:00
parent 0d8dba90df
commit b07abc1d13
15 changed files with 780 additions and 30 deletions
+41
View File
@@ -59,6 +59,47 @@ func (s *Server) handlePushSubscribe(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// handlePushHeal fills in the Matrix handle on a subscription stored before the
// column existed. W6 shipped owner-scoped adventure alerts keyed on the
// localpart, and every row that predates it carries an empty one — so those
// subscribers get the realm-wide Siege alerts and silently never get the ones
// about their own adventurer. Nothing in the browser re-subscribes on its own
// (pwa.js only calls subscribe() on a click), so without this they stay broken
// until they happen to toggle notifications off and on again.
//
// It takes only an endpoint, and it is deliberately not a subscribe: see
// HealPushSubscriptionLocalpart on why re-using the upsert here would have
// silenced the digest for anybody who reads the site regularly.
func (s *Server) handlePushHeal(w http.ResponseWriter, r *http.Request) {
u := s.requireUser(w, r)
if u == nil {
return
}
if !s.cfg.Push.Enabled {
http.Error(w, `{"error":"push disabled"}`, http.StatusNotFound)
return
}
var req struct {
Endpoint string `json:"endpoint"`
}
if !decodeStateBodyN(w, r, &req, maxPushBodyBytes) {
return
}
if req.Endpoint == "" {
http.Error(w, `{"error":"incomplete subscription"}`, http.StatusBadRequest)
return
}
// 204 whether or not a row moved. The client asks once per endpoint and has
// nothing to do with the answer, and reporting a miss would tell a caller
// whether somebody else's endpoint is on file.
if err := storage.HealPushSubscriptionLocalpart(u.Sub, req.Endpoint, buyerLocalpart(u)); err != nil {
slog.Error("push: heal failed", "sub", u.Sub, "err", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// handlePushUnsubscribe drops the caller's own stored subscription by endpoint.
// The delete is scoped to the signed-in user so one account can't remove
// another's subscription by presenting its endpoint string.