Add DEBUG-gated panel to buy any card in the shop

When the server's DEBUG env var is on, expose a card catalog endpoint
and a collapsible client panel that drops any pet or food straight into
your deck (free, off-turn). Gated server-side so it is inert in normal
play.
This commit is contained in:
Greyson Parrelli
2026-07-23 01:07:14 -04:00
parent 71fa20493a
commit b2aa7c5ca3
13 changed files with 282 additions and 7 deletions
+7 -1
View File
@@ -1,4 +1,4 @@
import type { Session } from './types'
import type { Card, Session } from './types'
const SESSION_KEY = 'sapbg-session'
@@ -21,6 +21,12 @@ export function joinGame(code: string, name: string): Promise<Session> {
return post('/api/join', { code, name })
}
export async function fetchCatalog(): Promise<Card[]> {
const res = await fetch('/api/catalog')
if (!res.ok) throw new Error('failed to load catalog')
return (await res.json()) as Card[]
}
export function loadSession(): Session | null {
try {
const raw = localStorage.getItem(SESSION_KEY)
+59
View File
@@ -0,0 +1,59 @@
import { useEffect, useState } from 'react'
import type { Card, ClientMessage } from '../types'
import { fetchCatalog } from '../api'
import { CardView } from './CardView'
interface Props {
canGrant: boolean // shop/cleanup phase — grants only land then
send: (msg: ClientMessage) => void
}
// DebugPanel is a testing aid (server DEBUG mode only): a collapsible drawer
// listing every card in the game, tier by tier. Clicking one drops it into
// your deck for free, off-turn.
export function DebugPanel({ canGrant, send }: Props) {
const [open, setOpen] = useState(false)
const [catalog, setCatalog] = useState<Card[]>([])
useEffect(() => {
fetchCatalog()
.then(setCatalog)
.catch(() => setCatalog([]))
}, [])
const tiers = [...new Set(catalog.map((c) => c.tier ?? 0))].sort((a, b) => a - b)
return (
<div className={`debug-panel ${open ? 'is-open' : ''}`}>
<button className="debug-toggle" onClick={() => setOpen((o) => !o)}>
🐛 {open ? '' : ''} Debug
</button>
{open && (
<div className="debug-body">
<div className="debug-head">
Buy any card
{!canGrant && <span className="muted"> · only in the shop</span>}
</div>
{tiers.map((tier) => (
<div key={tier} className="debug-tier">
<div className="debug-tier-label">Tier {tier}</div>
<div className="debug-grid">
{catalog
.filter((c) => (c.tier ?? 0) === tier)
.map((c) => (
<CardView
key={c.id}
card={c}
size="sm"
disabled={!canGrant}
onClick={canGrant ? () => send({ type: 'debugAdd', name: c.name }) : undefined}
/>
))}
</div>
</div>
))}
</div>
)}
</div>
)
}
+7
View File
@@ -5,6 +5,7 @@ import { ShopPhase } from './ShopPhase'
import { ArrangePhase } from './ArrangePhase'
import { BattlePhase } from './BattlePhase'
import { GameOver } from './GameOver'
import { DebugPanel } from './DebugPanel'
// Table connects to the game and routes to the right phase screen.
export function Table({ session, onLeave }: { session: Session; onLeave: () => void }) {
@@ -69,6 +70,12 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
<div className="banner banner-warn">An opponent is disconnected</div>
)}
{error && <div className="toast">{error}</div>}
{view.debug && (
<DebugPanel
canGrant={view.phase === 'shop' || view.phase === 'cleanup'}
send={send}
/>
)}
</div>
)
}
+63
View File
@@ -1276,3 +1276,66 @@ h3 {
height: 110px;
}
}
/* --- debug panel (server DEBUG mode) --- */
.debug-panel {
position: fixed;
top: 64px;
right: 0;
z-index: 120;
display: flex;
align-items: flex-start;
}
.debug-toggle {
background: #7a1f1f;
color: var(--cream);
border: 2px solid var(--cocoa);
border-right: none;
border-radius: 8px 0 0 8px;
font-weight: 800;
font-size: 0.8rem;
padding: 8px 10px;
cursor: pointer;
white-space: nowrap;
writing-mode: vertical-rl;
transform: rotate(180deg);
}
.debug-panel.is-open .debug-toggle {
writing-mode: horizontal-tb;
transform: none;
border-radius: 8px 0 0 0;
}
.debug-body {
width: min(340px, 80vw);
max-height: calc(100vh - 80px);
overflow-y: auto;
background: rgba(18, 53, 31, 0.97);
border: 2px solid var(--cocoa);
border-radius: 8px 0 0 8px;
padding: 12px;
box-shadow: -6px 6px 18px rgba(0, 0, 0, 0.45);
}
.debug-head {
color: var(--gold);
font-weight: 800;
margin-bottom: 8px;
}
.debug-tier-label {
color: var(--cream);
font-size: 0.75rem;
font-weight: 700;
opacity: 0.8;
margin: 8px 0 4px;
}
.debug-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
+2
View File
@@ -89,6 +89,7 @@ export interface GameView {
pending?: PendingTrade
battle?: BattleResult
winnerSeat: number
debug?: boolean // server DEBUG mode: unlocks the buy-any-card panel
}
export type ClientMessage =
@@ -99,6 +100,7 @@ export type ClientMessage =
| { type: 'pass' }
| { type: 'arrange'; order: string[] }
| { type: 'ready' }
| { type: 'debugAdd'; name: string }
export interface Session {
gameId: string