diff --git a/backend/main.py b/backend/main.py index eed813d..1d82b9e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -469,6 +469,7 @@ session = Session() caller_service = CallerService() _ai_response_lock = asyncio.Lock() # Prevents concurrent AI responses _session_epoch = 0 # Increments on hangup/call start — stale tasks check this +_show_on_air = False # Controls whether phone calls are accepted or get off-air message # --- News & Research Helpers --- @@ -551,6 +552,21 @@ async def index(): return FileResponse(frontend_dir / "index.html") +# --- On-Air Toggle --- + +@app.post("/api/on-air") +async def set_on_air(state: dict): + """Toggle whether the show is on air (accepting phone calls)""" + global _show_on_air + _show_on_air = bool(state.get("on_air", False)) + print(f"[Show] On-air: {_show_on_air}") + return {"on_air": _show_on_air} + +@app.get("/api/on-air") +async def get_on_air(): + return {"on_air": _show_on_air} + + # --- SignalWire Endpoints --- @app.post("/api/signalwire/voice") @@ -561,6 +577,15 @@ async def signalwire_voice_webhook(request: Request): call_sid = form.get("CallSid", "") print(f"[SignalWire] Inbound call from {caller_phone} (CallSid: {call_sid})") + if not _show_on_air: + print(f"[SignalWire] Show is off air — playing off-air message for {caller_phone}") + xml = """ + + Luke at the Roost is off the air right now. Please call back during the show for your chance to talk to Luke. Thanks for calling! + +""" + return Response(content=xml, media_type="application/xml") + # Use dedicated stream URL (ngrok) if configured, otherwise derive from request if settings.signalwire_stream_url: stream_url = settings.signalwire_stream_url diff --git a/frontend/css/style.css b/frontend/css/style.css index bc2d783..ad79cc6 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -54,6 +54,27 @@ header button { cursor: pointer; } +.on-air-btn { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + transition: background 0.2s; +} + +.on-air-btn.off { + background: #666 !important; +} + +.on-air-btn.on { + background: #cc2222 !important; + animation: on-air-pulse 2s ease-in-out infinite; +} + +@keyframes on-air-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + .new-session-btn { background: var(--accent) !important; } diff --git a/frontend/index.html b/frontend/index.html index 6531719..287e3f1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -11,6 +11,7 @@

Luke at The Roost

+
diff --git a/frontend/js/app.js b/frontend/js/app.js index e2a56a2..a20b275 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -57,6 +57,24 @@ function initEventListeners() { // New Session document.getElementById('new-session-btn')?.addEventListener('click', newSession); + // On-Air toggle + const onAirBtn = document.getElementById('on-air-btn'); + if (onAirBtn) { + fetch('/api/on-air').then(r => r.json()).then(data => { + updateOnAirBtn(onAirBtn, data.on_air); + }); + onAirBtn.addEventListener('click', async () => { + const isOn = onAirBtn.classList.contains('on'); + const res = await safeFetch('/api/on-air', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ on_air: !isOn }), + }); + updateOnAirBtn(onAirBtn, res.on_air); + log(res.on_air ? 'Show is ON AIR — accepting calls' : 'Show is OFF AIR — calls get off-air message'); + }); + } + // Server controls document.getElementById('restart-server-btn')?.addEventListener('click', restartServer); document.getElementById('stop-server-btn')?.addEventListener('click', stopServer); @@ -772,6 +790,12 @@ function log(text) { addMessage('System', text); } +function updateOnAirBtn(btn, isOn) { + btn.classList.toggle('on', isOn); + btn.classList.toggle('off', !isOn); + btn.textContent = isOn ? 'ON AIR' : 'OFF AIR'; +} + function showStatus(text) { const status = document.getElementById('status');