Commit Graph
399 Commits
Author SHA1 Message Date
SravanandClaude Opus 4.8 3b5dd81016 android(call): shared screen fits (contain) in portrait, not center-cropped
Received shared screen fit the window in landscape but was center-cropped in
portrait (only the middle showed). The LiveKit TextureViewRenderer's own
SCALE_ASPECT_FIT didn't letterbox a wide screen in a tall tile — it filled and
cropped. Fix: compute the contain (letterbox) rect ourselves and size the
renderer to it, centred in the tile, then FILL (renderer == video aspect → no
crop), so the WHOLE screen shows in both orientations. Landscape unchanged
(tile aspect ~= video aspect → rect ~= full tile).

The live frame aspect comes from a small VideoSink attached to the screen
track alongside the renderer; frameAspect is a ConcurrentHashMap (written on
the render thread, read on the UI thread). Sinks + aspect are cleaned up on
tile removal / track change / hole-punch teardown. Camera tiles unchanged
(full-tile cover). Pinch-zoom still applies on top of the fitted renderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-04 22:39:24 +05:30
SravanandClaude Opus 4.8 db6c00cbcf fix(meet): send peerId (not user id) to setTileZoom so screen zoom works
Pinch-to-zoom on a received shared screen was a no-op on Android (and iOS).
syncVideoTiles keys each native renderer by the tile's peerId (uid=id, the
LiveKit identity since #12), but the pinch handler forwarded
meetPeerUids.get(id) — the USER id — to setTileZoom. The plugin looked up the
renderer by that mismatched key and found nothing, so the zoom transform never
applied. Send `id` (the peerId), matching syncVideoTiles. Web-only fix; the
native setTileZoom was already correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 22:49:02 +05:30
SravanandClaude Opus 4.8 1e01130c6f android(call): Increment 2c — screen-share + received-screen pinch-zoom
Screen-share (send): startScreenShare requests the MediaProjection grant via
startActivityForResult, then CallMedia.setScreenShareEnabled(true,
ScreenCaptureParams(data)) publishes it over the native LiveKit room.
stopScreenShare unpublishes. Both emit screenShareState so the web button
reflects reality (incl. a declined grant). LiveKit's SDK manifest already
provides the mediaProjection foreground service + FOREGROUND_SERVICE_MEDIA_
PROJECTION (merged into the app), so no manifest change is needed.

Received shared screen: implemented setTileZoom (was a no-op) — the web
captures pinch/pan under hole-punch and forwards {scale,tx,ty}; CallVideo
applies it as a renderer transform (scale about centre + translate ×density),
scale<=1 clears it. Mirrors the iOS applyZoom contract. Screen tiles already
render aspect-fit.

