feat: migrar dashboard a React 19 + TypeScript + Vite + Tailwind v4

- Módulo HITL (/cases): 6 patrones de formularios dinámicos para 53 tipos de caso con validación Zod
- Módulo Monitor (/monitor): streaming token-a-token en tiempo real, auto-scroll y notas internas vía WebSocket
- Arquitectura híbrida: REST (canal autoritativo) + WebSocket (difusión/streaming)
- MSW para desarrollo sin backend, hooks de notificaciones/sonido/título preservados
- Backend legacy movido a legacy/, archivos residuales eliminados de raíz
This commit is contained in:
2026-07-23 18:27:03 -05:00
parent 69cc215954
commit 8f044567c0
78 changed files with 11818 additions and 1563 deletions
+114
View File
@@ -0,0 +1,114 @@
import { useRef, useEffect, useCallback } from 'react';
// ─────────────────────────────────────────────────────────────
// Constants
// ─────────────────────────────────────────────────────────────
/** C5 frequency in Hz (523.25) */
const C5 = 523.25;
/** E5 frequency in Hz (659.25) */
const E5 = 659.25;
/** Duration of each tone in seconds (120 ms) */
const TONE_DURATION = 0.12;
/** Overall gain / volume (0.08 = 8%) */
const VOLUME = 0.08;
/** Exponential fade-out duration in seconds (450 ms) */
const FADE_DURATION = 0.45;
// ─────────────────────────────────────────────────────────────
// useSound
// ─────────────────────────────────────────────────────────────
/**
* Hook for playing a notification chime using the Web Audio API.
*
* - Initialises an `AudioContext` lazily on the first user gesture
* (click or keydown) to comply with browser autoplay policies.
* - Exposes `playNotificationSound()` which plays a twotone chime:
* C5 (523.25 Hz) for 120 ms → E5 (659.25 Hz) for 120 ms,
* with a shared exponential fadeout envelope (0.08 → 0.001 over 450 ms).
* - If the `AudioContext` is suspended, a warning is logged and the
* call is silently ignored.
*/
export function useSound() {
const audioCtxRef = useRef<AudioContext | null>(null);
const initializedRef = useRef(false);
// ── Lazy initialisation on first user gesture ──────────────
useEffect(() => {
const initAudio = () => {
if (initializedRef.current) return;
try {
audioCtxRef.current = new AudioContext();
initializedRef.current = true;
} catch (err) {
console.warn('[useSound] Web Audio API is not available:', err);
}
// Remove both listeners after the first gesture
window.removeEventListener('click', initAudio);
window.removeEventListener('keydown', initAudio);
};
// Attach listeners for first gesture
window.addEventListener('click', initAudio, { once: true });
window.addEventListener('keydown', initAudio, { once: true });
return () => {
window.removeEventListener('click', initAudio);
window.removeEventListener('keydown', initAudio);
// Close AudioContext on unmount
const ctx = audioCtxRef.current;
if (ctx) {
ctx.close().catch(() => {});
audioCtxRef.current = null;
}
initializedRef.current = false;
};
}, []);
// ── Play the twotone chime ────────────────────────────────
const playNotificationSound = useCallback((): void => {
const ctx = audioCtxRef.current;
if (!ctx) {
console.warn('[useSound] AudioContext has not been initialised yet');
return;
}
if (ctx.state === 'suspended') {
console.warn('[useSound] AudioContext is suspended — cannot play sound');
return;
}
const now = ctx.currentTime;
// Single shared gain node for both oscillators → unified fadeout
const gain = ctx.createGain();
gain.gain.setValueAtTime(VOLUME, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + FADE_DURATION);
gain.connect(ctx.destination);
// Helper: create and schedule a single sinewave tone
const playTone = (frequency: number, startTime: number): void => {
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(frequency, startTime);
osc.connect(gain);
osc.start(startTime);
osc.stop(startTime + TONE_DURATION);
};
// Schedule the two tones
playTone(C5, now); // C5 starts immediately
playTone(E5, now + TONE_DURATION); // E5 starts after C5 ends
}, []);
return { playNotificationSound };
}