---
phase: 28-vu-meter-optimization
plan: 02
type: execute
wave: 2
depends_on: ["28-01"]
files_modified:
  - jam-ui/src/hooks/useVuHelpers.js
  - jam-ui/src/context/VuContext.js
  - jam-ui/src/components/client/SessionTrackVU.js
  - jam-ui/src/hooks/useMixerStore.js
autonomous: false

must_haves:
  truths:
    - "VU updates from native client flow to external store, not React state"
    - "VU meter visuals update via direct DOM manipulation, not React render"
    - "React DevTools shows no re-renders from VU updates"
    - "Session screen remains responsive during continuous VU updates"
  artifacts:
    - path: "jam-ui/src/hooks/useVuHelpers.js"
      provides: "VU helpers using external store"
      contains: "vuStore"
    - path: "jam-ui/src/context/VuContext.js"
      provides: "Simplified VU context providing store reference"
      contains: "vuStore"
    - path: "jam-ui/src/components/client/SessionTrackVU.js"
      provides: "VU component with direct DOM updates via refs"
      contains: "requestAnimationFrame"
    - path: "jam-ui/src/hooks/useMixerStore.js"
      provides: "Bridge callback routing VU data to external store"
      contains: "vuStore.updateLevel"
  key_links:
    - from: "jam-ui/src/hooks/useMixerStore.js"
      to: "jam-ui/src/stores/vuStore.js"
      via: "vuStore.updateLevel in handleBridgeCallback"
      pattern: "vuStore\\.updateLevel"
    - from: "jam-ui/src/components/client/SessionTrackVU.js"
      to: "jam-ui/src/stores/vuStore.js"
      via: "vuStore.getLevelSnapshot in RAF loop"
      pattern: "vuStore\\.getLevelSnapshot"
---

<objective>
Wire up existing VU components to use the external store and direct DOM updates, completing the VU meter optimization.

Purpose: Plan 01 created the external store infrastructure. This plan integrates it with existing components, replacing React state updates with direct DOM manipulation for zero-render VU visuals.

Output:
- Modified useVuHelpers.js - removes useState, uses external store
- Modified VuContext.js - simplified to provide store reference
- Modified SessionTrackVU.js - uses refs for direct DOM manipulation
- Modified useMixerStore.js - routes VU data to external store
</objective>

<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/28-vu-meter-optimization/28-RESEARCH.md
@.planning/phases/28-vu-meter-optimization/28-01-SUMMARY.md

# Files to modify
@jam-ui/src/hooks/useVuHelpers.js
@jam-ui/src/context/VuContext.js
@jam-ui/src/components/client/SessionTrackVU.js
@jam-ui/src/hooks/useMixerStore.js

# New files from Plan 01
@jam-ui/src/stores/vuStore.js
@jam-ui/src/hooks/useVuStore.js
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update useMixerStore to route VU data to external store</name>
  <files>
    jam-ui/src/hooks/useMixerStore.js
  </files>
  <action>
1. Add import at top of file:
   ```javascript
   import { vuStore } from '../stores/vuStore';
   ```

2. Modify `handleBridgeCallback` function (around line 135-191):

   **Current flow:**
   - Receives vuData from native client
   - Loops through VU events
   - Calls `mixerHelper.updateVU(...)` which eventually triggers React state updates

   **New flow:**
   - Keep the loop structure and dB-to-normalized conversion logic
   - Instead of calling `mixerHelper.updateVU(...)`, call `vuStore.updateLevel(fqId, normalizedLevel, leftClipping)`
   - The external store will buffer and batch updates via RAF

   **Specific changes in the loop:**
   ```javascript
   if (eventName === "vu") {
     const mixerId = vuInfo[1];
     const mode = vuInfo[2];
     const leftValue = vuInfo[3];
     const leftClipping = vuInfo[4];
     // rightValue and rightClipping can be ignored for now (mono VU)

     // Convert dB to 0.0-1.0 range (existing logic)
     const normalizedLevel = (leftValue + 80) / 80;

     // Create qualified ID matching existing pattern
     const fqId = (mode ? 'M' : 'P') + mixerId;

     // Route to external store instead of mixerHelper
     vuStore.updateLevel(fqId, normalizedLevel, leftClipping);
   }
   ```

