From 13d03dd51421e9a88b04a3679b97cb9dda36444d Mon Sep 17 00:00:00 2001 From: chaosste <126386863+chaosste@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:59:02 +0000 Subject: [PATCH] Add safety lock frequency guardrails and fix audio stream cleanup --- App.tsx | 35 +++++++++++++++++++++++++++++++++-- components/StrobeCanvas.tsx | 7 +++++-- services/proxyService.ts | 9 ++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/App.tsx b/App.tsx index c355e73..ee8fd1b 100644 --- a/App.tsx +++ b/App.tsx @@ -4,19 +4,27 @@ import StrobeCanvas from './components/StrobeCanvas'; import { AppState, StrobeStyle } from './types'; const App: React.FC = () => { + const SAFE_MAX_HZ = 20; + const ABSOLUTE_MAX_HZ = 60; const [state, setState] = useState(AppState.DISCLAIMER); const [frequency, setFrequency] = useState(10); const [colors, setColors] = useState(['#ffffff', '#000000']); const [style, setStyle] = useState(StrobeStyle.FIXED); const [isActive, setIsActive] = useState(false); const [audioLevel, setAudioLevel] = useState(0); + const [safetyLock, setSafetyLock] = useState(true); const audioContextRef = useRef(null); const analyserRef = useRef(null); + const streamRef = useRef(null); const handleStart = () => setState(AppState.IDLE); const safeCloseAudioContext = async () => { + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + } if (audioContextRef.current && audioContextRef.current.state !== 'closed') { try { await audioContextRef.current.close(); @@ -37,6 +45,7 @@ const App: React.FC = () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); if (!isMounted) return; + streamRef.current = stream; const ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); const analyser = ctx.createAnalyser(); @@ -136,6 +145,7 @@ const App: React.FC = () => { isActive={isActive} style={style} audioLevel={audioLevel} + maxFrequency={safetyLock ? SAFE_MAX_HZ : ABSOLUTE_MAX_HZ} /> {/* Persistent Play/Pause Button - Responsive to selected theme color */} @@ -171,10 +181,31 @@ const App: React.FC = () => { {style === StrobeStyle.AUDIO ? 'VOX' : `${frequency.toFixed(1)}Hz`} { setFrequency(parseFloat(e.target.value)); setStyle(StrobeStyle.FIXED); }} + type="range" min="0.5" max={safetyLock ? SAFE_MAX_HZ : ABSOLUTE_MAX_HZ} step="0.5" value={frequency} + onChange={(e) => { + const next = parseFloat(e.target.value); + setFrequency(next); + setStyle(StrobeStyle.FIXED); + }} className="w-full h-1.5 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-blue-500" /> +
+ + {!safetyLock && Advanced mode} +
diff --git a/components/StrobeCanvas.tsx b/components/StrobeCanvas.tsx index ce15fdf..a90f2dc 100644 --- a/components/StrobeCanvas.tsx +++ b/components/StrobeCanvas.tsx @@ -8,6 +8,7 @@ interface StrobeCanvasProps { isActive: boolean; style: StrobeStyle; audioLevel?: number; // 0 to 1 + maxFrequency?: number; } const StrobeCanvas: React.FC = ({ @@ -15,7 +16,8 @@ const StrobeCanvas: React.FC = ({ colors, isActive, style, - audioLevel = 0 + audioLevel = 0, + maxFrequency = 60 }) => { const canvasRef = useRef(null); const lastToggleTimeRef = useRef(0); @@ -33,8 +35,9 @@ const StrobeCanvas: React.FC = ({ let effectiveFreq = frequency; if (style === StrobeStyle.AUDIO) { - effectiveFreq = audioLevel * 60; // Up to 60Hz + effectiveFreq = audioLevel * maxFrequency; } + effectiveFreq = Math.min(effectiveFreq, maxFrequency); const cycleMs = effectiveFreq > 0 ? 1000 / (effectiveFreq * 2) : Infinity; diff --git a/services/proxyService.ts b/services/proxyService.ts index 1595dba..ecd298e 100644 --- a/services/proxyService.ts +++ b/services/proxyService.ts @@ -5,6 +5,7 @@ */ const PROXY_URL = import.meta.env.VITE_PROXY_URL || 'https://gemini-proxy-572556903588.us-central1.run.app'; +const PROXY_TIMEOUT_MS = Number(import.meta.env.VITE_PROXY_TIMEOUT_MS || 20000); interface GenerateContentRequest { model: string; @@ -27,12 +28,16 @@ interface GenerateContentResponse { export const generateContent = async ( request: GenerateContentRequest ): Promise => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), PROXY_TIMEOUT_MS); + try { const response = await fetch(`${PROXY_URL}/v1/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, + signal: controller.signal, body: JSON.stringify({ model: request.model, prompt: request.contents, @@ -43,7 +48,7 @@ export const generateContent = async ( }); if (!response.ok) { - const errorText = await response.text(); + const errorText = await response.text().catch(() => ''); throw new Error(`Proxy request failed: ${response.status} - ${errorText}`); } @@ -55,6 +60,8 @@ export const generateContent = async ( } catch (error) { console.error('Proxy service error:', error); throw error; + } finally { + clearTimeout(timeoutId); } };