Add on-air toggle for phone call routing
When off air, callers hear a message and get disconnected. When on air, calls route normally. Toggle button added to frontend header with pulsing red ON AIR indicator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -469,6 +469,7 @@ session = Session()
|
|||||||
caller_service = CallerService()
|
caller_service = CallerService()
|
||||||
_ai_response_lock = asyncio.Lock() # Prevents concurrent AI responses
|
_ai_response_lock = asyncio.Lock() # Prevents concurrent AI responses
|
||||||
_session_epoch = 0 # Increments on hangup/call start — stale tasks check this
|
_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 ---
|
# --- News & Research Helpers ---
|
||||||
@@ -551,6 +552,21 @@ async def index():
|
|||||||
return FileResponse(frontend_dir / "index.html")
|
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 ---
|
# --- SignalWire Endpoints ---
|
||||||
|
|
||||||
@app.post("/api/signalwire/voice")
|
@app.post("/api/signalwire/voice")
|
||||||
@@ -561,6 +577,15 @@ async def signalwire_voice_webhook(request: Request):
|
|||||||
call_sid = form.get("CallSid", "")
|
call_sid = form.get("CallSid", "")
|
||||||
print(f"[SignalWire] Inbound call from {caller_phone} (CallSid: {call_sid})")
|
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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Response>
|
||||||
|
<Say voice="woman">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!</Say>
|
||||||
|
<Hangup/>
|
||||||
|
</Response>"""
|
||||||
|
return Response(content=xml, media_type="application/xml")
|
||||||
|
|
||||||
# Use dedicated stream URL (ngrok) if configured, otherwise derive from request
|
# Use dedicated stream URL (ngrok) if configured, otherwise derive from request
|
||||||
if settings.signalwire_stream_url:
|
if settings.signalwire_stream_url:
|
||||||
stream_url = settings.signalwire_stream_url
|
stream_url = settings.signalwire_stream_url
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ header button {
|
|||||||
cursor: pointer;
|
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 {
|
.new-session-btn {
|
||||||
background: var(--accent) !important;
|
background: var(--accent) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<header>
|
<header>
|
||||||
<h1>Luke at The Roost</h1>
|
<h1>Luke at The Roost</h1>
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
|
<button id="on-air-btn" class="on-air-btn off">OFF AIR</button>
|
||||||
<button id="new-session-btn" class="new-session-btn">New Session</button>
|
<button id="new-session-btn" class="new-session-btn">New Session</button>
|
||||||
<button id="settings-btn">Settings</button>
|
<button id="settings-btn">Settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,6 +57,24 @@ function initEventListeners() {
|
|||||||
// New Session
|
// New Session
|
||||||
document.getElementById('new-session-btn')?.addEventListener('click', newSession);
|
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
|
// Server controls
|
||||||
document.getElementById('restart-server-btn')?.addEventListener('click', restartServer);
|
document.getElementById('restart-server-btn')?.addEventListener('click', restartServer);
|
||||||
document.getElementById('stop-server-btn')?.addEventListener('click', stopServer);
|
document.getElementById('stop-server-btn')?.addEventListener('click', stopServer);
|
||||||
@@ -772,6 +790,12 @@ function log(text) {
|
|||||||
addMessage('System', 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) {
|
function showStatus(text) {
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
|
|||||||
Reference in New Issue
Block a user