PURPOSE

Admin/debug tooling screen for beta management. Surfaces three destructive/privileged operations against the cloud save state: granting gems to any player, resetting pity counters on banners, and wiping a player’s cloud save. All operations are gated behind server-side admin role checks and are unreachable for non-admin profiles (the screen renders an “Admin access required” notice if the local profile lacks the admin role).

OWNS

  • Local form state for three independent admin actions (grant gems, reset pity, wipe save), each with its own target input, busy flag, and result badge.
  • A resolveTarget helper that maps blank input or the literal string self to the current admin’s player ID, otherwise passes the trimmed input through unchanged.
  • A local ResultBadge component that renders success/error feedback inline below each action button.
  • A local styles record holding the screen’s inline CSS (title, card chrome, input rows, action button, hint text).
  • The visual layout for the admin panel (player ID readout card plus three action cards) inside the standard shell.

READS FROM

  • usePlayerStore — selects profile to read the current player’s id and role. The screen rejects rendering its tools unless profile.role === 'admin'.

PUSHES TO

  • invokeRpc from ../services/supabase — calls three Supabase RPC endpoints:
    • admin_grant_gems with p_target_player_id and p_amount.
    • admin_reset_pity with p_target_player_id and p_banner_id (null for all banners).
    • admin_wipe_save with p_target_player_id.
  • All RPC calls re-verify the caller’s admin role server-side; the screen does not bypass server authorization.

DOES NOT

  • Does not navigate to or from other screens; routing into the admin screen is gated by ProfileScreen showing the link only to admin profiles.
  • Does not modify the player store, profile, or any local cache; results are reflected in the cloud and re-fetched by other systems as needed.
  • Does not validate target player IDs beyond trimming and substituting self; the server enforces existence and authorization.
  • Does not log telemetry, post to Discord, or write to Supabase tables directly outside the three admin RPCs.
  • Does not poll, subscribe, or refresh after an action; result feedback is one-shot per button press.
  • Does not render anything game-side (no canvas, no engine state).

Signals

  • Success and error results are surfaced via the inline ResultBadge component using green (#34d399) for success and red (#f87171) for error, with truncated target IDs (first 8 chars) in the message.
  • Each action sets its own *Busy flag while the RPC is in flight, swapping the button label to a progress state (GRANTING..., RESETTING..., WIPING...) and disabling the button.
  • The wipe action button uses a distinct red palette and the label WIPE SAVE (DESTRUCTIVE) to mark it as the highest-risk operation.
  • The admin-gate path renders a red “Admin access required” message inside the shell when profile is missing or profile.role !== 'admin'.

Entry points

  • Default export is AdminScreen (named export). Rendered by the metagame screen router when the player has profile.role === 'admin' and navigates from ProfileScreen.
  • No props; the screen reads everything it needs from usePlayerStore.
  • Wrapped in V32Shell (from ../components/V32Shell) for consistent chrome with the rest of the metagame screens.

Pattern notes

  • Three structurally identical async handlers (handleGrantGems, handleResetPity, handleWipeSave) each follow the same pattern: set busy → clear prior result → resolve target → call invokeRpc → set success or error result → clear busy in finally. The duplication is intentional and matches the “three similar lines is fine, no premature abstraction” project rule.
  • resolveTarget is the only shared input-preprocessing helper; numeric parsing for grantAmount is inlined in its handler with a manual isNaN/< 1 rejection.
  • Error messages are err instanceof Error ? err.message : 'Failed' — surfaces the server error string when present, falls back to a generic label.
  • Inline styles are declared once in a module-scope styles record cast to Record<string, React.CSSProperties>; the screen does not depend on a shared theme module.
  • The screen uses non-emoji ASCII labels everywhere except the title heading, which prefixes ADMIN TOOLS with a shield glyph as a visual marker.
  • The wipe button overrides actionBtn background and color via spread to mark the destructive variant rather than introducing a new style key.