feat(vision): add activeCameraSource tracking and simplify calibration UI

- Add explicit activeCameraSource field to VisionConfig to track which
  camera is in use (local vs phone), fixing button visibility bugs when
  switching between camera sources
- Simplify calibration UI by removing the confusing "Auto/Manual" mode
  toggle, replacing with a cleaner crop status indicator
- Remove calibration requirement from isVisionSetupComplete for local
  camera since auto-crop runs continuously when markers are detected
- Update DockedVisionFeed to use activeCameraSource instead of inferring
  from which configs are set

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2026-01-01 18:01:12 -06:00
parent 70b363ce88
commit 1be6151bae
13 changed files with 15121 additions and 8341 deletions

View File

@ -449,7 +449,20 @@
"Bash(apps/web/src/lib/vision/frameProcessor.ts )",
"Bash(apps/web/src/lib/vision/beadDetector.ts )",
"Bash(apps/web/public/models/abacus-column-classifier/model.json )",
"Bash(.claude/settings.local.json)"
"Bash(.claude/settings.local.json)",
"Bash(apps/web/src/components/MyAbacus.tsx )",
"Bash(apps/web/src/contexts/MyAbacusContext.tsx )",
"Bash(apps/web/src/components/vision/DockedVisionFeed.tsx )",
"Bash(apps/web/src/components/vision/VisionIndicator.tsx )",
"Bash(apps/web/src/components/vision/VisionSetupModal.tsx)",
"Bash(npx storybook:*)",
"Bash(apps/web/src/hooks/usePhoneCamera.ts )",
"Bash(apps/web/src/lib/remote-camera/session-manager.ts )",
"Bash(apps/web/src/test/setup.ts )",
"Bash(apps/web/src/hooks/__tests__/useRemoteCameraDesktop.test.ts )",
"Bash(apps/web/src/hooks/__tests__/useRemoteCameraPhone.test.ts )",
"Bash(apps/web/src/lib/remote-camera/__tests__/)",
"Bash(packages/abacus-react/CHANGELOG.md )"
],
"deny": [],
"ask": []

View File