APK-only: the web bridge already calls these methods and received-screen
rendering is gated on ANDROID_NATIVE_VIDEO (already on) — no server/web change.
All LiveKit 2.29.2 APIs (setScreenShareEnabled, ScreenCaptureParams) verified
against source. Clarity-when-zoomed (adaptive-stream) is a follow-up if needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 22:14:42 +05:30
SravanandClaude Opus 4.8 a1f7eef28a build: pre-deploy HTML inline-script syntax guard
A missing brace in a home.html inline <script> caused an app-wide login
outage (the whole block failed to parse, so boot never ran). Nothing in the
deploy path checked HTML syntax. Add tools/check-syntax.js: parses (never
runs) every inline <script> in server/public/*.html with vm and exits non-zero
on any syntax error, reporting the file + line.

Wired in two places:
- Dockerfile RUN after COPY server/ ./ — a syntax error now FAILS the build,
  so it can't deploy.
- redeploy.sh pre-flight — fast local feedback before triggering the remote
  build.

Verified: passes on current pages (16 blocks / 9 files); fails with the exact
location when a brace is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 21:33:01 +05:30
SravanandClaude Opus 4.8 c9bb9c75d1 fix(home): close callConnected arrow body — syntax error broke app boot
Production outage: after login the app hung forever on the splash. Chrome
showed "Uncaught SyntaxError: Unexpected token ')' at home:4398". The
callConnected listener's arrow body `()=>{ ... }` was missing its closing
brace (brace balance +1 on the line), so the whole inline <script> block
failed to parse — every boot function (loadSidebar, the boot IIFE) was never
defined, so the splash never hid. The login screen still rendered because it
lives in an earlier script block; only this block died.

The bug rode in with the Increment-2a callConnected wiring and was masked for
a while by service-worker caching (users' "quit and reopen fixed it"); once
the broken home.html was served fresh everywhere it became a total login hang,
on web and desktop alike. Verified all inline blocks parse after the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 21:24:57 +05:30
SravanandClaude Opus 4.8 0129489746 fix(db): pool safety timeouts to stop login hangs (connection exhaustion)
Production outage: logins (and eventually all requests) hung forever. Root
cause: the pg Pool was max:10 with NO connectionTimeoutMillis, so once all 10
connections were busy/stuck, every new query waited indefinitely for a free
client — the "login spinner that never ends". The reject path stayed fast
(one brief query); the success path needs several writes and blocked on
acquiring a connection. Server-side only (reproduced in a desktop browser).

Add fail-fast + self-healing pool config (all env-tunable):
- connectionTimeoutMillis (10s): callers error instead of hanging on a
  saturated pool.
- statement_timeout (60s): kill a runaway query so it can't pin a connection.
- idle_in_transaction_session_timeout (60s): reclaim a client left idle in a
  hung transaction (the classic slow pool leak).
- max 10 -> 20 for headroom; pool 'error' handler so a broken idle client
  can't crash the process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 21:00:18 +05:30
SravanandClaude Opus 4.8 4baace83bc android(call): Increment 2b — native camera video + hole-punch tiles
Mirrors the iOS native-video path on Android, gated by ANDROID_NATIVE_VIDEO.

- CallVideo (new): hole-punch (transparent WebView over a black parent with a
  container behind) + per-tile LiveKit TextureViewRenderer, positioned from the
  web's CSS-px tile rects scaled by display density. TextureView (not Surface)
  so it composites behind the transparent WebView. Local camera mirrored;
  camera = aspect-fill, screen = aspect-fit; stale renderers released.
- CallMedia: setCameraEnabled / switchCamera (LocalVideoTrack.switchCamera) +
  cameraTrack/screenTrack lookup (by LiveKit identity = mesh peerId) +
  initRenderer. All LiveKit 2.29.2 APIs verified against source.
- NativeCallPlugin: setCamera / switchCamera / setHolePunch / syncVideoTiles get
  real bodies (+ CAMERA runtime-permission prompt, video teardown on endCall).
  Screen-share (MediaProjection), tile zoom and #5 transcript stay no-ops.

Web needs no change: the shared NativeCall bridge already drives these for any
meetNative call; bzNativeVideoOn() enables hole-punch + the tile poll on Android
once the flag is on. Flip ANDROID_NATIVE_VIDEO=1 after a 2b APK is installed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 20:26:31 +05:30
SravanandClaude Opus 4.8 ae607f44b5 fix(android-call): no hole-punch without native video (white meeting bg)
On an Android native call the WebView turned on hole-punch (transparent
meeting grid, so native video can show BEHIND the WebView) whenever the
plugin exposed setHolePunch — but on the 2a audio-only build there is no
native video behind it, so the transparent grid revealed the white Android
window (reported: white meeting background; tiles stayed visible as they're
opaque web elements). iOS never showed this because CallKit covers the view.

Gate hole-punch on native video actually existing: new ANDROID_NATIVE_VIDEO
server flag (mirrors ANDROID_NATIVE_MEDIA, default off) exposed via
/api/meetings/config. Web: bzNativeVideoOn() — iOS always, Android only when
the flag is on (Increment 2b APK). While off, hole-punch stays off and the
grid keeps its dark background; the native-tile poll doesn't run. iOS
unchanged. Flip the flag on once the 2b native-video APK is installed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-11-03 20:12:21 +05:30
SravanandClaude Opus 4.8 ea4d65274e android(call): fix LiveKit audio API import + audioswitch duplicate-class
JitPack fix worked; two new failures surfaced, both fixed here:

1) checkDebugDuplicateClasses: LiveKit 2.29.2 already BUNDLES a fork
   (com.github.davidliu:audioswitch) carrying the same com.twilio.audioswitch
   classes at runtime, so our explicit `implementation com.twilio:audioswitch`
   put both on the runtime classpath. Switch it to `compileOnly` — we get the
   AudioDevice types at compile time, runtime uses only LiveKit's bundled fork.
   (Also clears the "namespace used in multiple modules" merge warning.)

2) CallMedia.kt: `Unresolved reference: AudioOptions`. In 2.29.2 AudioOptions
   is declared in package io.livekit.android (inside LiveKitOverrides.kt), not
   io.livekit.android.audio. Corrected the import. Verified AudioSwitchHandler's
   API (registerAudioDeviceChangeListener / availableAudioDevices /
   selectedAudioDevice / selectDevice) against the v2.29.2 source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-30 20:42:00 +05:30
SravanandClaude Opus 4.8 7cd3c3afff android(build): inject JitPack repo so LiveKit's audioswitch fork resolves
Codemagic build failed at :app:checkDebugAarMetadata — "Could not find
com.github.davidliu:audioswitch". LiveKit Android 2.29.2 pulls in that
JitPack-only fork (commit-hash version), and Gradle resolves the whole
:app transitive graph against the APP module's (+ allprojects) repos, not
the native-call plugin's. The plugin-level jitpack repo was therefore
ignored. android-patch.js now injects `maven { url 'https://jitpack.io' }`
into the generated root build.gradle's allprojects{} (and settings.gradle
dependencyResolutionManagement{} if a newer template uses it). Idempotent,
build-tolerant. TECHSTACK rev 3 note updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-30 19:11:23 +05:30
SravanandClaude Opus 4.8 0b3a8b0955 docs(techstack): record Android native call media (Increment 2a)
LiveKit Android 2.29.2 + audioswitch + Kotlin in the native-call plugin, the
microphone foreground service, the ANDROID_NATIVE_MEDIA flag, and minSdk 24.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-30 17:27:01 +05:30
SravanandClaude Opus 4.8 aa4aeeaba1 android(call): Increment 2a — native LiveKit call audio, background service, speaker/BT routing
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>
2026-10-30 17:25:18 +05:30
SravanandClaude Opus 4.8 08eff043ec call: stop the ring on a user's other devices when one answers (answered-elsewhere)
A user signed in on two devices (iPhone + Android) rang on both. When they
answered on Android, the iPhone kept ringing: markDmAnswered only emitted a
WS 'call-taken', which never reaches a backgrounded/killed device ringing on
CallKit (no live socket).

Now markDmAnswered also pushes a native cancel to the answerer's OTHER
devices. The device that actually answered is skipped via exceptToken (its own
call-push token) — critical on iOS, where a cancel would end the just-answered
CallKit call; Android's cancel branch never tears down a live call anyway.

- push.sendCallCancel(userId, uuid, { exceptToken, reason }) — skips the
  answering device's token; existing 2-arg callers unchanged.
- calls.markDmAnswered(room, userId, exceptToken) — fires the cancel.
- POST /api/calls/answered now takes deviceToken.
- home.html: the native answerCall handler POSTs /api/calls/answered with this
  device's own token (ios-voip or fcm), so native/LiveKit calls (which bypass
  the mesh) are marked answered and siblings stop ringing.

Server + web only — no native build needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-30 16:38:51 +05:30
SravanandClaude Opus 4.8 aa794102c3 android(call): volume key silences the ring like a real phone
The incoming ring is driven by our own Ringer (we aren't a telecom
ConnectionService yet), so the OS doesn't route volume keys to silence
it. Capture them two ways that together cover every state:

- Foreground/locked: IncomingCallActivity.dispatchKeyEvent consumes
  VOLUME_UP/DOWN/MUTE and silences.
- Unlocked/backgrounded heads-up (no activity): a MediaSession with a
  remote VolumeProvider so the framework delivers volume presses to us.

Silence stops audio + vibration but keeps the call ringing visually
(notification + full-screen stay) so answer/decline still work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-29 22:10:17 +05:30
SravanandClaude Opus 4.8 7ec6c3f227 docs(techstack): add Identity & auth section
Document the auth/identity subsystem: crypto-only (scrypt + TOTP MFA),
opaque sliding sessions over cookie/bearer/query transports, rotating
native refresh tokens, BizGaze IdP + signed SSO, roles & tenancy,
bzc_ API keys, server-side directory search, and the audit log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-27 20:29:59 +05:30
SravanandClaude Opus 4.8 5f755fbf6c Add TECHSTACK.md — living tech-stack reference
Full stack: plain Node (http/https + ws, custom router, no framework),
PostgreSQL 16 (SQLite retired), vanilla-JS web SPA, Capacitor 8 mobile with
custom native-call/audio/media/share plugins, two Electron clients, LiveKit
SFU + WebRTC mesh, Web Push/FCM/APNs, Docker + Nginx Proxy Manager, Codemagic
CI. Includes a maintenance rule + changelog so it's kept current.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-27 12:16:20 +05:30
SravanandClaude Opus 4.8 0edbc3f459 Android calls: ring sounds even when unlocked (call-scoped Ringer)
On an unlocked Android 14 phone the OS shows our call as a heads-up and does
NOT launch the full-screen IncomingCallActivity (full-screen-intent is
restricted for non-dialer apps while unlocked), so the ring — which lived in
the activity — was silent when unlocked (it rang only when locked, where the
activity launches).

Move the ring to a call-scoped Ringer (looping ringtone + vibration) started
in CallStore.showIncoming and stopped in CallStore.cancel (which every
answer/decline/cancel/end path already calls). The notification channel stays
silent, so there's still a single ring source. IncomingCallActivity no longer
rings — it's just UI — and no longer stops the ring on destroy (backgrounding
the call screen keeps ringing like a real call until answer/decline/timeout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 21:36:35 +05:30
SravanandClaude Opus 4.8 da5b44c0a5 Call push: ring ALL of a user's devices, not just the first
sendCallNotification returned after the iOS VoIP branch, so a user signed in
on both an iPhone (ios-voip token) and an Android phone only got the call on
iOS — the Android invite was never sent, so a swiped-away Android phone never
rang (confirmed on-device: only type=cancel reached Android, never
type=invite, since sendCallCancel does fan out to all platforms). Now send to
iOS VoIP AND Android FCM, and only fall back to the banner when no native
device was waked. Server-only; no app rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-19 20:40:46 +05:30
SravanandClaude Opus 4.8 cab0c79f57 Android calls: single-source ring (fix double-ring), drop battery dialog, add wake logging
On-device (OnePlus, build 5cb3d0c): new ring UI confirmed; backgrounded rang
twice (channel sound + activity ringtone, and both wake paths raised it);
battery dialog deemed unprofessional (WhatsApp/Teams don't prompt it).

- Double-ring: notification channel is now SILENT (new id bzc_incoming_calls_v2,
  no sound/vibration, onlyAlertOnce) so IncomingCallActivity is the single ring
  source (looping ringtone + vibrator). showIncoming dedups atomically
  (if(!active.add(uuid)) return) so the FCM push and WebSocket paths can't both
  ring the same call.
- Battery dialog removed: dropped requestCallReliability (web call + plugin
  method) and REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission.
- Logging: Log.i(bzcall, ...) across the wake chain to diagnose swiped-away
  (OnePlus force-kills on swipe and blocks FCM cold-start).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-28 18:53:06 +05:30
SravanandClaude Opus 4.8 5cb3d0c122 Android calls: professional incoming UI + killed-state ring reliability
Addresses on-device feedback from Increment 1:
- #2 Professional incoming screen: IncomingCallActivity redesigned — brand-
  blue backdrop, circular avatar with the caller's initial, name + call
  type, and two round icon buttons (red hang-up = Decline, green phone =
  Accept) instead of the full-screen colored buttons. New vector drawables
  ic_call_answer / ic_call_decline (also used on the notification actions).
- #1 Killed-state ring: the missed-call notification arriving while killed
  proves FCM reaches the app, so the ring (high-priority DATA) is being
  held by OEM battery management (OnePlus/Oppo Doze) until the call is over.
  Added REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + a requestCallReliability()
  plugin method that prompts the "allow background activity" dialog once;
  the web calls it on Android call setup. (OEM auto-launch is a manual
  toggle with no API — guide the user.)

#3 (speaker/Bluetooth) and #4 (background audio) ride Increment 2 (native
LiveKit Android media + foreground service) — kept separate so the LiveKit
dependency can't block these UI/reliability fixes (the iOS coupling lesson).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-27 15:15:18 +05:30
SravanandClaude Opus 4.8 b60ce6cda3 Android native calling — Increment 1: full-screen incoming ring + answer
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>
2026-09-25 11:44:52 +05:30
SravanandClaude Opus 4.8 a8e0f549d4 Fix Android keyboard gap: don't double-offset the composer
Device logs showed Android's interactive-widget=resizes-content already shrinks
the layout viewport to above the keyboard (innerHeight 800->472 for a 328px
keyboard), yet the app also lifted the composer by --kb=328 → a keyboard-height
white gap. Now gate the manual --kb lift to iOS only (WKWebView doesn't resize);
on Android keep --kb=0 and just toggle kb-open so --sab collapses and the composer
sits flush to the keyboard. Also removes the temporary kbdiag logging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-25 10:49:45 +05:30
SravanandClaude Opus 4.8 1ece8e1061 System bars: lock in Option 1 (brand-blue bars) on iOS + Android
User picked the brand-blue look for both platforms. Apply .sysbar-blue on any
native platform; set white icons via the safe-area plugin on Android and via
@capacitor/status-bar on iOS. capacitor.config statusBarStyle/navigationBarStyle
-> DARK (light icons) so native builds match before JS runs. iOS is live, so this
web change reaches it immediately — verify on an iPhone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 18:13:55 +05:30
SravanandClaude Opus 4.8 03de639e1a Android system bars — Option 2 preview: white bars + brand separator line
Swap the class to sysbar-sep (thin 1.5px brand line under the status bar + above
the nav bar, bars stay white) and icons back to dark. Second of the two looks for
the user to compare; pick one after this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 18:09:39 +05:30
SravanandClaude Opus 4.8 133208ae3f Android system bars — Option 1 preview: brand-blue bars + white icons
Fill the status/nav inset regions with brand blue (#1F3B73) via html.sysbar-blue
pseudo-elements and switch the bar icons to light so they read on blue. This is
one of two looks for the user to compare on-device; Option 2 (white + separator)
is already in the CSS and swaps in by changing the class + icon style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 18:07:31 +05:30
SravanandClaude Opus 4.8 703cf1fcf0 TEMP: keyboard diagnostics for the Android repeat-open gap
Behavior-neutral console logging on keyboard show/hide (kb height, innerHeight,
clientHeight, visualViewport height, --kb) to determine whether the Android
WebView resizes (interactive-widget) or relies on the manual --kb lift, and to
spot any residual between the first and second open. Remove once fixed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 18:00:47 +05:30
SravanandClaude Opus 4.8 9d5af66a0e Android: dark system-bar icons (visible on the white app)
Under Android edge-to-edge the status/nav bar icons default to light (white) and
vanished on the white app (clock/battery/nav invisible). Set SafeArea
statusBarStyle/navigationBarStyle to LIGHT (= dark content on a light background)
in capacitor.config.json, and add a runtime SafeArea.setSystemBarsStyle call in
home.html so already-installed APKs get dark icons on relaunch (no rebuild). iOS
unaffected (its status bar already shows dark content on white).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 00:24:39 +05:30
SravanandClaude Opus 4.8 ab6d8161ee push: accept FCM_SERVICE_ACCOUNT_B64 (base64 service-account key)
Base64 is a single env-safe token, so the FCM service-account key can live in
.env without the quoting/interpolation hazards of inline JSON. Falls back to the
existing FCM_SERVICE_ACCOUNT (inline JSON or file path). No behavior change when
neither is set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 00:04:31 +05:30
SravanandClaude Opus 4.8 c013536e2b Android: wire Firebase google-services.json into the build (client half)
Commit the client Firebase config (bizgaze-connect, package com.bizgaze.connect)
and have the Codemagic Android step copy it into android/app/ so the google-services
Gradle plugin applies and FirebaseApp initializes — fixing the root cause of the
"Default FirebaseApp is not initialized" crash. google-services.json is client-side
(ships in the APK), safe for this private repo. The service-account key stays OUT of
git (.gitignore: *firebase-adminsdk*.json / *service-account*.json) — it goes on the
server as FCM_SERVICE_ACCOUNT separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-17 19:22:22 +05:30
SravanandClaude Opus 4.8 ba75dc8daa Android: fix black gap above keyboard (SafeArea/SystemBars config)
The safe-area plugin logged: set SystemBars.insetsHandling to "disable". Under
Android edge-to-edge (targetSdk 36) this + offsetForKeyboardInsetBug were causing
a black strip between the input and the soft keyboard on the login view. Set
SystemBars.insetsHandling="disable" and SafeArea.offsetForKeyboardInsetBug=true.
Native config — takes effect on the next Android build; verify on device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 21:57:35 +05:30
SravanandClaude Opus 4.8 a4726dd10c Fix Android crash: don't register push without Firebase
On Android, PushNotifications.register() calls FirebaseMessaging.getInstance(),
which throws "Default FirebaseApp is not initialized" when the build has no
google-services.json. Capacitor runs the plugin method on its own thread, so the
exception is an UNCAUGHT NATIVE crash a JS try/catch can't stop — the app died
right after sign-in and on every relaunch.

Gate Android push registration on a new server flag: /api/meetings/config now
returns fcm (push.fcmReady() = FCM_SERVICE_ACCOUNT present). setupNativePush skips
register() on Android unless fcm is true. Web-side fix — deploys without a rebuild;
push auto-enables once Firebase (client google-services.json + server FCM) is set up.
iOS/APNs unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-15 21:55:09 +05:30
SravanandClaude Opus 4.8 b03a32b10d Android CI: use JDK 21 (Capacitor 8 Android requires it)
@capacitor/android is ^8; Capacitor 8's Android toolchain (AGP 8.7 / Gradle 8.11)
requires JDK 21. The workflow used java: 17, which fails the Gradle build before an
APK is produced. Bump to java: 21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 16:36:47 +05:30
SravanandClaude Opus 4.8 e39828ed56 Android CI: fail the build on a Gradle error + surface the real cause
The build step ended with `find … *.apk`, which exits 0 even when Gradle failed
and no APK exists — so Codemagic marked a failed Gradle run as a green build with
no artifact. Now: capture gradle output, use PIPESTATUS to detect failure, print
the "What went wrong" / FAILURE block, exit non-zero, and assert an APK exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 16:35:39 +05:30
SravanandClaude Opus 4.8 35ce641046 Android CI: run on mac_mini_m2 (linux_x2 not on the billing plan)
The android-apk workflow requested linux_x2, which Codemagic rejected with
"selected instance type is not available with the current billing plan". Switch
to mac_mini_m2 — the same instance the iOS workflow already uses on this account;
Codemagic's macOS images include the Android SDK + JDK. Also make the SDK path
(ANDROID_SDK_ROOT -> ANDROID_HOME fallback) and the google-services base64 decode
(GNU --decode / BSD -D) portable to the macOS image.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 16:08:52 +05:30
SravanandClaude Opus 4.8 1efb2e4314 Fix: message text not selectable on desktop
#msgs .bubble had a blanket user-select:none (added to suppress iOS's long-press
magnifier), which also blocked mouse text-selection on desktop. Scope the
none to touch devices only (@media (hover:none)); keep it in multi-select mode.
Desktop can now highlight/copy message text. Verified: desktop=auto,
touch=none, sel-mode=none.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 19:23:21 +05:30
SravanandClaude Opus 4.8 677d418ed0 Fix: dragging multiple files into a chat only sent the first
The conversation drop handler read dataTransfer.files[0] (a single file), so a
multi-file drag-and-drop uploaded just one. Iterate every dropped file, matching
the file-picker path. Verified in-browser: dropping 3 files queues + sends all 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 18:22:57 +05:30
SravanandClaude Opus 4.8 07c728bf6a Android CI build + delete-chat (self-only) + SFU copy refresh
- codemagic.yaml: add an Android workflow that builds an installable debug
  APK (Linux instance, no signing needed to test) — the Android equivalent of
  the iOS TestFlight loop. mobile/scripts/android-patch.js injects the
  camera/mic/notification permissions into the CI-generated manifest (android/
  is gitignored, same as ios/).
- Delete chat (self-only): new POST /api/messages/clear bulk-hides a whole DM
  (or group) for the requester via the existing per-user message_hidden
  mechanism — the other party keeps their copy entirely. "Delete chat" button
  added to the DM contact-info panel; chat-cleared synced to my other devices.
  Verified end-to-end on Postgres (A clears -> empty for A, B untouched).
- Meetings copy: SFU is live, so drop the "coming soon" / "small group (mesh)"
  wording on the welcome card and the Meetings header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 17:12:42 +05:30
SravanandClaude Opus 4.8 948eae249f App Store: add 13-inch iPad screenshots (universal build requires them)
The Capacitor build is universal (iPhone + iPad), so App Store Connect requires
iPad screenshots. Added three 2064x2752 (13" iPad) shots rendered on the app's
two-pane iPad layout (chat sidebar + open conversation, meetings, schedule),
anonymized. No rebuild needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 16:13:55 +05:30
SravanandClaude Opus 4.8 88a6b6a21e App Store: add screen-sharing screenshot (6 total)
06-screenshare.png — a meeting with a shared screen (a slide + chart on the
stage, "Sharing" badge, participant tiles, controls), rendered on the real call
UI at 1320x2868 and anonymized. Updated the submission pack's screenshot order to
include it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 14:51:15 +05:30
SravanandClaude Opus 4.8 620039a2ff iOS remote support: Share Screen -> Connect Screen over LiveKit (core)
Rebuild the iOS remote-support screen share the RIGHT way: keep the exact
Share/Connect UX + session (consent + symmetric session-ended), swap only the
media to LiveKit since WKWebView can't getDisplayMedia. (Replaces the earlier
"route to a meeting" detour, which was reverted.)

Flow: customer taps Share Screen -> gets a 6-digit code (unchanged UI) -> helper
enters it in Connect Screen -> on the customer's "Allow", the app publishes the
screen natively (ReplayKit -> LiveKit) into a per-session room and tells the
agent it's a LiveKit session -> connect.html joins that room and shows the screen
in its existing viewer (recording/controls intact). Chat runs over the session
socket (no P2P data channel in this mode). Either side ending fires the existing
session-ended -> both tear down (symmetric disconnect).

- signaling.js: relay 'rs-livekit' + 'rs-chat' between the two ends.
- home.html: parent bridge so the /share iframe can drive startMeetingScreenShare/
  stop on the native plugin (+ a capability handshake).
- share.html (iOS): publish via native LiveKit instead of getDisplayMedia; chat
  over WS; hide mic (voice = next iteration) + remote-control (impossible on iOS).
- connect.html: LiveKit viewer for iOS-shared sessions, reusing the P2P viewer.

Web-only, no new build (reuses the shipped startMeetingScreenShare). Desktop
Share/Connect P2P unchanged. Two-way voice is the planned follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 13:16:06 +05:30
Sravan 224a2800c5 Revert "iOS "Share Screen": route to a screen-share meeting (remote-support on iPhone)"
This reverts commit eb685b42ca.
2026-08-22 12:21:36 +05:30
Sravan ba51a3c5d7 Revert "iOS Share Screen: show the code and wait; start sharing when the helper joins"
This reverts commit 1a089c0349.
2026-08-22 12:21:36 +05:30
SravanandClaude Opus 4.8 1a089c0349 iOS Share Screen: show the code and wait; start sharing when the helper joins
Fix the confusing flow: tapping Share Screen dumped the user straight into a
meeting and auto-shared, with the join code hard to find. Now it shows a clear
full-screen "Share your screen" step with the big 6-digit code + Copy + a
"Waiting for them to join…" spinner (and Cancel). The native screen broadcast
starts only when the helper actually joins with that code (meeting-peer-joined) —
matching the remote-support mental model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 12:00:14 +05:30
SravanandClaude Opus 4.8 eb685b42ca iOS "Share Screen": route to a screen-share meeting (remote-support on iPhone)
The remote-support Share/Connect tabs use P2P WebRTC (screen + voice + chat) that
WKWebView can't capture, so "Share Screen" never worked on iPhone. Rather than
rebuild the whole P2P session on LiveKit, route iOS "Share Screen" to a
screen-share MEETING (chosen with the user): tapping Share Screen on iOS starts
an instant meeting, auto-starts the native ReplayKit screen share (the path just
verified on device), and toasts the join code. A helper joins by code (Meeting)
to watch — and gets voice + chat for free. Viewing already works in the webview,
so only the SHARE side is rerouted; desktop keeps the full P2P remote-support
flow, and iOS Safari (no app) is unchanged.

Web-only, no new build (reuses the shipped startMeetingScreenShare native method).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 11:36:48 +05:30
SravanandClaude Opus 4.8 467a8b9c6e iOS meeting screen share: publish ReplayKit broadcast into the SFU room
Gap: on iOS, screen share only worked in native CallKit calls. In a scheduled /
code-joined SFU meeting the webview called setScreenShareEnabled() ->
getDisplayMedia(), which WKWebView does not implement, so it failed silently.
Production meetings are real LiveKit rooms, so we can publish natively instead.

Fix (native ReplayKit -> same LiveKit room as a dedicated screen participant):
- server: /api/meetings/token accepts screen:true -> mints a distinct
  <peerId>-screen identity (LiveKit allows one connection per identity), so the
  native publisher doesn't collide with the webview's own connection.
- native plugin: startMeetingScreenShare/stopMeetingScreenShare + connectScreenRoom
  (a screen-only LiveKit connection: mic off, no camera, no callConnected) that
  reuses the existing broadcast-extension publishing path.
- webview: toggleScreen routes to the native method on iOS SFU meetings; the
  screenShareState listener now drives the SFU case too (and tears the screen
  connection down on the system "Stop Broadcast"); sfuAttach/sfuDetach map the
  '<peerId>-screen' participant's screen track onto the sharer's tile and suppress
  the phantom person tile + the sharer's own self-view.

Web/server deploy now; the native method needs the next Codemagic build to test
on device. Verified: db-smoke 22/22; Swift braces balanced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 01:01:42 +05:30
SravanandClaude Opus 4.8 14eb99edaf App Store: add group-chat + schedule-meeting screenshots (5 total)
Two more anonymized 1320x2868 shots of features that work on iOS: a group
conversation (sender chips, read receipts) and the Schedule-a-call flow
(participant names anonymized). Deliberately did NOT screenshot Share/Connect:
webview screen capture (getDisplayMedia) is unavailable in WKWebView, so the
remote-support screen-share tab doesn't function on iOS — advertising it would
be inaccurate metadata. (Screen share works in native calls via ReplayKit and
fully on desktop.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 00:37:03 +05:30
SravanandClaude Opus 4.8 2948e0cfcc App Store assets: 6.9" screenshots + wired privacy/support URLs into submission pack
- mobile/appstore-screenshots/: 3 anonymized 1320x2868 screenshots generated from
  the live app (Chats, a Conversation, Meetings). Real names/faces replaced with
  demo identities, notification banner dismissed, internal roadmap copy removed.
- APPSTORE_SUBMISSION.md: pre-submission checklist now all green; Privacy Policy
  and Support URLs filled with the live /privacy and /support pages; demo login
  and export-compliance noted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 12:46:12 +05:30
SravanandClaude Opus 4.8 a9d95ed8c6 Add public /privacy and /support pages (App Store required URLs)
App Store submission needs a public Privacy Policy URL and Support URL, reachable
without login. Added branded, self-contained pages served at /privacy and
/support (static.js path mappings, like /home). The privacy policy accurately
describes Biz Connect: BizGaze-account login, messages/media stored on our
servers, call media via LiveKit (only recorded on explicit user action),
on-device transcription (audio never leaves the device), push via APNs/FCM, no
sale of data / no ads, retention, security, and user/GDPR rights. Contact:
support@bizgaze.com (confirm/replace if different).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 12:35:06 +05:30
SravanandClaude Opus 4.8 18edc5dbfb Moderation UX: Block on DM contact info + visible report-resolved notice
Tester feedback:
1. "Block contact not on the contact info." The earlier Block button was only on
   the group-sender mini-profile, which a 1:1 chat never opens. A DM's info panel
   is openSharedItems('dm',…) (tap the conversation header). Added a full-width
   "Block contact / Unblock contact" button there.
2. "Reporter gets no notification on resolution." Delivery was actually working
   (verified: the reporter receives the 'report-resolved' event over the chat WS),
   but the client only added a silent bell entry — easy to miss — and a self-
   resolve (same admin reported + resolved) is intentionally skipped. Made it a
   visible toast + ping for an online reporter (report-new likewise toasts admins).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 22:39:13 +05:30
SravanandClaude Opus 4.8 928119725e Moderation follow-ups: block on contact profile, z-index, account suspend, reporter notify
Addresses tester feedback on the Report/Block feature:

1. Block/Unblock is now on the CONTACT's profile card (openMiniProfile), not only
   in a message's ⋮/long-press menu — you can block someone straight from their
   info popup.
2. Admin Delete/confirm popups were appearing BEHIND the Reports window. The
   moderation modals used z-index 100000 (above bzConfirm's .modal-ov at 9800);
   lowered them to 9750 so the confirm dialog sits on top.
3. Clarified admin action. "Block" is a PERSONAL mute (per guideline 1.2) and does
   not touch login. The report view now offers a real account action instead:
   "Suspend account" (deactivate -> signed out + cannot log in) with a confirm,
   toggling to "Reactivate account" — both reversible in-place, driven by a new
   reportedActive flag on /api/reports. (Uses the existing /api/users/manage
   deactivate/activate.)
4. Resolving a report now notifies the reporter (live 'report-resolved' event +
   background push), and admins get a 'report-new' activity entry.

repos: reports.byId. Verified: moderation suite 18/18 (adds reportedActive +
suspend->login-blocked->reactivate->login-restored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 11:50:55 +05:30