My Sea iter 6c: bud-btn invite stub + #id_my_sea_menu gear (NVM-only, %applet-menu-styled, on both /gameboard/my-sea/ and the gatekeeper) + PAID DRAW now deletes the row and redirects to ?phase=picker so the user drops straight into picking cards instead of looping back to GATE VIEW — Sprint 5 iter 6c of My Sea roadmap — TDD

Bundled fix for the PAID-DRAW-loops-to-GATE-VIEW bug surfaced 2026-05-20 in
live testing: previously the view reset `created_at = now()` + cleared the
hand, but the row's continued existence meant `quota_spent=True` on the
next render → landing rendered GATE VIEW → user clicked it → back to
gatekeeper → loop.

Now PAID DRAW does `active_draw.delete()` after debiting the token + then
redirects to `/gameboard/my-sea/?phase=picker`. The my_sea view honors
`?phase=picker` (only when no active_draw exists — can't bypass
post-DEL GATE VIEW) by forcing `show_picker=True` so the user lands in
the picker ready to draw. First card draw creates a fresh row w. fresh
`created_at`, starting the new 24h quota cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-20 09:47:47 -04:00
parent 1e37fe1475
commit 4417b8c972
9 changed files with 267 additions and 41 deletions

View File

@@ -219,7 +219,18 @@ def my_sea(request):
# (the daily quota's spent already; landing's primary nav routes to
# the upcoming gatekeeper). New users + post-24h users land on the
# standard FREE DRAW landing.
show_picker = active_draw is not None and not hand_empty
#
# `?phase=picker` query param (set by PAID DRAW's redirect) forces
# the picker even when active_draw is None — the user just paid a
# token, so drop them straight into the picker rather than making
# them click FREE DRAW first. Only honored when active_draw is None
# (post-PAID-DRAW state); existing rows route through the normal
# logic above so the param can't accidentally bypass a GATE VIEW
# or empty-hand state.
phase_param = request.GET.get("phase") == "picker"
show_picker = (active_draw is not None and not hand_empty) or (
active_draw is None and phase_param
)
quota_spent = active_draw is not None # any active row = quota committed
# Sprint 6 iter 6b — landing center-btn 3-way + seat-1 persistence.
# `deposit_reserved` toggles the landing primary from GATE VIEW to
@@ -453,11 +464,23 @@ def my_sea_refund_token(request):
@login_required(login_url="/")
@require_POST
def my_sea_paid_draw(request):
"""Commit the deposited token + reset the row for a fresh quota
cycle. The token is debited via `debit_my_sea_token` (FREE/TITHE
consumed; COIN 24h cooldown + unequipped; PASS no-op). Hand wiped,
`created_at` reset to now, deposit fields cleared. User redirects
back to /gameboard/my-sea/ ready to draw a fresh hand."""
"""Commit the deposited token + drop the active_draw row so the
user returns to a fresh "able-to-draw-now" state. Without the row,
`quota_spent` resolves to False on the next my-sea render → the
user can draw cards immediately (the token they just spent earns
them this 24h cycle's worth of draws).
The token is debited via `debit_my_sea_token` (FREE/TITHE consumed;
COIN 24h cooldown + unequipped; PASS no-op). The row is then
deleted (rather than just reset) — user-spec 2026-05-20: keeping
the row but resetting created_at left `quota_spent=True` on the
next view, looping the user back to GATE VIEW. Delete sidesteps
that entirely.
Redirects to /gameboard/my-sea/?phase=picker so the user lands
directly in the picker (skipping the FREE DRAW landing click).
"""
from django.urls import reverse
from apps.lyric.models import Token
active_draw = active_draw_for(request.user)
if active_draw is None or active_draw.deposit_token_id is None:
@@ -473,14 +496,28 @@ def my_sea_paid_draw(request):
active_draw.save(update_fields=["deposit_token_id", "deposit_reserved_at"])
return redirect("my_sea")
debit_my_sea_token(request.user, token)
active_draw.hand = []
active_draw.created_at = timezone.now()
active_draw.deposit_token_id = None
active_draw.deposit_reserved_at = None
active_draw.save(update_fields=[
"hand", "created_at", "deposit_token_id", "deposit_reserved_at",
])
return redirect("my_sea")
active_draw.delete()
return redirect(reverse("my_sea") + "?phase=picker")
@login_required(login_url="/")
@require_POST
def my_sea_invite(request):
"""Sprint 6 iter 6c — bud-btn invite stub on my-sea gatekeeper.
Async multi-user invite is deferred to a later sprint; this endpoint
just returns a Brief banner announcing "coming soon" so the bud-btn
panel has a non-broken success path."""
from django.urls import reverse
return JsonResponse({
"brief": {
"title": "Multiplayer my-sea",
"line_text": "Look!—multiplayer my-sea is coming soon. Stay tuned.",
"post_url": reverse("gameboard"),
"created_at": "",
"kind": "NUDGE",
},
"recipient_display": (request.POST.get("recipient") or "").strip(),
})
def _my_sea_deck_data(user, exclude_id=None):