feat(know-your-world): add hot/cold debug panel and production debug mode

- Add ?debug=1 URL param to unlock debug mode in production
- Debug mode persists in localStorage once unlocked
- Add hot/cold debug panel showing all enable conditions:
  - Assistance level, user toggle, fine pointer, magnifier, mobile dragging
  - Overall enabled/disabled status
  - Current feedback type and target region
- Change debug flags from build-time to runtime gating
- Fix isDevelopment reference in AppNavBar dropdown menu

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-12-02 07:23:47 -06:00
parent 2ce5e180b7
commit 493313a3bb
3 changed files with 157 additions and 20 deletions

View File

@ -53,14 +53,14 @@ import {
import { CelebrationOverlay } from './CelebrationOverlay' import { CelebrationOverlay } from './CelebrationOverlay'
import { DevCropTool } from './DevCropTool' import { DevCropTool } from './DevCropTool'
// Debug flag: show technical info in magnifier (dev only) // Debug flag: show technical info in magnifier (gated by isVisualDebugEnabled at runtime)
const SHOW_MAGNIFIER_DEBUG_INFO = process.env.NODE_ENV === 'development' const SHOW_MAGNIFIER_DEBUG_INFO = true
// Debug flag: show bounding boxes with importance scores (dev only) // Debug flag: show bounding boxes with importance scores (gated by isVisualDebugEnabled at runtime)
const SHOW_DEBUG_BOUNDING_BOXES = process.env.NODE_ENV === 'development' const SHOW_DEBUG_BOUNDING_BOXES = true
// Debug flag: show safe zone rectangles (leftover area and crop region) - dev only // Debug flag: show safe zone rectangles (gated by isVisualDebugEnabled at runtime)
const SHOW_SAFE_ZONE_DEBUG = process.env.NODE_ENV === 'development' const SHOW_SAFE_ZONE_DEBUG = true
// Precision mode threshold: screen pixel ratio that triggers pointer lock recommendation // Precision mode threshold: screen pixel ratio that triggers pointer lock recommendation
const PRECISION_MODE_THRESHOLD = 20 const PRECISION_MODE_THRESHOLD = 20
@ -589,7 +589,7 @@ export function MapRenderer({
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark' const isDark = resolvedTheme === 'dark'
// Visual debug mode from global context (only enabled in dev AND when user toggles it on) // Visual debug mode from global context (enabled when user toggles it on, in dev or with ?debug=1)
const { isVisualDebugEnabled } = useVisualDebugSafe() const { isVisualDebugEnabled } = useVisualDebugSafe()
// Effective debug flags - combine prop with context // Effective debug flags - combine prop with context
@ -1444,7 +1444,15 @@ export function MapRenderer({
}) })
return result return result
}, [otherPlayerCursors, viewerId, gameMode, currentPlayer, localPlayerId, playerMetadata, memberPlayers]) }, [
otherPlayerCursors,
viewerId,
gameMode,
currentPlayer,
localPlayerId,
playerMetadata,
memberPlayers,
])
// State for give-up zoom animation target values // State for give-up zoom animation target values
const [giveUpZoomTarget, setGiveUpZoomTarget] = useState({ const [giveUpZoomTarget, setGiveUpZoomTarget] = useState({
@ -5846,6 +5854,101 @@ export function MapRenderer({
</> </>
)} )}
{/* Hot/Cold Debug Panel - shows enable conditions and current state */}
{isVisualDebugEnabled && (
<div
data-element="hot-cold-debug-panel"
style={{
position: 'absolute',
top: '10px',
right: '10px',
padding: '8px 12px',
background: 'rgba(0, 0, 0, 0.85)',
color: 'white',
fontSize: '11px',
fontFamily: 'monospace',
borderRadius: '6px',
zIndex: 1000,
maxWidth: '280px',
lineHeight: 1.4,
}}
>
<div
style={{
fontWeight: 'bold',
marginBottom: '6px',
borderBottom: '1px solid #444',
paddingBottom: '4px',
}}
>
🔥 Hot/Cold Debug
</div>
{/* Enable conditions */}
<div style={{ marginBottom: '6px' }}>
<div style={{ color: '#888', fontSize: '10px', marginBottom: '2px' }}>
Enable Conditions:
</div>
<div style={{ color: assistanceAllowsHotCold ? '#4ade80' : '#f87171' }}>
{assistanceAllowsHotCold ? '✓' : '✗'} Assistance allows: {assistanceLevel}
</div>
<div style={{ color: hotColdEnabled ? '#4ade80' : '#f87171' }}>
{hotColdEnabled ? '✓' : '✗'} User toggle: {hotColdEnabled ? 'ON' : 'OFF'}
</div>
<div style={{ color: hasAnyFinePointer ? '#4ade80' : '#f87171' }}>
{hasAnyFinePointer ? '✓' : '✗'} Fine pointer (desktop)
</div>
<div style={{ color: showMagnifier ? '#4ade80' : '#f87171' }}>
{showMagnifier ? '✓' : '✗'} Magnifier active
</div>
<div style={{ color: isMobileMapDragging ? '#4ade80' : '#f87171' }}>
{isMobileMapDragging ? '✓' : '✗'} Mobile dragging
</div>
{gameMode === 'turn-based' && (
<div style={{ color: currentPlayer === localPlayerId ? '#4ade80' : '#f87171' }}>
{currentPlayer === localPlayerId ? '✓' : '✗'} Is my turn
</div>
)}
</div>
{/* Overall status */}
<div
style={{
padding: '4px 8px',
borderRadius: '4px',
background:
assistanceAllowsHotCold &&
hotColdEnabled &&
(hasAnyFinePointer || showMagnifier || isMobileMapDragging) &&
(gameMode !== 'turn-based' || currentPlayer === localPlayerId)
? 'rgba(74, 222, 128, 0.2)'
: 'rgba(248, 113, 113, 0.2)',
marginBottom: '6px',
}}
>
<strong>Status: </strong>
{assistanceAllowsHotCold &&
hotColdEnabled &&
(hasAnyFinePointer || showMagnifier || isMobileMapDragging) &&
(gameMode !== 'turn-based' || currentPlayer === localPlayerId)
? '🟢 ENABLED'
: '🔴 DISABLED'}
</div>
{/* Current feedback */}
<div>
<span style={{ color: '#888' }}>Current feedback: </span>
<span style={{ color: '#fbbf24' }}>{hotColdFeedbackType || 'none'}</span>
</div>
{/* Target info */}
<div style={{ marginTop: '4px' }}>
<span style={{ color: '#888' }}>Target region: </span>
<span>{currentPrompt || 'none'}</span>
</div>
</div>
)}
{/* Other players' cursors - show in multiplayer when not exclusively our turn */} {/* Other players' cursors - show in multiplayer when not exclusively our turn */}
{/* Cursor rendering debug - only log when cursor count changes */} {/* Cursor rendering debug - only log when cursor count changes */}
{svgRef.current && {svgRef.current &&
@ -5872,7 +5975,10 @@ export function MapRenderer({
} }
} }
if (!player) { if (!player) {
console.log('[CursorShare] ⚠️ No player found in playerMetadata or memberPlayers for playerId:', position.playerId) console.log(
'[CursorShare] ⚠️ No player found in playerMetadata or memberPlayers for playerId:',
position.playerId
)
return null return null
} }

View File

@ -73,7 +73,7 @@ function MenuContent({
}) { }) {
const isDark = resolvedTheme === 'dark' const isDark = resolvedTheme === 'dark'
const { open: openDeploymentInfo } = useDeploymentInfo() const { open: openDeploymentInfo } = useDeploymentInfo()
const { isVisualDebugEnabled, toggleVisualDebug, isDevelopment } = useVisualDebug() const { isVisualDebugEnabled, toggleVisualDebug, isDebugAllowed } = useVisualDebug()
const linkStyle = { const linkStyle = {
display: 'flex', display: 'flex',
@ -317,8 +317,8 @@ function MenuContent({
<ThemeToggle /> <ThemeToggle />
</div> </div>
{/* Developer Section - only in development */} {/* Developer Section - shown in dev or when ?debug=1 is used */}
{isDevelopment && ( {isDebugAllowed && (
<> <>
<div style={separatorStyle} /> <div style={separatorStyle} />
<div style={sectionHeaderStyle}>Developer</div> <div style={sectionHeaderStyle}>Developer</div>
@ -441,8 +441,8 @@ function MenuContent({
<ThemeToggle /> <ThemeToggle />
</DropdownMenu.Item> </DropdownMenu.Item>
{/* Developer Section - only in development */} {/* Developer Section - shown in dev or when ?debug=1 is used */}
{isDevelopment && ( {isDebugAllowed && (
<> <>
<DropdownMenu.Separator style={separatorStyle} /> <DropdownMenu.Separator style={separatorStyle} />
<div style={sectionHeaderStyle}>Developer</div> <div style={sectionHeaderStyle}>Developer</div>

View File

@ -3,23 +3,48 @@
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from 'react' import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from 'react'
const STORAGE_KEY = 'visual-debug-enabled' const STORAGE_KEY = 'visual-debug-enabled'
const PRODUCTION_DEBUG_KEY = 'allow-production-debug'
interface VisualDebugContextType { interface VisualDebugContextType {
/** Whether visual debug elements are enabled (only functional in development) */ /** Whether visual debug elements are enabled */
isVisualDebugEnabled: boolean isVisualDebugEnabled: boolean
/** Toggle visual debug mode on/off */ /** Toggle visual debug mode on/off */
toggleVisualDebug: () => void toggleVisualDebug: () => void
/** Whether we're in development mode (visual debug toggle only shows in dev) */ /** Whether we're in development mode */
isDevelopment: boolean isDevelopment: boolean
/** Whether debug mode is allowed (dev mode OR production debug unlocked) */
isDebugAllowed: boolean
} }
const VisualDebugContext = createContext<VisualDebugContextType | null>(null) const VisualDebugContext = createContext<VisualDebugContextType | null>(null)
export function VisualDebugProvider({ children }: { children: ReactNode }) { export function VisualDebugProvider({ children }: { children: ReactNode }) {
const [isEnabled, setIsEnabled] = useState(false) const [isEnabled, setIsEnabled] = useState(false)
const [productionDebugAllowed, setProductionDebugAllowed] = useState(false)
const isDevelopment = process.env.NODE_ENV === 'development' const isDevelopment = process.env.NODE_ENV === 'development'
// Load from localStorage on mount // Check for production debug unlock via URL param or localStorage
useEffect(() => {
if (typeof window === 'undefined') return
// Check URL param: ?debug=1 or ?debug=true
const urlParams = new URLSearchParams(window.location.search)
const debugParam = urlParams.get('debug')
if (debugParam === '1' || debugParam === 'true') {
// Unlock production debug permanently
localStorage.setItem(PRODUCTION_DEBUG_KEY, 'true')
setProductionDebugAllowed(true)
return
}
// Check localStorage for previously unlocked
const stored = localStorage.getItem(PRODUCTION_DEBUG_KEY)
if (stored === 'true') {
setProductionDebugAllowed(true)
}
}, [])
// Load debug enabled state from localStorage on mount
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
const stored = localStorage.getItem(STORAGE_KEY) const stored = localStorage.getItem(STORAGE_KEY)
@ -38,8 +63,11 @@ export function VisualDebugProvider({ children }: { children: ReactNode }) {
setIsEnabled((prev) => !prev) setIsEnabled((prev) => !prev)
}, []) }, [])
// Only enable visual debug in development mode // Debug is allowed in development OR if production debug is unlocked
const isVisualDebugEnabled = isDevelopment && isEnabled const isDebugAllowed = isDevelopment || productionDebugAllowed
// Enable visual debug if allowed AND user has toggled it on
const isVisualDebugEnabled = isDebugAllowed && isEnabled
return ( return (
<VisualDebugContext.Provider <VisualDebugContext.Provider
@ -47,6 +75,7 @@ export function VisualDebugProvider({ children }: { children: ReactNode }) {
isVisualDebugEnabled, isVisualDebugEnabled,
toggleVisualDebug, toggleVisualDebug,
isDevelopment, isDevelopment,
isDebugAllowed,
}} }}
> >
{children} {children}
@ -69,10 +98,12 @@ export function useVisualDebug(): VisualDebugContextType {
export function useVisualDebugSafe(): VisualDebugContextType { export function useVisualDebugSafe(): VisualDebugContextType {
const context = useContext(VisualDebugContext) const context = useContext(VisualDebugContext)
if (!context) { if (!context) {
const isDev = process.env.NODE_ENV === 'development'
return { return {
isVisualDebugEnabled: false, isVisualDebugEnabled: false,
toggleVisualDebug: () => {}, toggleVisualDebug: () => {},
isDevelopment: process.env.NODE_ENV === 'development', isDevelopment: isDev,
isDebugAllowed: isDev,
} }
} }
return context return context