3. REMOVE the mixerHelper.updateVU call entirely - the external store now handles all VU data

4. Keep the mixerHelperRef and other mixerHelper usage for non-VU functionality (mixer configs, etc.)

**Important:** The native client still calls `window.JK.HandleBridgeCallback2` at 50-70/sec, but now those updates go to the external store's buffer instead of triggering React state updates.
  </action>
  <verify>
    - `grep "vuStore.updateLevel" jam-ui/src/hooks/useMixerStore.js` shows the new call
    - `grep "mixerHelper.updateVU" jam-ui/src/hooks/useMixerStore.js` returns empty (removed)
    - File still has `import { vuStore }` at top
  </verify>
  <done>
    - useMixerStore imports vuStore
    - handleBridgeCallback routes VU data to vuStore.updateLevel
    - mixerHelper.updateVU call removed from bridge callback
  </done>
</task>

<task type="auto">
  <name>Task 2: Simplify useVuHelpers and VuContext</name>
  <files>
    jam-ui/src/hooks/useVuHelpers.js
    jam-ui/src/context/VuContext.js
  </files>
  <action>
**Modify useVuHelpers.js:**

1. Remove the useState import and state:
   - Remove `useState` from React imports
   - Remove `const [vuStates, setVuStates] = useState({});`

2. Remove React state-based functions:
   - Remove `updateVuState` callback (lines 99-104)
   - Remove `removeVuState` callback (lines 106-112)
   - Remove `updateVU3` callback (lines 114-123) - this is the React state update path
   - Remove the cleanup useEffect (lines 125-130)

3. Remove `VuMeterComponent` function (lines 133-181) - this will be replaced by direct DOM updates in SessionTrackVU

4. Import vuStore:
   ```javascript
   import { vuStore } from '../stores/vuStore';
   ```

5. Keep the legacy functions for backward compatibility:
   - `createQualifiedId` - still useful
   - `renderVU` - legacy table-based VU
   - `updateVU` - legacy DOM manipulation (different from new pattern but may be used elsewhere)

6. Update return object to expose vuStore methods:
   ```javascript
   return {
     // External store methods (new)
     updateVuState: vuStore.updateLevel,
     removeVuState: vuStore.removeLevel,
     vuStore, // Expose full store for advanced usage

     // Legacy functions for backward compatibility
     createQualifiedId,
     renderVU,
     updateVU,
   };
   ```

**Modify VuContext.js:**

1. Simplify to just provide vuStore reference:
   ```javascript
   import React, { createContext, useContext } from 'react';
   import { vuStore } from '../stores/vuStore';
   import useVuHelpers from '../hooks/useVuHelpers.js';

   const VuContext = createContext();

   export const VuProvider = ({ children }) => {
     const vuHelpers = useVuHelpers();

     // Combine vuHelpers with vuStore for context value
     const value = {
       ...vuHelpers,
       vuStore,
     };

     return (
       <VuContext.Provider value={value}>
         {children}
       </VuContext.Provider>
     );
   };

   // Rest unchanged
   ```

**Why keep useVuHelpers:** It provides createQualifiedId and legacy renderVU/updateVU functions that may still be used by other components. The goal is minimal changes - replace the hot path (VU state updates) while maintaining backward compatibility.
  </action>
  <verify>
    - `grep "useState" jam-ui/src/hooks/useVuHelpers.js` returns empty or only in comments
    - `grep "vuStore" jam-ui/src/hooks/useVuHelpers.js` shows import
    - `grep "vuStore" jam-ui/src/context/VuContext.js` shows import and usage
  </verify>
  <done>
    - useVuHelpers no longer uses useState for VU levels
    - useVuHelpers exposes vuStore methods via return object
    - VuContext provides vuStore reference to consumers
    - Legacy functions preserved for backward compatibility
  </done>
