feat(calls): CallKit kill-switch + fix ~1s re-ring on cancel

MIC BROKEN with CallKit: a CallKit call reserves the microphone, so the WebView's
WebRTC can't capture it — calls are unusable until native LiveKit media lands. Add a
server kill-switch (CALLKIT_ENABLED, default OFF) so CallKit can be flipped without an
app rebuild: /api/meetings/config now returns callkit; setupNativeCall bails when off
(-> WebView calls, mic works); push.js only sends VoIP/CallKit pushes when enabled.
Deploying with the flag unset immediately restores working WebView calls.

RE-RING: a late cancel push for an already-declined call hit the plugin's 'unknown
uuid' path and re-reported a fresh incoming call (~1s re-ring). Track endedCalls and
make a late cancel for an already-ended call a no-op.

Server part deploys now (no rebuild); plugin re-ring fix ships with the native build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 17:54:21 +05:30
parent 3e21aa30d7
commit e7c3231c50
5 changed files with 26 additions and 4 deletions
+2 -2
View File
@@ -116,7 +116,7 @@ const API_KEY_SCOPES = ['report:read', 'audit:read'];
const { onlineAgents, meetingRooms, groupCalls, dmCalls } = require('./presence');
const CALLS = require('./calls');
require('./reminders'); // start the 10-minute meeting-reminder loop
const { REC_DIR, TRANS_DIR, UPLOADS_DIR, SESSION_TTL, REFRESH_TTL, LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_ENABLED, PUBLIC_BASE_URL, GIPHY_API_KEY } = require('./config');
const { REC_DIR, TRANS_DIR, UPLOADS_DIR, SESSION_TTL, REFRESH_TTL, LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_ENABLED, PUBLIC_BASE_URL, GIPHY_API_KEY, CALLKIT_ENABLED } = require('./config');
const https = require('https');
// Small GET-JSON helper for the GIPHY proxy (keeps the key server-side).
function fetchJSON(url) {
@@ -995,7 +995,7 @@ route('POST', '/api/calls/invite', async (req, res) => {
// Does this deployment use the LiveKit SFU for meeting media? The client asks on load; if sfu is
// false it uses the built-in P2P mesh. url is the browser-facing signaling endpoint (wss://…).
route('GET', '/api/meetings/config', (req, res) => {
json(res, 200, { sfu: LIVEKIT_ENABLED, url: LIVEKIT_ENABLED ? LIVEKIT_URL : '' });
json(res, 200, { sfu: LIVEKIT_ENABLED, url: LIVEKIT_ENABLED ? LIVEKIT_URL : '', callkit: CALLKIT_ENABLED });
});
// #5 GIF search — server-side GIPHY proxy so the API key never reaches the browser. Empty q → trending.