@ -301,7 +301,9 @@ export function AbacusDisplayDropdown({
step="1"
value={config.physicalAbacusColumns}
onChange={(e) =>
updateConfig({ physicalAbacusColumns: parseInt(e.target.value, 10) })
updateConfig({
physicalAbacusColumns: parseInt(e.target.value, 10),
})
}
className={css({
flex: 1,

View File

@ -29,6 +29,8 @@ export interface VisionConfigurationChange {
calibration?: import('@/types/vision').CalibrationGrid | null
/** Remote camera session ID (for phone camera) */
remoteCameraSessionId?: string | null
/** Active camera source (tracks which camera is in use) */
activeCameraSource?: CameraSource | null
}
export interface AbacusVisionBridgeProps {
@ -172,23 +174,26 @@ export function AbacusVisionBridge({
if (source === 'phone') {
// Stop local camera
vision.disable()
// Clear local camera config in parent context
onConfigurationChange?.({ cameraDeviceId: null, calibration: null })
// Set active camera source to phone and clear local camera config
// (but keep local config in storage for when we switch back)
onConfigurationChange?.({ activeCameraSource: 'phone' })
// Check for persisted session and reuse it
const persistedSessionId = remoteGetPersistedSessionId()
if (persistedSessionId) {
console.log('[AbacusVisionBridge] Reusing persisted remote session:', persistedSessionId)
setRemoteCameraSessionId(persistedSessionId)
// Notify parent about the reused session
onConfigurationChange?.({ remoteCameraSessionId: persistedSessionId })
onConfigurationChange?.({
remoteCameraSessionId: persistedSessionId,
})
}
// If no persisted session, RemoteCameraQRCode will create one
} else {
// Switching to local camera
// Clear remote session in context so DockedVisionFeed uses local camera
// The session still persists in localStorage (via useRemoteCameraDesktop) for when we switch back
// Set active camera source to local
// The remote session still persists in localStorage (via useRemoteCameraDesktop) for when we switch back
setRemoteCameraSessionId(null)
onConfigurationChange?.({ remoteCameraSessionId: null })
onConfigurationChange?.({ activeCameraSource: 'local' })
vision.enable()
}
},
@ -284,7 +289,7 @@ export function AbacusVisionBridge({
}
}, [vision.cameraError, onError])
// Notify about local camera device changes
// Notify about local camera device changes and ensure activeCameraSource is set
useEffect(() => {
if (
cameraSource === 'local' &&
@ -292,7 +297,11 @@ export function AbacusVisionBridge({
vision.selectedDeviceId !== lastReportedCameraRef.current
) {
lastReportedCameraRef.current = vision.selectedDeviceId
onConfigurationChange?.({ cameraDeviceId: vision.selectedDeviceId })
// Set both the camera device ID and the active camera source
onConfigurationChange?.({
cameraDeviceId: vision.selectedDeviceId,
activeCameraSource: 'local',
})
}
}, [cameraSource, vision.selectedDeviceId, onConfigurationChange])
@ -437,7 +446,10 @@ export function AbacusVisionBridge({
const updateDimensions = () => {
const rect = container.getBoundingClientRect()
if (rect.width > 0 && rect.height > 0) {
setRemoteContainerDimensions({ width: rect.width, height: rect.height })
setRemoteContainerDimensions({
width: rect.width,
height: rect.height,
})
}
}
@ -757,145 +769,170 @@ export function AbacusVisionBridge({
)}
</div>
{/* Calibration mode toggle (both local and phone camera) */}
{/* Crop status - shows either marker detection or manual crop status */}
<div
data-element="calibration-mode"
data-element="crop-status"
className={css({
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
gap: 2,
p: 2,
bg: 'gray.800',
borderRadius: 'md',
})}
>
<span className={css({ color: 'gray.400', fontSize: 'sm' })}>Mode:</span>
<button
type="button"
onClick={() =>
cameraSource === 'local'
? vision.setCalibrationMode('auto')
: handleRemoteModeChange('auto')
}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: 'none',
borderRadius: 'md',
cursor: 'pointer',
bg:
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) === 'auto'
? 'blue.600'
: 'gray.700',
color: 'white',
_hover: {
bg:
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) ===
'auto'
? 'blue.500'
: 'gray.600',
},
})}
>
Auto (Markers)
</button>
<button
type="button"
onClick={() =>
cameraSource === 'local'
? vision.setCalibrationMode('manual')
: handleRemoteModeChange('manual')
}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: 'none',
borderRadius: 'md',
cursor: 'pointer',
bg:
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) ===
'manual'
? 'blue.600'
: 'gray.700',
color: 'white',
_hover: {
bg:
(cameraSource === 'local' ? vision.calibrationMode : remoteCalibrationMode) ===
'manual'
? 'blue.500'
: 'gray.600',
},
})}
>
Manual
</button>
</div>
{/* Marker detection status (in auto mode) */}
{((cameraSource === 'local' && vision.calibrationMode === 'auto') ||
(cameraSource === 'phone' && remoteCalibrationMode === 'auto')) && (
<div
data-element="marker-status"
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
p: 2,
bg:
cameraSource === 'local'
? vision.markerDetection.allMarkersFound
? 'green.900'
: 'gray.800'
: remoteFrameMode === 'cropped'
? 'green.900'
: 'gray.800',
borderRadius: 'md',
transition: 'background-color 0.2s',
})}
>
<div className={css({ display: 'flex', alignItems: 'center', gap: 2 })}>
<span
className={css({
width: '8px',
height: '8px',
borderRadius: 'full',
bg:
cameraSource === 'local'
? vision.markerDetection.allMarkersFound
? 'green.400'
: 'yellow.400'
: remoteFrameMode === 'cropped'
? 'green.400'
: 'yellow.400',
})}
/>
<span className={css({ color: 'white', fontSize: 'sm' })}>
{cameraSource === 'local'
? vision.markerDetection.allMarkersFound
? 'All markers detected'
: `Markers: ${vision.markerDetection.markersFound}/4`
: remoteFrameMode === 'cropped'
? 'Phone auto-cropping active'
: 'Waiting for phone markers...'}
</span>
</div>
<a
href="/create/vision-markers"
target="_blank"
rel="noopener noreferrer"
{/* Manual crop indicator (if set) */}
{((cameraSource === 'local' && vision.isCalibrated) ||
(cameraSource === 'phone' && remoteCalibration)) && (
<div
className={css({
color: 'blue.300',
fontSize: 'xs',
textDecoration: 'underline',
_hover: { color: 'blue.200' },
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
})}
>
Get markers
</a>
</div>
)}
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: 2,
})}
>
<span
className={css({
width: '8px',
height: '8px',
borderRadius: 'full',
bg: 'blue.400',
})}
/>
<span className={css({ color: 'white', fontSize: 'sm' })}>Using manual crop</span>
</div>
<button
type="button"
onClick={() => {
if (cameraSource === 'local') {
vision.resetCalibration()
} else {
handleRemoteResetCalibration()
}
}}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
bg: 'transparent',
color: 'gray.400',
border: '1px solid',
borderColor: 'gray.600',
borderRadius: 'md',
cursor: 'pointer',
_hover: { borderColor: 'gray.500', color: 'gray.300' },
})}
>
Reset
</button>
</div>
)}
{/* Marker detection status (always shown when no manual crop) */}
{!(
(cameraSource === 'local' && vision.isCalibrated) ||
(cameraSource === 'phone' && remoteCalibration)
) && (
<div
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: 2,
})}
>
<span
className={css({
width: '8px',
height: '8px',
borderRadius: 'full',
bg:
cameraSource === 'local'
? vision.markerDetection.allMarkersFound
? 'green.400'
: 'yellow.400'
: remoteFrameMode === 'cropped'
? 'green.400'
: 'yellow.400',
})}
/>
<span className={css({ color: 'white', fontSize: 'sm' })}>
{cameraSource === 'local'
? vision.markerDetection.allMarkersFound
? 'Auto-crop active'
: `Markers: ${vision.markerDetection.markersFound}/4`
: remoteFrameMode === 'cropped'
? 'Phone auto-cropping'
: 'Waiting for markers...'}
</span>
</div>
<div className={css({ display: 'flex', gap: 2 })}>
<a
href="/create/vision-markers"
target="_blank"
rel="noopener noreferrer"
className={css({
color: 'blue.300',
fontSize: 'xs',
textDecoration: 'underline',
_hover: { color: 'blue.200' },
})}
>
Get markers
</a>
</div>
</div>
)}
{/* Manual crop button - only show when not calibrating and no manual calibration */}
{!(
(cameraSource === 'local' && vision.isCalibrated) ||
(cameraSource === 'phone' && remoteCalibration)
) &&
!(cameraSource === 'local' ? vision.isCalibrating : remoteIsCalibrating) && (
<button
type="button"
onClick={() => {
if (cameraSource === 'local') {
vision.setCalibrationMode('manual')
vision.startCalibration()
} else {
handleRemoteModeChange('manual')
handleRemoteStartCalibration()
}
}}
disabled={cameraSource === 'local' ? !vision.videoStream : !remoteIsPhoneConnected}
className={css({
px: 3,
py: 1.5,
fontSize: 'sm',
bg: 'transparent',
color: 'gray.300',
border: '1px solid',
borderColor: 'gray.600',
borderRadius: 'md',
cursor: 'pointer',
_hover: { borderColor: 'gray.500', bg: 'gray.700' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
})}
>
Set crop manually
</button>
)}
</div>
{/* Camera feed */}
<div ref={cameraFeedContainerRef} className={css({ position: 'relative' })}>
@ -1168,134 +1205,6 @@ export function AbacusVisionBridge({
</div>
)}
{/* Actions (manual mode - both local and phone camera) */}
{((cameraSource === 'local' && vision.calibrationMode === 'manual') ||
(cameraSource === 'phone' && remoteCalibrationMode === 'manual')) && (
<div
data-element="actions"
className={css({
display: 'flex',
gap: 2,
})}
>
{cameraSource === 'local' ? (
/* Local camera actions */
!vision.isCalibrated ? (
<button
type="button"
onClick={vision.startCalibration}
disabled={!videoDimensions}
className={css({
flex: 1,
py: 2,
bg: 'blue.600',
color: 'white',
border: 'none',
borderRadius: 'md',
fontWeight: 'medium',
cursor: 'pointer',
_hover: { bg: 'blue.500' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
})}
>
Calibrate
</button>
) : (
<>
<button
type="button"
onClick={vision.startCalibration}
className={css({
flex: 1,
py: 2,
bg: 'gray.700',
color: 'white',
border: 'none',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'gray.600' },
})}
>
Recalibrate
</button>
<button
type="button"
onClick={vision.resetCalibration}
className={css({
py: 2,
px: 3,
bg: 'red.700',
color: 'white',
border: 'none',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'red.600' },
})}
>
Reset
</button>
</>
)
) : /* Phone camera actions */
!remoteCalibration ? (
<button
type="button"
onClick={handleRemoteStartCalibration}
disabled={!remoteIsPhoneConnected}
className={css({
flex: 1,
py: 2,
bg: 'blue.600',
color: 'white',
border: 'none',
borderRadius: 'md',
fontWeight: 'medium',
cursor: 'pointer',
_hover: { bg: 'blue.500' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
})}
>
Calibrate
</button>
) : (
<>
<button
type="button"
onClick={handleRemoteStartCalibration}
className={css({
flex: 1,
py: 2,
bg: 'gray.700',
color: 'white',
border: 'none',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'gray.600' },
})}
>
Recalibrate
</button>
<button
type="button"
onClick={handleRemoteResetCalibration}
className={css({
py: 2,
px: 3,
bg: 'red.700',
color: 'white',
border: 'none',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'red.600' },
})}
>
Reset
</button>
</>
)}
</div>
)}
{/* Instructions */}
{cameraSource === 'local' && !vision.isCalibrated && !vision.isCalibrating && (
<p
@ -1305,9 +1214,7 @@ export function AbacusVisionBridge({
textAlign: 'center',
})}
>
{vision.calibrationMode === 'auto'
? 'Place ArUco markers on your abacus corners for automatic detection'
: 'Point your camera at a soroban and click Calibrate to set up detection'}
Place ArUco markers on your abacus corners, or set the crop manually
</p>
)}
@ -1334,9 +1241,7 @@ export function AbacusVisionBridge({
textAlign: 'center',
})}
>
{remoteCalibrationMode === 'auto'
? 'Place ArUco markers on your abacus corners for automatic detection'
: 'Point your phone camera at a soroban and click Calibrate to set up detection'}
Phone auto-detects ArUco markers, or set the crop manually
</p>
)}