</task>

<task type="auto">
  <name>Task 3: Rewrite SessionTrackVU with direct DOM updates</name>
  <files>
    jam-ui/src/components/client/SessionTrackVU.js
  </files>
  <action>
Rewrite SessionTrackVU to use refs for direct DOM manipulation, bypassing React reconciliation entirely for VU visual updates.

**New implementation structure:**

```javascript
import React, { useEffect, useRef, memo } from 'react';
import { vuStore } from '../../stores/vuStore';

const SessionTrackVU = memo(function SessionTrackVU({
  lightCount = 16,
  orientation = 'vertical',
  lightWidth = 15,
  lightHeight = 10,
  mixers
}) {
  const containerRef = useRef(null);
  const lightsRef = useRef([]);
  const mixerIdRef = useRef(null);
  const rafIdRef = useRef(null);

  // Update mixerId when mixers prop changes
  useEffect(() => {
    const mixer = mixers?.vuMixer;
    mixerIdRef.current = mixer ? `${mixer.mode ? 'M' : 'P'}${mixer.id}` : null;

    return () => {
      // Cleanup VU state on unmount
      if (mixerIdRef.current) {
        vuStore.removeLevel(mixerIdRef.current);
      }
    };
  }, [mixers]);

  // Animation loop for direct DOM updates - separate from React lifecycle
  useEffect(() => {
    const updateVisuals = () => {
      const mixerId = mixerIdRef.current;

      if (!mixerId) {
        rafIdRef.current = requestAnimationFrame(updateVisuals);
        return;
      }

      const data = vuStore.getLevelSnapshot(mixerId);
      const level = data?.level ?? 0;
      const clipping = data?.clipping ?? false;
      const activeLights = Math.round(level * lightCount);

      // Direct DOM manipulation - no React involvement
      for (let i = 0; i < lightCount; i++) {
        const light = lightsRef.current[i];
        if (!light) continue;

        const positionFromBottom = lightCount - 1 - i;
        const isActive = positionFromBottom < activeLights;

        // Use className assignment for efficient class manipulation
        // (faster than classList.add/remove for multiple classes)
        if (isActive) {
          if (clipping || positionFromBottom >= Math.floor(lightCount * 0.75)) {
            light.className = 'vu-light vu-bg-danger';
          } else if (positionFromBottom >= Math.floor(lightCount * 0.5)) {
            light.className = 'vu-light vu-bg-warning';
          } else {
            light.className = 'vu-light vu-bg-success';
          }
        } else {
          light.className = 'vu-light vu-bg-secondary';
        }
      }

      rafIdRef.current = requestAnimationFrame(updateVisuals);
    };

    rafIdRef.current = requestAnimationFrame(updateVisuals);

    return () => {
      if (rafIdRef.current) {
        cancelAnimationFrame(rafIdRef.current);
      }
    };
  }, [lightCount]);

  // Render ONCE - never re-renders for VU updates
  return (
    <div ref={containerRef} className="vu">
      <div className="d-flex flex-column" style={{ height: `${lightCount * (lightHeight + 1)}px` }}>
        {Array.from({ length: lightCount }).map((_, i) => (
          <div
            key={i}
            ref={el => lightsRef.current[i] = el}
            className="vu-light vu-bg-secondary"
            style={{
              height: `${lightHeight}px`,
              width: '25px',
              marginTop: '1px',
              borderRadius: '2px',
              border: '1px solid #eee'
            }}
          />
        ))}
      </div>
    </div>
  );
});

export default SessionTrackVU;
```

**Key changes from current implementation:**
1. Remove useVuContext import and usage - directly import vuStore
2. Remove VuMeter component usage - render lights directly
3. Add own RAF loop for polling vuStore.getLevelSnapshot
4. Store light element refs for direct DOM manipulation
5. Wrap with React.memo to prevent re-renders from parent
6. className assignment instead of classList for efficiency

