Replaces WebView call media on Android with a native LiveKit connection (iOS
parity), so call audio survives backgrounding and the output can be switched
to Speaker/Bluetooth/Earpiece — the #3 (missing speaker control) and #4
(background audio) items.
Gated behind a new, default-off server flag ANDROID_NATIVE_MEDIA (independent
of ANDROID_CALLS, which only governs the native ring). Flag off = today's
WebView media, video included; flag on = native audio (video is Increment 2b,
so enable it for VOICE testing first). No behavior change until flipped.
Native (mobile/plugins/native-call/android):
- build.gradle: add kotlin-android + io.livekit:livekit-android 2.29.2 (+ twilio
audioswitch, coroutines) + JitPack repo; minSdk 24 (LiveKit floor).
- CallMedia.kt: LiveKit Room wrapper — connect/mic/disconnect + AudioSwitchHandler
routing (auto + selectDevice) and route-change events.
- CallForegroundService.java: microphone-type foreground service for background
audio (no-ops without RECORD_AUDIO so it never crashes the call).
- NativeCallPlugin.java: real bodies for reportOutgoingCall/reconnectRoom/setMuted
/endCall + new setAudioRoute/getAudioRoute; fires callConnected/callError/audioRoute.
- AndroidManifest: FG service + FOREGROUND_SERVICE(_MICROPHONE)/RECORD_AUDIO/
MODIFY_AUDIO_SETTINGS/BLUETOOTH_CONNECT.
- android-patch.js: bump the generated app minSdk to 24.
Server:
- config.js/routes.js: ANDROID_NATIVE_MEDIA → /api/meetings/config.androidNativeMedia.
Web (home.html):
- Android flips to the native-media path when androidNativeMedia is set; the
existing outgoing/answer/mic/end code then drives the plugin unchanged.
- Speaker button wired for Android native: cycles speaker/earpiece/bluetooth via
NC.setAudioRoute and reflects the real output from the audioRoute event.
Video stays no-op on the native path (Increment 2b).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of Android Phase C (full LiveKit parity), mirroring how iOS
started: native full-screen incoming call UI that wakes the app from
closed, with call MEDIA still in the WebView (foreground). Native LiveKit
media + a foreground service for true background audio are Increment 2.
Native (new Android side of the native-call plugin, Java to match the
proven official-plugin module layout):
- NativeCallPlugin: registered as window.Capacitor.Plugins.NativeCall
(same jsName as iOS, so the shared web bridge drives both). getToken
(FCM token), reportIncomingCall (ring), reportOutgoingCall, endCall,
setMuted; video/screen/hole-punch/transcript methods are safe no-ops
until later increments so the shared bridge never rejects.
- BzCallMessagingService: extends @capacitor/push-notifications'
MessagingService — handles high-priority call-wake DATA messages
(data.bzcall==1) to ring when closed, forwards all other push via super.
- IncomingCallActivity: full-screen call screen over the lock screen
(showWhenLocked/turnScreenOn), Accept/Decline, looping ringtone.
- CallStore: incoming-call notification (channel, full-screen-intent,
actions) + UUID dedup so the FCM and WebSocket paths don't double-ring.
- Plugin manifest: BzCallMessagingService + IncomingCallActivity + call
perms (USE_FULL_SCREEN_INTENT/VIBRATE/WAKE_LOCK).
- android-patch.js: removes Capacitor's MessagingService in the APP
manifest (highest merge priority) so ours is the sole MESSAGING_EVENT
receiver; ensures the tools namespace. Idempotent.
- package.json: capacitor.android.src wired.
Server (dormant until ANDROID_CALLS=1):
- push.js sendFcmData: high-priority data-only FCM (rings when closed).
sendCallNotification rings 'android' tokens via it; sendCallCancel too.
- config.js ANDROID_CALLS flag; /api/meetings/config exposes androidCall.
Web (home.html) — platform-aware, iOS/web behavior byte-for-byte unchanged:
- setupNativeCall: Android gated on cfg.androidCall, sets _callWebViewMedia
(native ring, WebView media); iOS keeps CallKit native-media path.
- answerCall + both outgoing sites branch on _callWebViewMedia so Android
joins the meeting normally while the callee still rings via the server.
Safe to deploy now: dormant until ANDROID_CALLS=1 AND an APK carrying the
plugin is installed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session (New): stop the ~24h auto-logout. SESSION_TTL 24h -> 90d, and /api/me now
SLIDES the session forward + re-stamps the cookie on every app load / focus /
6h heartbeat — so an actively-used session never lapses; you only log out by
choosing to. Login no longer depends on "remember me".
#2 A new message in the chat you're actively viewing now plays a soft, distinct
in-chat tone (playMsgTone) — no popup — instead of being silent. A different
chat / a backgrounded chat still gets the alert ping + notification.
#13 Pin/unpin is now written to the audit log (actor + which message, and whose pin
was removed on an unpin) — the accountability gap when anyone can unpin.
Pagination: a floating "Loading earlier messages…" pill now shows while older
history is being fetched (loadOlder had no visible indicator).
#9 Mobile long-press now opens a dimmed + blurred bottom ACTION SHEET (quick
reactions + reply/edit/forward/copy/pin/delete) instead of the flaky hover-style
reveal that hid behind images and broke after the lightbox opened.
#14 editTarget is cleared on conversation switch — starting an edit then switching
chats used to leave editTarget set, which silently stopped ALL draft saving.
Also added Edit to the shared action list so mobile long-press can edit too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Server: GET /api/gifs proxies GIPHY search/trending. The API key is read from the server
env only (config.GIPHY_API_KEY, from the gitignored .env) and NEVER reaches the browser;
the picker is hidden when it isn't configured.
- Emoji picker gains a GIF tab (separated from the emoji categories) with a search box + a 2-col
grid, "Powered by GIPHY" attribution. Clicking a GIF sends it immediately.
- GIFs are HOTLINKED to GIPHY's CDN (their terms require this — no re-hosting): the message body
is the GIF url, and a body that is a lone GIF url renders inline as the animated GIF (reusing
the image/lightbox path). Sidebar previews + notifications show "🎞️ GIF", not the raw url.
Key is NOT in git — set as GIPHY_API_KEY in the server .env.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#4: scheduled meetings can now invite people who aren't on Connect.
- SMTP config (config.js, env-gated: SMTP_HOST/PORT/USER/PASS/FROM/SECURE,
PUBLIC_BASE_URL) + a small nodemailer wrapper (mailer.js) with a branded
meeting-invite template carrying the guest join link. No-op until SMTP is set.
- /api/meetings/schedule + /update accept participantEmails; external emails are
persisted (scheduled_meetings.guest_emails migration) and emailed the guest
link (plus any invited Connect users with an email on file). Fire-and-forget —
a mail outage never fails scheduling.
- Meetings list DTO returns `link`; schedule form gains an "Invite by email"
chip input; each scheduled-meeting card gets a "Copy link" action.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Broken images: UPLOADS_DIR/REC_DIR/TRANS_DIR were server/<dir> INSIDE the image,
so every deploy.sh rebuild wiped uploaded files — old images 404'd ('broken
image') though their DB rows survived. Make them env-overridable and point prod
at /data/uploads|recordings|transcripts (persistent volume), matching DB/downloads.
NOTE: files already lost to prior rebuilds can't be recovered; new uploads persist.
- Duplicate notifications: harden connectChatWs — close/detach any prior socket
before opening a new one and keep a single pending reconnect timer, so a flaky
reconnect can't leave two live sockets delivering every event/notification twice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the server side of scaling meetings past the ~5-peer mesh:
- config.js: LIVEKIT_URL/API_KEY/API_SECRET + LIVEKIT_ENABLED flag. All optional;
when unset the app keeps the built-in P2P mesh (fully additive, like push).
- routes.js: GET /api/meetings/config (tells the client sfu on/off + wss url) and
POST /api/meetings/token (mints a per-user, per-room LiveKit join token — hand-rolled
HS256 JWT like the FCM/APNs tokens, no new dependency; secret stays server-side).
- docker-compose.yml: optional livekit service (single-node, no Redis), keys injected
via LIVEKIT_KEYS from the same .env; media over published UDP 50000-50100 + TCP 7881,
signaling proxied by NPM.
- livekit.yaml + .env.example documented.
Client (mesh->LiveKit media swap, behind the flag) lands in phase 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Downloads/updates:
- config: DOWNLOADS_DIR (override to a mounted volume in prod).
- static.js: serves /downloads/* (installer, latest.yml, .blockmap) with range support for
resumable + differential auto-updates; /download/windows redirects to the current .exe
(stable link). Landing page gets a "Download the Windows desktop app" button (hidden in-app).
Install tracking (who installed the app):
- db app_installs + repos.appInstalls (upsert by install_id, fills in the user on sign-in).
- POST /api/v1/telemetry/install (records install + user once authenticated);
GET /api/v1/admin/installs (admin: list installs with user/version/os/last-seen).
- desktop main.js: stable per-install id in userData, exposed via preload
(bizConnectNative.installId/version/os); home.html reportInstall() posts it after login.
- e2e: +2 checks (telemetry recorded, admin sees it). 119/119. build batch20.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User-facing
- New post-login home (/home): chat rail + Share/Connect (embedded) + Meeting; login lives here when logged out
- Landing: "Log in with BizGaze" + no-login screen share
- Console replaced by a role-scoped Dashboard (/dashboard): admins see all team sessions, others see only their own; stats + CSV/PDF export
- Recordings saved as MP4 (H.264/AAC) with WebM fallback; old .webm still downloadable
- Fix: duplicate "Sign in" on the login card
Auth / integration
- BizGaze as identity provider: /api/login validates against BIZGAZE_LOGIN_URL (env-gated) and provisions a local user
- Phase 2 start: /api/v1 alias for all /api routes; Authorization: Bearer accepted across HTTP + WS; login returns a token (for native desktop/mobile clients)
Backend refactor (Phase 1, behavior-preserving)
- Split server.js into config/lib/session/presence/routes/static/signaling + repos (data-access) + bizgaze (service)
- All SQL behind repos.js, tenant-scoped (tenantId == team_id for now)
- e2e updated to current flow (21/21 pass before and after)
Docs: ARCHITECTURE.md (target architecture + phased plan), CLAUDE.md repo layout, .env.example BIZGAZE_LOGIN_URL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>