View File

@ -90,10 +90,9 @@ export function DockedVisionFeed({ onValueDetected, columnCount = 5 }: DockedVis
// Stability tracking for detected values (hook must be called unconditionally)
const stability = useFrameStability()
// Determine camera source (must be before effects that use these)
// Prioritize local camera if configured - remote camera only if no local camera
const isLocalCamera = visionConfig.cameraDeviceId !== null
const isRemoteCamera = !isLocalCamera && visionConfig.remoteCameraSessionId !== null
// Determine camera source from explicit activeCameraSource field
const isLocalCamera = visionConfig.activeCameraSource === 'local'
const isRemoteCamera = visionConfig.activeCameraSource === 'phone'
// Load and initialize ArUco on mount (for local camera auto-calibration)
useEffect(() => {

View File

@ -58,8 +58,14 @@ export function RemoteCameraReceiver({
const [calibration, setCalibration] = useState<CalibrationGrid | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const imageRef = useRef<HTMLImageElement>(null)
const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 })
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 })
const [containerDimensions, setContainerDimensions] = useState({
width: 0,
height: 0,
})
const [imageDimensions, setImageDimensions] = useState({
width: 0,
height: 0,
})
// Subscribe when sessionId changes
useEffect(() => {
@ -100,7 +106,10 @@ export function RemoteCameraReceiver({
// Track image dimensions when it loads
const handleImageLoad = useCallback((e: React.SyntheticEvent<HTMLImageElement>) => {
const img = e.currentTarget
setImageDimensions({ width: img.naturalWidth, height: img.naturalHeight })
setImageDimensions({
width: img.naturalWidth,
height: img.naturalHeight,
})
}, [])
// Create image src from base64 data

View File

@ -20,6 +20,7 @@ export function VisionSetupModal() {
setVisionCamera,
setVisionCalibration,
setVisionRemoteSession,
setVisionCameraSource,
dock,
} = useMyAbacus()
@ -27,6 +28,7 @@ export function VisionSetupModal() {
setVisionCamera(null)
setVisionCalibration(null)
setVisionRemoteSession(null)
setVisionCameraSource(null)
setVisionEnabled(false)
}
@ -75,10 +77,14 @@ export function VisionSetupModal() {
if (config.remoteCameraSessionId !== undefined) {
setVisionRemoteSession(config.remoteCameraSessionId)
}
if (config.activeCameraSource !== undefined) {
setVisionCameraSource(config.activeCameraSource)
}
}}
// Start with phone camera selected if remote session is configured but no local camera
// Use saved activeCameraSource if available, otherwise infer from configs
initialCameraSource={
visionConfig.remoteCameraSessionId && !visionConfig.cameraDeviceId ? 'phone' : 'local'
visionConfig.activeCameraSource ??
(visionConfig.remoteCameraSessionId && !visionConfig.cameraDeviceId ? 'phone' : 'local')
}
// Show enable/disable and clear buttons
showVisionControls={true}

View File

@ -12,6 +12,11 @@ import {
} from 'react'
import type { CalibrationGrid } from '@/types/vision'
/**
* Camera source type for vision
*/
export type CameraSourceType = 'local' | 'phone'
/**
* Configuration for abacus vision (camera-based input)
*/
@ -24,6 +29,8 @@ export interface VisionConfig {
calibration: CalibrationGrid | null
/** Remote phone camera session ID (for phone-as-camera mode) */
remoteCameraSessionId: string | null
/** Currently active camera source - tracks which camera is in use */
activeCameraSource: CameraSourceType | null
}
const DEFAULT_VISION_CONFIG: VisionConfig = {
@ -31,6 +38,7 @@ const DEFAULT_VISION_CONFIG: VisionConfig = {
cameraDeviceId: null,
calibration: null,
remoteCameraSessionId: null,
activeCameraSource: null,
}
const VISION_CONFIG_STORAGE_KEY = 'abacus-vision-config'
@ -196,6 +204,8 @@ interface MyAbacusContextValue {
setVisionCalibration: (calibration: CalibrationGrid | null) => void
/** Set the remote camera session ID */
setVisionRemoteSession: (sessionId: string | null) => void
/** Set the active camera source */
setVisionCameraSource: (source: CameraSourceType | null) => void
/** Whether the vision setup modal is open */
isVisionSetupOpen: boolean
/** Open the vision setup modal */
@ -310,12 +320,13 @@ export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
}, [])
// Vision callbacks
// Setup is complete if either:
// - Local camera: has camera device AND calibration
// Setup is complete if an active camera source is set and configured:
// - Local camera: has camera device (calibration is optional - auto-crop works without it)
// - Remote camera: has remote session ID (phone handles calibration)
const isVisionSetupComplete =
(visionConfig.cameraDeviceId !== null && visionConfig.calibration !== null) ||
visionConfig.remoteCameraSessionId !== null
visionConfig.activeCameraSource !== null &&
((visionConfig.activeCameraSource === 'local' && visionConfig.cameraDeviceId !== null) ||
(visionConfig.activeCameraSource === 'phone' && visionConfig.remoteCameraSessionId !== null))
const setVisionEnabled = useCallback((enabled: boolean) => {
setVisionConfig((prev) => {
@ -349,6 +360,14 @@ export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
})
}, [])
const setVisionCameraSource = useCallback((source: CameraSourceType | null) => {
setVisionConfig((prev) => {
const updated = { ...prev, activeCameraSource: source }
saveVisionConfig(updated)
return updated
})
}, [])
const openVisionSetup = useCallback(() => {
setIsVisionSetupOpen(true)
}, [])
@ -407,6 +426,7 @@ export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
setVisionCamera,
setVisionCalibration,
setVisionRemoteSession,
setVisionCameraSource,
isVisionSetupOpen,
openVisionSetup,
closeVisionSetup,

View File

@ -167,10 +167,7 @@ describe('useRemoteCameraDesktop', () => {
})
// Should not emit subscribe
expect(mockSocket.emit).not.toHaveBeenCalledWith(
'remote-camera:subscribe',
expect.anything()
)
expect(mockSocket.emit).not.toHaveBeenCalledWith('remote-camera:subscribe', expect.anything())
})
})

View File