**Why per-component RAF instead of store subscription:**
- Each VU meter needs different mixerId
- Direct polling is simpler than setting up individual subscriptions
- RAF automatically pauses when tab is backgrounded
- ~60fps polling matches display refresh rate

**CSS classes used:**
- `vu-light` - base class for light elements
- `vu-bg-danger` - red (clipping or top 25%)
- `vu-bg-warning` - yellow (50-75%)
- `vu-bg-success` - green (bottom 50%)
- `vu-bg-secondary` - off state

These classes should already exist from the current VuMeterComponent in useVuHelpers.js.
  </action>
  <verify>
    - `grep "requestAnimationFrame" jam-ui/src/components/client/SessionTrackVU.js` shows RAF usage
    - `grep "useVuContext" jam-ui/src/components/client/SessionTrackVU.js` returns empty (removed)
    - `grep "vuStore.getLevelSnapshot" jam-ui/src/components/client/SessionTrackVU.js` shows direct store access
    - `grep "React.memo\\|memo(" jam-ui/src/components/client/SessionTrackVU.js` shows memoization
  </verify>
  <done>
    - SessionTrackVU uses direct vuStore import instead of context
    - Component uses RAF loop with direct DOM manipulation via refs
    - Component wrapped with React.memo to prevent re-renders
    - No React state updates for VU visuals
  </done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <what-built>
    Complete VU meter optimization:
    - External vuStore with RAF batching
    - useMixerStore routes VU data to external store
    - SessionTrackVU uses direct DOM manipulation
    - React state completely bypassed for VU updates
  </what-built>
  <how-to-verify>
    1. Start the application:
       ```bash
       cd jam-ui && npm run start
       ```

    2. Open React DevTools Profiler in browser

    3. Join or create a music session with audio input enabled

    4. Start recording in React DevTools Profiler

    5. Play audio through your input for 30+ seconds to generate VU meter activity

    6. Stop profiler recording and examine:
       - SessionTrackVU components should show minimal or zero renders
       - No "Cascading updates" warnings
       - Commit times should be stable (no spikes from VU updates)

    7. Visual verification:
       - VU meters should animate smoothly
       - No visual stuttering or freezing
       - Page should remain responsive during VU activity

    8. Extended test (optional):
       - Leave session open for 5+ minutes with continuous audio
       - Page should remain responsive throughout
  </how-to-verify>
  <resume-signal>Type "approved" if VU meters work correctly with no React re-renders, or describe any issues observed</resume-signal>
</task>

</tasks>

<verification>
1. **No React state for VU:**
   - `grep -r "setVuStates" jam-ui/src/` returns empty
   - `grep "useState" jam-ui/src/hooks/useVuHelpers.js` returns empty

2. **External store wiring:**
   - `grep "vuStore.updateLevel" jam-ui/src/hooks/useMixerStore.js` shows bridge integration
   - `grep "vuStore.getLevelSnapshot" jam-ui/src/components/client/SessionTrackVU.js` shows component integration

3. **Direct DOM updates:**
   - `grep "requestAnimationFrame" jam-ui/src/components/client/SessionTrackVU.js` shows RAF loop
   - `grep "lightsRef" jam-ui/src/components/client/SessionTrackVU.js` shows ref-based DOM access

4. **Build check:**
   - `cd jam-ui && npm run build` completes without errors
</verification>

<success_criteria>
Requirements satisfied:
- VU-01: RAF batching via vuStore flushUpdates loop
- VU-02: SessionTrackVU uses refs for direct DOM updates
- VU-03: VU data stored in external vuStore

Observable outcomes:
- React DevTools Profiler shows 0 re-renders from VU updates
- VU meters animate smoothly at 60fps
- Session screen remains responsive during extended sessions
</success_criteria>

<output>
After completion, create `.planning/phases/28-vu-meter-optimization/28-02-SUMMARY.md`
</output>
