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 { DevCropTool } from './DevCropTool'
// Debug flag: show technical info in magnifier (dev only)
const SHOW_MAGNIFIER_DEBUG_INFO = process.env.NODE_ENV === 'development'
// Debug flag: show technical info in magnifier (gated by isVisualDebugEnabled at runtime)
const SHOW_MAGNIFIER_DEBUG_INFO = true
// Debug flag: show bounding boxes with importance scores (dev only)
const SHOW_DEBUG_BOUNDING_BOXES = process.env.NODE_ENV === 'development'
// Debug flag: show bounding boxes with importance scores (gated by isVisualDebugEnabled at runtime)
const SHOW_DEBUG_BOUNDING_BOXES = true
// Debug flag: show safe zone rectangles (leftover area and crop region) - dev only
const SHOW_SAFE_ZONE_DEBUG = process.env.NODE_ENV === 'development'
// Debug flag: show safe zone rectangles (gated by isVisualDebugEnabled at runtime)
const SHOW_SAFE_ZONE_DEBUG = true
// Precision mode threshold: screen pixel ratio that triggers pointer lock recommendation
const PRECISION_MODE_THRESHOLD = 20
@ -589,7 +589,7 @@ export function MapRenderer({
const { resolvedTheme } = useTheme()
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()
// Effective debug flags - combine prop with context
@ -1444,7 +1444,15 @@ export function MapRenderer({
})
return result
}, [otherPlayerCursors, viewerId, gameMode, currentPlayer, localPlayerId, playerMetadata, memberPlayers])
}, [
otherPlayerCursors,
viewerId,
gameMode,
currentPlayer,
localPlayerId,
playerMetadata,
memberPlayers,
])
// State for give-up zoom animation target values
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 */}
{/* Cursor rendering debug - only log when cursor count changes */}
{svgRef.current &&
@ -5872,7 +5975,10 @@ export function MapRenderer({
}
}
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
}

View File

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

View File

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