@ -273,7 +273,9 @@ export function useRemoteCameraDesktop(): UseRemoteCameraDesktopReturn {
const unsubscribe = useCallback(() => {
if (!socket || !currentSessionIdRef.current) return
socket.emit('remote-camera:leave', { sessionId: currentSessionIdRef.current })
socket.emit('remote-camera:leave', {
sessionId: currentSessionIdRef.current,
})
currentSessionIdRef.current = null
setCurrentSessionId(null)
// Don't clear persisted session - unsubscribe is for temporary disconnect
@ -293,7 +295,9 @@ export function useRemoteCameraDesktop(): UseRemoteCameraDesktopReturn {
*/
const clearSession = useCallback(() => {
if (socket && currentSessionIdRef.current) {
socket.emit('remote-camera:leave', { sessionId: currentSessionIdRef.current })
socket.emit('remote-camera:leave', {
sessionId: currentSessionIdRef.current,
})
}
currentSessionIdRef.current = null
setCurrentSessionId(null)

View File

@ -137,7 +137,10 @@ export function useRemoteCameraPhone(
// Auto-reconnect to session if we have one
const sessionId = sessionIdRef.current
if (sessionId) {
console.log('[RemoteCameraPhone] Auto-reconnecting to session after socket reconnect:', sessionId)
console.log(
'[RemoteCameraPhone] Auto-reconnecting to session after socket reconnect:',
sessionId
)
socketInstance.emit('remote-camera:join', { sessionId })
setIsConnected(true)
isConnectedRef.current = true

View File

@ -16,7 +16,12 @@ export interface RemoteCameraSession {
phoneConnected: boolean
/** Calibration data sent from desktop (persists for reconnects) */
calibration?: {
corners: { topLeft: { x: number; y: number }; topRight: { x: number; y: number }; bottomLeft: { x: number; y: number }; bottomRight: { x: number; y: number } }
corners: {
topLeft: { x: number; y: number }
topRight: { x: number; y: number }
bottomLeft: { x: number; y: number }
bottomRight: { x: number; y: number }
}
}
}
@ -126,7 +131,9 @@ export function setSessionCalibration(
/**
* Get calibration data from session
*/
export function getSessionCalibration(sessionId: string): RemoteCameraSession['calibration'] | null {
export function getSessionCalibration(
sessionId: string
): RemoteCameraSession['calibration'] | null {
const sessions = getSessions()
const session = sessions.get(sessionId)

View File

@ -1,102 +1,98 @@
# [2.19.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.18.0...abacus-react-v2.19.0) (2026-01-01)
### Features
* **vision:** add physical abacus column setting and fix remote flash toggle ([b206eb3](https://github.com/antialias/soroban-abacus-flashcards/commit/b206eb30712e4b98525a9fa2544c2b5a235a8b72))
* **vision:** improve remote camera calibration and UX ([8846cec](https://github.com/antialias/soroban-abacus-flashcards/commit/8846cece93941a36c187abd4ecee9cc88de0c2ec))
- **vision:** add physical abacus column setting and fix remote flash toggle ([b206eb3](https://github.com/antialias/soroban-abacus-flashcards/commit/b206eb30712e4b98525a9fa2544c2b5a235a8b72))
- **vision:** improve remote camera calibration and UX ([8846cec](https://github.com/antialias/soroban-abacus-flashcards/commit/8846cece93941a36c187abd4ecee9cc88de0c2ec))
# [2.18.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.17.0...abacus-react-v2.18.0) (2026-01-01)
### Bug Fixes
* allow teacher-parents to enroll their children in other classrooms ([52df7f4](https://github.com/antialias/soroban-abacus-flashcards/commit/52df7f469718128fd3d8933941ffb8d4bb8db208))
* **bkt:** handle missing helpLevelUsed in legacy data causing NaN ([b300ed9](https://github.com/antialias/soroban-abacus-flashcards/commit/b300ed9f5cc3bfb0c7b28faafe81c80a59444998))
* **camera:** handle race condition in camera initialization ([2a24700](https://github.com/antialias/soroban-abacus-flashcards/commit/2a24700e6cb6efe0ae35d9ebd6c428e3a1a1a736))
* **classroom:** auto-transition tutorial→session observation + fix NaN display ([962a52d](https://github.com/antialias/soroban-abacus-flashcards/commit/962a52d7562f566e78f6272816b049bf77daa7c9))
* **classroom:** broadcast digit-by-digit answer and correct phase indicator ([fb73e85](https://github.com/antialias/soroban-abacus-flashcards/commit/fb73e85f2daacefafa572e03c16b10fab619ea57))
* **dashboard:** compute skill stats from session results in curriculum API ([11d4846](https://github.com/antialias/soroban-abacus-flashcards/commit/11d48465d710d0293ebf41f64b4fd0f1f03d8bf8))
* **db:** add missing is_paused column to session_plans ([9d8b5e1](https://github.com/antialias/soroban-abacus-flashcards/commit/9d8b5e1148911f881d08d07608debaaef91609c2))
* **db:** add missing journal entries for migrations 0041-0042 ([398603c](https://github.com/antialias/soroban-abacus-flashcards/commit/398603c75a094e28122c5ccdced5b82badc7fbfb))
* **docker:** add canvas native deps for jsdom/vitest ([5f51bc1](https://github.com/antialias/soroban-abacus-flashcards/commit/5f51bc1871aec325feb32a0b29edabb3b6c5dd1f))
* **docker:** override canvas with mock package for Alpine/musl ([8be1995](https://github.com/antialias/soroban-abacus-flashcards/commit/8be19958af624d22fa2c6cb48f5723f5efc820c3))
* **docker:** skip canvas native build (optional jsdom dep) ([d717f44](https://github.com/antialias/soroban-abacus-flashcards/commit/d717f44fccb8ed2baa30499df65784a4b89c6ffc))
* **observer:** seed results panel with full session history ([aab7469](https://github.com/antialias/soroban-abacus-flashcards/commit/aab7469d9ea87c91a0165e4c48a60ac130cdc1b2))
* only show session stats when there are actual problems ([62aefad](https://github.com/antialias/soroban-abacus-flashcards/commit/62aefad6766ba32ad27e8ed3db621a6f77520cbe))
* **practice:** allow teachers to create student profiles ([5fee129](https://github.com/antialias/soroban-abacus-flashcards/commit/5fee1297e1775b5e6133919d179e23b6e70b2518))
* **practice:** always show add student FAB button ([a658414](https://github.com/antialias/soroban-abacus-flashcards/commit/a6584143ebf1f3e5b3c9f3283e690458a06beb60))
* **practice:** real-time progress in observer modal + numeric answer comparison ([c0e63ff](https://github.com/antialias/soroban-abacus-flashcards/commit/c0e63ff68b26fd37eedd657504f7f79e5ce40a10))
* **practice:** show active sessions for teacher's own children ([ece3197](https://github.com/antialias/soroban-abacus-flashcards/commit/ece319738b6ab1882469d79ea24b604316d28b34))
* **practice:** use Next.js Link for student tiles + fix session observer z-index ([6def610](https://github.com/antialias/soroban-abacus-flashcards/commit/6def6108771b427e4885bebd23cecdad7a50efb0))
* **seed:** accurate BKT simulation for developing classifications ([d5e4c85](https://github.com/antialias/soroban-abacus-flashcards/commit/d5e4c858db8866e5177b8fa2317aba42b30171e8))
* **share:** use getShareUrl for correct production URLs ([98a69f1](https://github.com/antialias/soroban-abacus-flashcards/commit/98a69f1f80e465415edce49043e2c019a856f8e5))
* **vision:** fix manual calibration overlay not showing on remote camera ([44dcb01](https://github.com/antialias/soroban-abacus-flashcards/commit/44dcb01473bac00c09dddbbefd77dd26b3a27817))
* **vision:** fix remote camera calibration coordinate system ([e52f94e](https://github.com/antialias/soroban-abacus-flashcards/commit/e52f94e4b476658c41f23668d2941af1288e4ed8))
* **vision:** swap corners diagonally for webcam orientation ([dd8efe3](https://github.com/antialias/soroban-abacus-flashcards/commit/dd8efe379d4bbcfc4b60f7c00ad6180465b7e7b6))
- allow teacher-parents to enroll their children in other classrooms ([52df7f4](https://github.com/antialias/soroban-abacus-flashcards/commit/52df7f469718128fd3d8933941ffb8d4bb8db208))
- **bkt:** handle missing helpLevelUsed in legacy data causing NaN ([b300ed9](https://github.com/antialias/soroban-abacus-flashcards/commit/b300ed9f5cc3bfb0c7b28faafe81c80a59444998))
- **camera:** handle race condition in camera initialization ([2a24700](https://github.com/antialias/soroban-abacus-flashcards/commit/2a24700e6cb6efe0ae35d9ebd6c428e3a1a1a736))
- **classroom:** auto-transition tutorial→session observation + fix NaN display ([962a52d](https://github.com/antialias/soroban-abacus-flashcards/commit/962a52d7562f566e78f6272816b049bf77daa7c9))
- **classroom:** broadcast digit-by-digit answer and correct phase indicator ([fb73e85](https://github.com/antialias/soroban-abacus-flashcards/commit/fb73e85f2daacefafa572e03c16b10fab619ea57))
- **dashboard:** compute skill stats from session results in curriculum API ([11d4846](https://github.com/antialias/soroban-abacus-flashcards/commit/11d48465d710d0293ebf41f64b4fd0f1f03d8bf8))
- **db:** add missing is_paused column to session_plans ([9d8b5e1](https://github.com/antialias/soroban-abacus-flashcards/commit/9d8b5e1148911f881d08d07608debaaef91609c2))
- **db:** add missing journal entries for migrations 0041-0042 ([398603c](https://github.com/antialias/soroban-abacus-flashcards/commit/398603c75a094e28122c5ccdced5b82badc7fbfb))
- **docker:** add canvas native deps for jsdom/vitest ([5f51bc1](https://github.com/antialias/soroban-abacus-flashcards/commit/5f51bc1871aec325feb32a0b29edabb3b6c5dd1f))
- **docker:** override canvas with mock package for Alpine/musl ([8be1995](https://github.com/antialias/soroban-abacus-flashcards/commit/8be19958af624d22fa2c6cb48f5723f5efc820c3))
- **docker:** skip canvas native build (optional jsdom dep) ([d717f44](https://github.com/antialias/soroban-abacus-flashcards/commit/d717f44fccb8ed2baa30499df65784a4b89c6ffc))
- **observer:** seed results panel with full session history ([aab7469](https://github.com/antialias/soroban-abacus-flashcards/commit/aab7469d9ea87c91a0165e4c48a60ac130cdc1b2))
- only show session stats when there are actual problems ([62aefad](https://github.com/antialias/soroban-abacus-flashcards/commit/62aefad6766ba32ad27e8ed3db621a6f77520cbe))
- **practice:** allow teachers to create student profiles ([5fee129](https://github.com/antialias/soroban-abacus-flashcards/commit/5fee1297e1775b5e6133919d179e23b6e70b2518))
- **practice:** always show add student FAB button ([a658414](https://github.com/antialias/soroban-abacus-flashcards/commit/a6584143ebf1f3e5b3c9f3283e690458a06beb60))
- **practice:** real-time progress in observer modal + numeric answer comparison ([c0e63ff](https://github.com/antialias/soroban-abacus-flashcards/commit/c0e63ff68b26fd37eedd657504f7f79e5ce40a10))
- **practice:** show active sessions for teacher's own children ([ece3197](https://github.com/antialias/soroban-abacus-flashcards/commit/ece319738b6ab1882469d79ea24b604316d28b34))
- **practice:** use Next.js Link for student tiles + fix session observer z-index ([6def610](https://github.com/antialias/soroban-abacus-flashcards/commit/6def6108771b427e4885bebd23cecdad7a50efb0))
- **seed:** accurate BKT simulation for developing classifications ([d5e4c85](https://github.com/antialias/soroban-abacus-flashcards/commit/d5e4c858db8866e5177b8fa2317aba42b30171e8))
- **share:** use getShareUrl for correct production URLs ([98a69f1](https://github.com/antialias/soroban-abacus-flashcards/commit/98a69f1f80e465415edce49043e2c019a856f8e5))
- **vision:** fix manual calibration overlay not showing on remote camera ([44dcb01](https://github.com/antialias/soroban-abacus-flashcards/commit/44dcb01473bac00c09dddbbefd77dd26b3a27817))
- **vision:** fix remote camera calibration coordinate system ([e52f94e](https://github.com/antialias/soroban-abacus-flashcards/commit/e52f94e4b476658c41f23668d2941af1288e4ed8))
- **vision:** swap corners diagonally for webcam orientation ([dd8efe3](https://github.com/antialias/soroban-abacus-flashcards/commit/dd8efe379d4bbcfc4b60f7c00ad6180465b7e7b6))
### Features
* API authorization audit + teacher enrollment UI + share codes ([d6e369f](https://github.com/antialias/soroban-abacus-flashcards/commit/d6e369f9dc9b963938ca8de4562c87f9f1b6d389))
* **camera:** auto-start camera when opening camera modal ([f3bb0ae](https://github.com/antialias/soroban-abacus-flashcards/commit/f3bb0aee4fe23eeffc7b7099981f51ec54636a35))
* **camera:** fullscreen modal with edge-to-edge preview ([db17c96](https://github.com/antialias/soroban-abacus-flashcards/commit/db17c96168078f2d0d723b24395096756a2f63ec))
* **chart:** add grouped structure to chart hover tooltip ([594e22c](https://github.com/antialias/soroban-abacus-flashcards/commit/594e22c428e0a4ee4322c233f127f9250e88b5fa))
* **chart:** improve skill classification visual hierarchy with colors and patterns ([c9518a6](https://github.com/antialias/soroban-abacus-flashcards/commit/c9518a6b9952bda60ab2663d7655092637139fec))
* **classroom:** add active sessions API endpoint ([07f6bb7](https://github.com/antialias/soroban-abacus-flashcards/commit/07f6bb7f9cc2dfbe6da8d16361e89b698405e1c0))
* **classroom:** add real-time enrollment/unenrollment reactivity ([a0693e9](https://github.com/antialias/soroban-abacus-flashcards/commit/a0693e90840f651094f852a6a6f523013786b322))
* **classroom:** add session broadcast and active session indicators ([9636f7f](https://github.com/antialias/soroban-abacus-flashcards/commit/9636f7f44a71da022352c19e80f9ec147dd3af5f))
* **classroom:** add unified add-student modal with two-column layout ([dca696a](https://github.com/antialias/soroban-abacus-flashcards/commit/dca696a29fc20a2697b491c0d2efbe036569a716))
* **classroom:** add unified TeacherClassroomCard with auto-enrollment ([4d6adf3](https://github.com/antialias/soroban-abacus-flashcards/commit/4d6adf359ede5d17c2decd9275ba68635ee0bd4f))
* **classroom:** complete reactivity fixes (Steps 7-11) ([2015494](https://github.com/antialias/soroban-abacus-flashcards/commit/2015494c0eca28457031aa39490d70a2af3da4df))
* **classroom:** consolidate filter pill to single-row design ([78a63e3](https://github.com/antialias/soroban-abacus-flashcards/commit/78a63e35e39948729cbf41e6c5af4e688a506c8d))
* **classroom:** implement enrollment system (Phase 4) ([1952a41](https://github.com/antialias/soroban-abacus-flashcards/commit/1952a412edcd04b332655199737c340a4389d174))
* **classroom:** implement entry prompts system ([de39ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/de39ab52cc60f5782fc291246f98013ae15142ca))
* **classroom:** implement real-time enrollment updates ([bbe0500](https://github.com/antialias/soroban-abacus-flashcards/commit/bbe0500fe9000d0d016417c1b586e9569e3eb888))
* **classroom:** implement real-time presence with WebSocket (Phase 6) ([629bfcf](https://github.com/antialias/soroban-abacus-flashcards/commit/629bfcfc03c611cd3928bb98a67bace485ee3a7b))
* **classroom:** implement real-time session observation (Step 3) ([2feb684](https://github.com/antialias/soroban-abacus-flashcards/commit/2feb6844a4fce48ba7a87d2a77769783c4e8b2f9))
* **classroom:** implement real-time skill tutorial observation ([4b73879](https://github.com/antialias/soroban-abacus-flashcards/commit/4b7387905d2b050327f9b67b834d4e9dfc0b19cb))
* **classroom:** implement teacher classroom dashboard (Phase 3) ([2202716](https://github.com/antialias/soroban-abacus-flashcards/commit/2202716f563053624dbe5c6abb969a3b0d452fd1))
* **classroom:** implement teacher-initiated pause and fix manual pause ([ccea0f8](https://github.com/antialias/soroban-abacus-flashcards/commit/ccea0f86ac213b32cac7363f28e193b1976bd553))
* **classroom:** implement two-way abacus sync for session observation (Step 5) ([2f7002e](https://github.com/antialias/soroban-abacus-flashcards/commit/2f7002e5759db705e213eb9f8474589c8e6149e7))
* **classroom:** improve enrollment reactivity and UX ([77336be](https://github.com/antialias/soroban-abacus-flashcards/commit/77336bea5b5bbf16b393da13588de6e5082e818f))
* **classroom:** integrate create student form into unified add-student modal ([da92289](https://github.com/antialias/soroban-abacus-flashcards/commit/da92289ed1ae570ff48cc28818122d4640d6c84c))
* **classroom:** integrate Enter Classroom into StudentActionMenu ([2f1b9df](https://github.com/antialias/soroban-abacus-flashcards/commit/2f1b9df9d9d605b0c120af6961670ae84718c8d7))
* **dashboard:** add skill progress chart with trend analysis and timing awareness ([1fc8949](https://github.com/antialias/soroban-abacus-flashcards/commit/1fc8949b0664591aa1b0cfcd7c7abd2a4c586281))
* enable parents to observe children's practice sessions ([7b82995](https://github.com/antialias/soroban-abacus-flashcards/commit/7b829956644d369dfdfb0789a33e0b857958e84f))
* **family:** implement parent-to-parent family code sharing (Phase 2) ([0284227](https://github.com/antialias/soroban-abacus-flashcards/commit/02842270c9278174934407a9620777589f79ee1e))
* improve session summary header and add practice type badges ([518fe15](https://github.com/antialias/soroban-abacus-flashcards/commit/518fe153c9fc2ae2f2f7fc0ed4de27ee1c5c5646))
* **observer:** add live active session item to history list ([91d6d6a](https://github.com/antialias/soroban-abacus-flashcards/commit/91d6d6a1b6938b559d8488fe296d562695cf16d1))
* **observer:** add live results panel and session progress indicator ([8527f89](https://github.com/antialias/soroban-abacus-flashcards/commit/8527f892e2b300d51d83056d779474592a2fd955))
* **observer:** implement shareable session observation links ([3ac7b46](https://github.com/antialias/soroban-abacus-flashcards/commit/3ac7b460ec0dc207a5691fbed8d539b484374fe7))
* **practice:** add auto-rotation for captured documents ([ff79a28](https://github.com/antialias/soroban-abacus-flashcards/commit/ff79a28c657fb0a19752990e23f9bb0ced4e9343))
* **practice:** add document adjustment UI and auto-capture ([473b7db](https://github.com/antialias/soroban-abacus-flashcards/commit/473b7dbd7cd15be511351a1fd303a0fc32b9d941))
* **practice:** add document scanning with multi-quad tracking ([5f4f1fd](https://github.com/antialias/soroban-abacus-flashcards/commit/5f4f1fde3372e5d65d3f399216b04ab0e4c9972e))
* **practice:** add fixed filter bar, sticky headers, and shared EmojiPicker ([0e03561](https://github.com/antialias/soroban-abacus-flashcards/commit/0e0356113ddef1ec92cd0b3fda0852d99c6067d2))
* **practice:** add intervention system and improve skill chart hierarchy ([bf5b99a](https://github.com/antialias/soroban-abacus-flashcards/commit/bf5b99afe967c0b17765a7e6f1911d03201eed95))
* **practice:** add mini start practice banner to QuickLook modal ([d1176da](https://github.com/antialias/soroban-abacus-flashcards/commit/d1176da9aa8bd926ca96699d1091e65f4a34d782))
* **practice:** add Needs Attention to unified compact layout ([8727782](https://github.com/antialias/soroban-abacus-flashcards/commit/8727782e45c7ac269c4dbcc223b2a8be57be8bb2))
* **practice:** add photo attachments for practice sessions ([9b85311](https://github.com/antialias/soroban-abacus-flashcards/commit/9b853116ecfbb19bec39923da635374963cf002c))
* **practice:** add photo editing with rotation persistence and auto-detect ([156a0df](https://github.com/antialias/soroban-abacus-flashcards/commit/156a0dfe967a48c211be527da27c92ef8b1ab20c))
* **practice:** add smooth fullscreen transition from QuickLook to dashboard ([cb8b0df](https://github.com/antialias/soroban-abacus-flashcards/commit/cb8b0dff676d48bcba4775c5981ac357d573ab27))
* **practice:** add student organization with filtering and archiving ([538718a](https://github.com/antialias/soroban-abacus-flashcards/commit/538718a814402bd9c83b3c354c5a3386ff69104d))
* **practice:** add StudentActionMenu to dashboard + fix z-index layering ([bf262e7](https://github.com/antialias/soroban-abacus-flashcards/commit/bf262e7d5305e2358d3a2464db10bc3b0866104c))
* **practice:** compact single-student categories and UI improvements ([0e7f326](https://github.com/antialias/soroban-abacus-flashcards/commit/0e7f3265fe2de3b693c47a8a556d3e7cbc726ef4))
* **practice:** implement measurement-based compact layout ([1656b93](https://github.com/antialias/soroban-abacus-flashcards/commit/1656b9324f6fb24a318820e04559c480c99762f5))
* **practice:** implement retry wrong problems system ([474c4da](https://github.com/antialias/soroban-abacus-flashcards/commit/474c4da05a8d761e63a32187f5c301b57fb6aae4))
* **practice:** parent session observation + relationship UI + error boundaries ([07484fd](https://github.com/antialias/soroban-abacus-flashcards/commit/07484fdfac3c6613a6a7709bdee25e1f8e047227))
* **practice:** polish unified student list with keyboard nav and mobile UX ([0ba1551](https://github.com/antialias/soroban-abacus-flashcards/commit/0ba1551feaa30d8f41ec5d771c00561396b043f3))
* **seed:** add category field to all mock student profiles ([f883fbf](https://github.com/antialias/soroban-abacus-flashcards/commit/f883fbfe233b7fb3d366062e7c156e3fc8e0e3a7))
* **session-summary:** redesign ProblemToReview with BKT integration and animations ([430c46a](https://github.com/antialias/soroban-abacus-flashcards/commit/430c46adb929a6c0ce7c67da4b1df7d3e2846cfd))
* **storybook:** add TeacherClassroomCard stories ([a5e5788](https://github.com/antialias/soroban-abacus-flashcards/commit/a5e5788fa96f57e0d918620e357f7920ef792b19))
* **vision:** add AbacusVisionBridge for physical soroban detection ([47088e4](https://github.com/antialias/soroban-abacus-flashcards/commit/47088e4850c25e76fe49879587227b46f699ba91))
* **vision:** add ArUco marker auto-calibration for abacus detection ([9e9a06f](https://github.com/antialias/soroban-abacus-flashcards/commit/9e9a06f2e4dc37d208ac19259be9b9830c7ad949))
* **vision:** add remote phone camera support for abacus detection ([8e4975d](https://github.com/antialias/soroban-abacus-flashcards/commit/8e4975d395c4b10bc40ae2c71473fdb1a50c114c))
- API authorization audit + teacher enrollment UI + share codes ([d6e369f](https://github.com/antialias/soroban-abacus-flashcards/commit/d6e369f9dc9b963938ca8de4562c87f9f1b6d389))
- **camera:** auto-start camera when opening camera modal ([f3bb0ae](https://github.com/antialias/soroban-abacus-flashcards/commit/f3bb0aee4fe23eeffc7b7099981f51ec54636a35))
- **camera:** fullscreen modal with edge-to-edge preview ([db17c96](https://github.com/antialias/soroban-abacus-flashcards/commit/db17c96168078f2d0d723b24395096756a2f63ec))
- **chart:** add grouped structure to chart hover tooltip ([594e22c](https://github.com/antialias/soroban-abacus-flashcards/commit/594e22c428e0a4ee4322c233f127f9250e88b5fa))
- **chart:** improve skill classification visual hierarchy with colors and patterns ([c9518a6](https://github.com/antialias/soroban-abacus-flashcards/commit/c9518a6b9952bda60ab2663d7655092637139fec))
- **classroom:** add active sessions API endpoint ([07f6bb7](https://github.com/antialias/soroban-abacus-flashcards/commit/07f6bb7f9cc2dfbe6da8d16361e89b698405e1c0))
- **classroom:** add real-time enrollment/unenrollment reactivity ([a0693e9](https://github.com/antialias/soroban-abacus-flashcards/commit/a0693e90840f651094f852a6a6f523013786b322))
- **classroom:** add session broadcast and active session indicators ([9636f7f](https://github.com/antialias/soroban-abacus-flashcards/commit/9636f7f44a71da022352c19e80f9ec147dd3af5f))
- **classroom:** add unified add-student modal with two-column layout ([dca696a](https://github.com/antialias/soroban-abacus-flashcards/commit/dca696a29fc20a2697b491c0d2efbe036569a716))
- **classroom:** add unified TeacherClassroomCard with auto-enrollment ([4d6adf3](https://github.com/antialias/soroban-abacus-flashcards/commit/4d6adf359ede5d17c2decd9275ba68635ee0bd4f))
- **classroom:** complete reactivity fixes (Steps 7-11) ([2015494](https://github.com/antialias/soroban-abacus-flashcards/commit/2015494c0eca28457031aa39490d70a2af3da4df))
- **classroom:** consolidate filter pill to single-row design ([78a63e3](https://github.com/antialias/soroban-abacus-flashcards/commit/78a63e35e39948729cbf41e6c5af4e688a506c8d))
- **classroom:** implement enrollment system (Phase 4) ([1952a41](https://github.com/antialias/soroban-abacus-flashcards/commit/1952a412edcd04b332655199737c340a4389d174))
- **classroom:** implement entry prompts system ([de39ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/de39ab52cc60f5782fc291246f98013ae15142ca))
- **classroom:** implement real-time enrollment updates ([bbe0500](https://github.com/antialias/soroban-abacus-flashcards/commit/bbe0500fe9000d0d016417c1b586e9569e3eb888))
- **classroom:** implement real-time presence with WebSocket (Phase 6) ([629bfcf](https://github.com/antialias/soroban-abacus-flashcards/commit/629bfcfc03c611cd3928bb98a67bace485ee3a7b))
- **classroom:** implement real-time session observation (Step 3) ([2feb684](https://github.com/antialias/soroban-abacus-flashcards/commit/2feb6844a4fce48ba7a87d2a77769783c4e8b2f9))
- **classroom:** implement real-time skill tutorial observation ([4b73879](https://github.com/antialias/soroban-abacus-flashcards/commit/4b7387905d2b050327f9b67b834d4e9dfc0b19cb))
- **classroom:** implement teacher classroom dashboard (Phase 3) ([2202716](https://github.com/antialias/soroban-abacus-flashcards/commit/2202716f563053624dbe5c6abb969a3b0d452fd1))
- **classroom:** implement teacher-initiated pause and fix manual pause ([ccea0f8](https://github.com/antialias/soroban-abacus-flashcards/commit/ccea0f86ac213b32cac7363f28e193b1976bd553))
- **classroom:** implement two-way abacus sync for session observation (Step 5) ([2f7002e](https://github.com/antialias/soroban-abacus-flashcards/commit/2f7002e5759db705e213eb9f8474589c8e6149e7))
- **classroom:** improve enrollment reactivity and UX ([77336be](https://github.com/antialias/soroban-abacus-flashcards/commit/77336bea5b5bbf16b393da13588de6e5082e818f))
- **classroom:** integrate create student form into unified add-student modal ([da92289](https://github.com/antialias/soroban-abacus-flashcards/commit/da92289ed1ae570ff48cc28818122d4640d6c84c))
- **classroom:** integrate Enter Classroom into StudentActionMenu ([2f1b9df](https://github.com/antialias/soroban-abacus-flashcards/commit/2f1b9df9d9d605b0c120af6961670ae84718c8d7))
- **dashboard:** add skill progress chart with trend analysis and timing awareness ([1fc8949](https://github.com/antialias/soroban-abacus-flashcards/commit/1fc8949b0664591aa1b0cfcd7c7abd2a4c586281))
- enable parents to observe children's practice sessions ([7b82995](https://github.com/antialias/soroban-abacus-flashcards/commit/7b829956644d369dfdfb0789a33e0b857958e84f))
- **family:** implement parent-to-parent family code sharing (Phase 2) ([0284227](https://github.com/antialias/soroban-abacus-flashcards/commit/02842270c9278174934407a9620777589f79ee1e))
- improve session summary header and add practice type badges ([518fe15](https://github.com/antialias/soroban-abacus-flashcards/commit/518fe153c9fc2ae2f2f7fc0ed4de27ee1c5c5646))
- **observer:** add live active session item to history list ([91d6d6a](https://github.com/antialias/soroban-abacus-flashcards/commit/91d6d6a1b6938b559d8488fe296d562695cf16d1))
- **observer:** add live results panel and session progress indicator ([8527f89](https://github.com/antialias/soroban-abacus-flashcards/commit/8527f892e2b300d51d83056d779474592a2fd955))
- **observer:** implement shareable session observation links ([3ac7b46](https://github.com/antialias/soroban-abacus-flashcards/commit/3ac7b460ec0dc207a5691fbed8d539b484374fe7))
- **practice:** add auto-rotation for captured documents ([ff79a28](https://github.com/antialias/soroban-abacus-flashcards/commit/ff79a28c657fb0a19752990e23f9bb0ced4e9343))
- **practice:** add document adjustment UI and auto-capture ([473b7db](https://github.com/antialias/soroban-abacus-flashcards/commit/473b7dbd7cd15be511351a1fd303a0fc32b9d941))
- **practice:** add document scanning with multi-quad tracking ([5f4f1fd](https://github.com/antialias/soroban-abacus-flashcards/commit/5f4f1fde3372e5d65d3f399216b04ab0e4c9972e))
- **practice:** add fixed filter bar, sticky headers, and shared EmojiPicker ([0e03561](https://github.com/antialias/soroban-abacus-flashcards/commit/0e0356113ddef1ec92cd0b3fda0852d99c6067d2))
- **practice:** add intervention system and improve skill chart hierarchy ([bf5b99a](https://github.com/antialias/soroban-abacus-flashcards/commit/bf5b99afe967c0b17765a7e6f1911d03201eed95))
- **practice:** add mini start practice banner to QuickLook modal ([d1176da](https://github.com/antialias/soroban-abacus-flashcards/commit/d1176da9aa8bd926ca96699d1091e65f4a34d782))
- **practice:** add Needs Attention to unified compact layout ([8727782](https://github.com/antialias/soroban-abacus-flashcards/commit/8727782e45c7ac269c4dbcc223b2a8be57be8bb2))
- **practice:** add photo attachments for practice sessions ([9b85311](https://github.com/antialias/soroban-abacus-flashcards/commit/9b853116ecfbb19bec39923da635374963cf002c))
- **practice:** add photo editing with rotation persistence and auto-detect ([156a0df](https://github.com/antialias/soroban-abacus-flashcards/commit/156a0dfe967a48c211be527da27c92ef8b1ab20c))
- **practice:** add smooth fullscreen transition from QuickLook to dashboard ([cb8b0df](https://github.com/antialias/soroban-abacus-flashcards/commit/cb8b0dff676d48bcba4775c5981ac357d573ab27))
- **practice:** add student organization with filtering and archiving ([538718a](https://github.com/antialias/soroban-abacus-flashcards/commit/538718a814402bd9c83b3c354c5a3386ff69104d))
- **practice:** add StudentActionMenu to dashboard + fix z-index layering ([bf262e7](https://github.com/antialias/soroban-abacus-flashcards/commit/bf262e7d5305e2358d3a2464db10bc3b0866104c))
- **practice:** compact single-student categories and UI improvements ([0e7f326](https://github.com/antialias/soroban-abacus-flashcards/commit/0e7f3265fe2de3b693c47a8a556d3e7cbc726ef4))
- **practice:** implement measurement-based compact layout ([1656b93](https://github.com/antialias/soroban-abacus-flashcards/commit/1656b9324f6fb24a318820e04559c480c99762f5))
- **practice:** implement retry wrong problems system ([474c4da](https://github.com/antialias/soroban-abacus-flashcards/commit/474c4da05a8d761e63a32187f5c301b57fb6aae4))
- **practice:** parent session observation + relationship UI + error boundaries ([07484fd](https://github.com/antialias/soroban-abacus-flashcards/commit/07484fdfac3c6613a6a7709bdee25e1f8e047227))
- **practice:** polish unified student list with keyboard nav and mobile UX ([0ba1551](https://github.com/antialias/soroban-abacus-flashcards/commit/0ba1551feaa30d8f41ec5d771c00561396b043f3))
- **seed:** add category field to all mock student profiles ([f883fbf](https://github.com/antialias/soroban-abacus-flashcards/commit/f883fbfe233b7fb3d366062e7c156e3fc8e0e3a7))
- **session-summary:** redesign ProblemToReview with BKT integration and animations ([430c46a](https://github.com/antialias/soroban-abacus-flashcards/commit/430c46adb929a6c0ce7c67da4b1df7d3e2846cfd))
- **storybook:** add TeacherClassroomCard stories ([a5e5788](https://github.com/antialias/soroban-abacus-flashcards/commit/a5e5788fa96f57e0d918620e357f7920ef792b19))
- **vision:** add AbacusVisionBridge for physical soroban detection ([47088e4](https://github.com/antialias/soroban-abacus-flashcards/commit/47088e4850c25e76fe49879587227b46f699ba91))
- **vision:** add ArUco marker auto-calibration for abacus detection ([9e9a06f](https://github.com/antialias/soroban-abacus-flashcards/commit/9e9a06f2e4dc37d208ac19259be9b9830c7ad949))
- **vision:** add remote phone camera support for abacus detection ([8e4975d](https://github.com/antialias/soroban-abacus-flashcards/commit/8e4975d395c4b10bc40ae2c71473fdb1a50c114c))
### Performance Improvements
* reduce practice page dev bundle from 47MB to 115KB ([fd1df93](https://github.com/antialias/soroban-abacus-flashcards/commit/fd1df93a8fa320800275c135d5dd89390eb72c19))
- reduce practice page dev bundle from 47MB to 115KB ([fd1df93](https://github.com/antialias/soroban-abacus-flashcards/commit/fd1df93a8fa320800275c135d5dd89390eb72c19))
# [2.17.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.16.0...abacus-react-v2.17.0) (2025-12-20)

File diff suppressed because it is too large Load Diff