Commit Graph

48 Commits

Author SHA1 Message Date
Sravan 2460c0f9eb wip(db): async call-site conversion in progress (Phase 3) — DO NOT MERGE yet
On the db-migration branch only; master stays clean + deployable. Foundation
(adapter, pg schema, smoke harness) is already on master and safe.

Done:
- repos.js fully async (Phase 2, validated: node --check clean, no missed transforms).
- session.js currentUser/apiKeyFromReq async.
- Mechanical `await` prefix applied across routes/static/calls/signaling/reminders/
  webhooks/push.

Remaining (does NOT compile yet — deterministic to finish):
1. Async cascade: helper fns that now contain `await` must be marked async and their
   callers awaited. node --check points to each (namesFor, authAttachmentRaw/
   authAttachment in static, the WS handlers in calls/signaling, reminders/webhooks
   loops).
2. DTO builders are the real work: namesFor, avatarsFor, buildPollDTO, buildMsgDTO,
   recDTO all became async — every `.map(x => buildMsgDTO(...))` etc. must become
   `await Promise.all(arr.map(async x => ...))`.
3. Chained calls `R.x.y(...).map/.length/.includes` → `(await R.x.y(...)).method`.
4. Then: node --check all green → node test/db-smoke.js green → e2e → merge to master.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:26:10 +05:30
Sravan d664dde798 feat(share): send from inside the share sheet — the Teams-style in-extension picker
Replaces the wrong approach (stage + try to bounce into the app, which iOS blocks)
with the one Teams/WhatsApp actually use: the picker and the send happen INSIDE the
share extension, so there's no app-open at all. Tap Share → Biz Connect → pick a
chat → it uploads and sends, right there in the sheet.

How the extension can send without the app: it's a separate process that can't see
the web app's HttpOnly cookie, so:
- server: GET /api/share/token mints a bearer token for the logged-in user.
- web: on every launch the app fetches that token and hands it to the extension via
  the App Group (ShareInbox.setAuth writes token+base to the shared UserDefaults).
- extension: reads the token and calls the SAME API the native client uses —
  GET /api/messages/conversations to list chats, POST /api/messages/upload for each
  file, POST /api/messages to send. Native UITableView picker with search.

Robustness: it still stages the files + writes a manifest first, so if there's no
token yet (user never signed in) or the send fails, the file isn't lost — the app
collects it on next open, exactly as before. On success the manifest is cleared so
the app doesn't re-offer it.

Server + web are live now; the token endpoint is harmless until a build ships the
extension. NEEDS A NEW iOS BUILD for the picker itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 16:33:23 +05:30
Sravan 8a5409987c perf(video): stream a capped, faststart rendition instead of the raw upload
THE ANSWER to "why does an already-downloaded video still buffer?" — it was never
the download, and it was not the server. Probing the real uploads on the box:

  d0e49e58…  1920x1080  19.4 Mbps   75 MB / 31 s
  ad929d0b…  1920x1080  19.0 Mbps   27 MB / 11 s
  9f4e0865…   720x1584   3.6 Mbps   14 MB / 31 s

To play a 19 Mbps file the client has to SUSTAIN a 19 Mbps download for the whole
clip. No mobile link does, so the <video> buffer drains every few seconds: buffers,
plays, buffers, plays. Server-side disk read was instant and load was 1.7 on 20
cores throughout — the bottleneck is the media itself, not the delivery path.

Second, independent defect: phone MP4s store `moov` AFTER `mdat` (verified on two
uploads), so the player must fetch the file's tail before it can start at all.

Fix — keep the original bytes untouched (that is what the download button serves,
full quality) and build <id>.web.mp4 beside it: longest side capped at 1280,
~2.5 Mbps ceiling, +faststart. Measured on the 19 Mbps file:

  27.3 MB @ 19.0 Mbps  ->  2.55 MB @ 1.78 Mbps   (10.7x less bandwidth)
  transcode took 2.4 s for an 11.5 s clip

- server/media.js (new): probe, decide, 2-at-a-time background queue. Already
  light + correctly sized + faststart => no rendition at all. Light but wrong atom
  order => remux -c copy (seconds, no re-encode). Otherwise re-encode. A rendition
  that lands bigger than the original is discarded. MP4 box-walker for the
  faststart test is unit-checked against known fast/slow files, both directions.
- /stream/<id> serves the rendition, falling back to the original while it is still
  transcoding, so a video is never unplayable. /files/<id> is unchanged and still
  serves the pristine original for download.
- Renditions are queued at upload, and backfilled 15 s after boot for the videos
  that predate this. Range serving is now one shared helper for both routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:49:04 +05:30
Sravan 85a1b9d2ca fix(mobile): pull-refresh loader below the Dynamic Island; in-chat video player
Bug 1: .ptr-ind used top:8px so the pull-to-refresh spinner sat under the notch/Dynamic
Island. Now top:calc(var(--sat)+8px) clears the safe-area inset.

Bug 2: video attachments rendered as a plain download link that re-downloaded on every
tap. Server now sends isVideo/isAudio on message attachments; videos render as an in-chat
player — masked poster with a DOWNLOAD button that loads the file ONCE (preload=none ->
load on tap), then becomes a PLAY button; playing hands off to native inline controls, so
no repeat downloads. build batch155.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:48:09 +05:30
Sravan 200624bd24 chore: remove all temporary debug instrumentation
Investigations are closed, so strip the probes: bzDbg + __perfProbe definitions, the
older/render/slide/pwaFocus call sites, and the server-side /api/dbg (MDBG) sink. Kept
the functional code around each probe (renderThread's innerHTML build, the older-page
re-anchor, the slide fade). No behavior change. build batch153.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:46:54 +05:30
Sravan 5960efb612 uploads: support up to 1 GB attachments, streamed to disk (was 25 MB, buffered in memory) (batch145)
- Server streams the upload body straight to /data/uploads (a .part temp file, atomic rename on
  success), backpressure-aware, so a 1 GB file never buffers in RAM. MAX_UPLOAD_MB env (default
  1024 = 1 GB) controls the cap; error message reflects it.
- Client size guard raised 25 MB -> 1 GB.
- docker-compose documents MAX_UPLOAD_MB and the required Nginx Proxy Manager client_max_body_size.
NOTE: the actual bottleneck for the user's 9.7 MB reject is almost certainly NPM's client_max_body_size
(nginx default 1 MB) — that must be raised in the NPM admin; the app change alone can't lift it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:06:19 +05:30
Sravan d02d8976d3 diag(mobile): instrument slide frame-timing + render cost + load-older scroll to find the real jank
Re-add /api/dbg + a frame-gap/long-task probe over the chat-open slide, the innerHTML render time, and
before/after scroll for load-older — to measure on-device where the jank actually is (main-thread block
vs compositor) instead of guessing. Temporary; removed once fixed.
Build marker -> 2026-07-19-batch122.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:00:28 +05:30
Sravan 529bc26cb5 perf(mobile): compositor 'render-then-arm' chat-open slide + robust load-older anchor; remove diagnostics
Slide (workflow-verified): the jump was the main thread building the message DOM + scroll writes DURING
the 300ms animation. Now: render off-screen first, then arm a transform-only slide on a promoted GPU
layer (translate3d/contain:layout paint) with all innerHTML/scrollTop work deferred to transitionend
(afterOpenSlide queue). armOpenSlide runs after the sync render; appendBubble defers live writes mid-slide.
Load-older jump: anchor on the specific oldest-loaded message element (not scrollHeight math, which broke
when prepended images loaded and shifted content) and re-anchor as those images load.
Removed all /api/dbg diagnostics (client reporter + server route + spike/diag probes).
Build marker -> 2026-07-18-batch121.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:49:38 +05:30
Sravan f3c3e08be4 perf(mobile): paginate threads to 40 (was 500) — kills the heavy-DOM jank at the source
User's insight: the jank/jumps come from rendering ALL ~500 messages on open. Load only the latest 40
(server + client PAGE), and the existing loadOlder() pages in older history on scroll-up with a scroll
anchor (no jump). Whole-thread SEARCH is a separate endpoint, unaffected. Also #1: keyboard-show only
pins to newest when already near the bottom, so replying to an OLD message no longer yanks to latest.
Build marker -> 2026-07-18-batch120.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 23:31:05 +05:30
Sravan f9d6472f77 chore(mobile): add /api/dbg diagnostics sink + client reporter (build, overflow, viewport, scroll)
Temporary: the app POSTs its real runtime state to the server log so device-only bugs (budge source,
keyboard viewport numbers, chat-open scroll state) can be diagnosed from docker logs instead of
screenshots. Removed once mobile issues settle.
Build marker -> 2026-07-18-batch108.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:18:58 +05:30
Sravan 02075fbd47 feat(chat): GIF picker via server-proxied GIPHY (#5) (batch87)
- 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>
2026-07-15 15:30:44 +05:30
Sravan c3dc47a94c fix(meetings): Past shows only scheduled + instant meetings; ad-hoc calls hidden w/o recording (batch86)
Revised rule per user: a call is not a meeting. Past meetings now = real SCHEDULED meetings +
INSTANT meetings (Start-a-meeting, logged in call_history as adhoc). Ad-hoc CALLS — 1:1 direct,
group calls, and a 1:1 that a 3rd person joined — are shown ONLY if they produced a
recording/transcript. Dropped the earlier ">2 participants shows it" exception.
Also stop rendering a bare "Host: —" on logged call cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:58:48 +05:30
Sravan f708cc2f12 fix(meetings): hide empty 1:1 direct calls; date from/to validation + field alignment (batch85)
#3 The 136 "Direct Call" cards: calls.js writes a scheduled_meetings row for EVERY call
   ("Direct Call" / "Group call"), which is separate from the call_history table. Those rows
   came through the scheduled-meetings path and my earlier filter never touched them. Now an
   auto call-history row is shown in Past ONLY if it produced a recording/transcript OR the
   call ever held >2 people (peak from the call log); plain 1:1s with neither are dropped.
   Removed the now-redundant callRows synthesis. Recordings stay attached (rows WITH a
   recording are always kept).
#4 Date range: from ≤ to enforced by disabling out-of-range days in each picker (can't pick a
   from after to, or a to before from). Filter controls share one height/baseline so the
   calendar icon, preset dropdown and date fields align cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:19:12 +05:30
Sravan fad8a52da4 fix: last-seen backfill, report-style date filter, hover overflow (batch84)
#1 Last seen still read "Offline": last_seen is a NEW column, so every existing user was NULL
   until they happened to reconnect. Backfill it from the newest message each user sent (best
   evidence we already have), and also stamp it on message send — not just on connect/disconnect
   — so it stays fresh if a socket never closes cleanly.
#3 Past-meetings filter is a report filter now: presets (Today, Yesterday, Last 7/30 days, This
   month, Year to date, All time) plus a Custom range that uses the SAME branded calendar popup
   as the scheduler. The raw <input type=date> looked foreign and behaved differently per browser.
#4 Hover action row overflowed off-panel on SHORT received messages: it's wider than the bubble
   and was anchored to the bubble's right edge, so it ran off the left. Received bubbles now
   anchor it from the left (growing into the empty space); own messages keep the right anchor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 01:02:59 +05:30
Sravan 6fa261b68f feat(meetings): call log, past pagination + date filter; last-seen exact time (batch83)
#7 The past list was hard-capped at 12 (a .slice(0,12) in the client) and there was NO record
   of how many people were ever in a finished call — so the "only calls with >2 people" rule
   was impossible to apply. Added a call_history table: signaling tracks the HIGH-WATER
   participant count per room and logs the call when the room tears down (scheduled meetings
   are skipped — they already have their own row).
   Past meetings now follow the rules asked for:
     • a plain 1:1 direct call is NOT listed — unless it produced a recording/transcript
       (those already surface as recording entries);
     • a call that ever held MORE than 2 people IS listed (e.g. a 1:1 a third person joined),
       showing its participant count and duration;
     • entries are visible only to people who were actually in the call (or the group).
   Server-side pagination (10/page) + a from/to date filter; nothing is double-listed.

#2 Last seen now shows the exact time/date, WhatsApp-style — "last seen today at 1:36 PM",
   "last seen yesterday at 10:15 AM", "last seen 14/07/2026 at 9:00 AM" — instead of "10
   minutes ago".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 00:42:53 +05:30
Sravan 860a7bd6cf feat(chat): emoji speed, action rework, copy, phone links, image zoom, upload %, profile card, last seen (batch82)
#6 Emoji were slow because Twemoji swapped EVERY emoji for an <img> fetched individually
   from a CDN — opening the picker fired hundreds of image requests. Now uses the OS's own
   colour emoji font: instant, zero network. twemojify() kept as a no-op.
#4 Message hover row reworked: three one-tap reactions (Like/Laugh/Surprised) + the emoji
   picker + (own) Edit. Reply / Forward / Copy / Delete moved behind a ⋮ menu. Added Copy.
   The row now sits FULLY above the bubble (was top:-14px, overlapping the first text line).
#9 Phone numbers linkify to tel: — mobile gets the OS "call this number?" prompt; desktop has
   no dialer so it offers to copy. Regex kept conservative (10–15 digits, needs +/grouping) so
   it won't grab amounts, dates or 6-digit meeting codes.
#3 Image preview zooms: wheel + pinch + double-click + ± buttons, drag to pan, keys (+/-/0),
   cursor-anchored. Arrows hide while zoomed so panning isn't hijacked.
#8 Upload progress: fetch() can't report upload progress at all, so a large file just said
   "uploading…". Switched to XHR (upload.onprogress) → real bar + %, and cancel aborts in flight.
#1 Clicking a sender in a group opens a mini profile card (photo, presence, last seen) with a
   Message button that opens the 1:1 (and a view-photo button).
#2 Last seen: new users.last_seen column, stamped on connect and when the last socket drops;
   carried on the presence broadcast, so an offline contact reads "Last seen 10 minutes ago"
   instead of a bare "Offline".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 23:46:52 +05:30
Sravan c1f01e796a fix: DP missing in 1:1 for un-messaged contacts; make web updates fully silent (batch80)
DP bug — real cause found (client-side, not the DB):
loadSidebar builds a DM row for every contact you haven't messaged yet, but it copied only
{name, online} from the contact and DROPPED `avatar` (and status/email). So an un-messaged
contact always rendered initials in the 1:1, while the SAME person showed their photo in a
group (which reads /api/groups/members). Carry the whole contact through.
Kept a server-side safety net: avatarsFor() now indexes known photos under person-id, email
AND name, so a duplicate row missing a photo can match on any of them (the previous single
composite key missed twins with different emails).

Web updates are now completely silent: no banner, no toast. A web build is an implementation
detail — surfacing it makes users reason about "web build vs app version", which is exactly
the confusion to avoid. New code simply applies itself as soon as it's safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 16:06:28 +05:30
Sravan 2aeeb0a096 fix: auto-apply new web builds; DP missing in DM but shown in group (batch79)
Auto-update (no manual step, no version confusion):
- New code now applies ITSELF. The client polls /api/build and, as soon as it's SAFE,
  silently hard-reloads onto the new build. Safe = not in a call, no live screen session,
  no dialog open, nothing half-typed; if the user is busy we wait and apply the moment
  they're free. A brief "Updated to the latest version" toast confirms it.
- Removed the "web build" row from Settings: users must never have to reason about an app
  version vs a web build. The only version surfaced is the desktop app's (auto-updater).

DP bug: a contact showed their photo in a GROUP but fell back to initials in the 1:1.
Two causes, both handled:
- Duplicate rows for one person (signed in by email once and by mobile another time before
  the bizgaze_user_id merge landed) — only one row carries the DP, and the group happened to
  reference the row WITH the photo. avatarsFor() now keys rows by stable person identity
  (bizgaze person id → email → name) so a photo-less row borrows its twin's photo. Applied to
  contacts, conversations, group members and group info.
- A DM whose counterparty was merged away is now keyed by the SURVIVING account, so the row
  carries that account's name/photo/presence (and split threads collapse into one).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:34:35 +05:30
Sravan a0017f2036 feat: detect new web builds + hard-refresh escape hatch (0.1.17/batch78)
Root cause of "the fix works on mobile but not on desktop/web": nothing was wrong with
the code — the desktop app now CLOSES TO TRAY, so it can run for weeks on the page it
loaded on day one and never re-fetch after a deploy. There was also no way to force it
off a stale page (no menu bar → no reload accelerator).

- Server: GET /api/build returns home.html's __BUILD marker.
- Client: polls it (boot, on focus/visibility, every 10 min); when the server's build
  differs from the running one, shows a branded "A new version is available — Refresh"
  banner. Settings gains an always-available "Refresh app" with the current build shown.
- hardReloadApp(): in the browser it unregisters service workers + clears CacheStorage
  then reloads cache-busted; in the shell it calls the native hard reload.
- Desktop: hard-reload IPC (clears the session HTTP cache + reloadIgnoringCache), wired to
  Ctrl+R (reload), Ctrl+Shift+R / F5 (hard reload), and a "Refresh app (get latest)" tray
  item. Previously there was literally no way to clear the cache from the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:38:55 +05:30
Sravan d0f9355553 feat: close-to-tray, meeting lobby, speaker select, link expiry, mobile RC touch (0.1.14/batch72)
General #1/#2 (closed-app notifications): the desktop app now CLOSES TO TRAY instead
of quitting, keeping its chat WebSocket alive so calls/messages still notify. Tray
icon + menu (Open / Quit), single-instance lock, first-close hint.

Guest #4 (lobby/admit): meetings can require the host to admit guests joining by link.
Setting on the schedule form ("Guests must be admitted by the host", default on) +
ad-hoc default. Guests wait on a "waiting to be let in" screen; the host gets an
Admit/Deny prompt; auto-cleanup on leave. Logged-in members always join directly.

Guest #5 (speaker): headphones/speaker output picker in the meeting (setSinkId),
remembered and applied to every tile.

Guest #3 (link expiry): guest link/token dies ~2h after a scheduled meeting's end
(HTTP 410) with a clear message; live-room links expire when the room empties.

RC #4 (mobile): touch→mouse mapping so a phone/tablet viewer can control (tap=click,
drag=move). Uses the same letterbox-correct coordinate mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:44:58 +05:30
Sravan bd488b3286 fix(rc+guest): coordinate/keyboard/UI for remote control; guest media + panel fixes (batch71)
Remote control (connect.html viewer):
- Coordinate offset fixed: map clicks to the actual video CONTENT rect (object-fit
  letterbox-aware), not the element rect — cursor now lands where you click.
- Keyboard: capture at document level while a session is live (the <video> lost
  focus on bar clicks → keys did nothing). Skips the chat input.
- Control bar moved to bottom-right with tiny modern icons; video fills the viewport.
- Smoother cursor (mousemove ~60/s).

Guest meetings:
- Guest mic inaudible + guest invisible in the participant list + outsider screen
  share not showing (#1/#8/#11): root cause was the SFU media→tile map keyed on
  identity==uid, but guests had a random LiveKit identity and a null signaling uid.
  Guests now carry ONE stable id across signaling (meeting-join guestId) and the
  LiveKit token identity, so their media attaches and they appear to everyone.
- Guests can't add participants (#9) and don't see the transcript button (#12).
- Search box in the schedule participant list (#10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 14:28:37 +05:30
Sravan 7bc40d8397 feat(meetings): email invites + add external participants by email + share link (batch69)
#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>
2026-07-10 16:02:45 +05:30
Sravan 466c0d5d70 feat(meetings): guest join by link — external people can join without a Connect account (batch68)
- Server: POST /api/meetings/guest-token issues an unauthenticated LiveKit token
  for a room that is currently live or a valid scheduled meeting (throwaway guest
  identity). Mesh signaling already tolerates anonymous peers.
- Client: opening /home?meet=CODE while signed out runs a lightweight GUEST mode —
  name prompt, then straight into the meeting with the full meeting client and the
  chat/sidebar chrome hidden. Signed-in users who open the link auto-join.
- sfuConnect uses the guest token for guests. Guests get a friendly "left the
  meeting / rejoin" screen (no chat to fall back to).
- Meeting "Add people" panel gains a "Copy invite link" (copyMeetingLink) that
  copies the guest link — foundation for the emailed invites in #4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:44:28 +05:30
Sravan 49718e5d37 feat: hyperlinks, group seen-by for all, profile-pic preview, in-call invite fix + desktop update progress (0.1.6)
- #1: URLs in chat are now clickable links (open externally on desktop).
- #2: group 'Seen by X, Y +N more' now shows under the LATEST message to EVERYONE
  (server computes reads for all messages; click shows the names).
- #5: tapping the conversation-header avatar previews the DP/group photo full-size.
- #4: in-call invite lists only people you've messaged, excludes those already in the
  call/invited, and the Invite button is sticky.
- #3 (0.1.6 shell): auto-updater emits checking/available/downloading/ready/error to
  the web UI, which shows a progress banner (+ Restart button) so updates aren't silent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:58:09 +05:30
Sravan f52d54a093 fix(chat): forward polish + scrollbar + image preview + media tabs
- #4: forward modal buttons now branded (bzc-* un-scoped from .bz-confirm); target
  rows get real checkboxes + border highlight; selected MESSAGES highlight in green
  with a filled tick (distinct from blue bubbles), checkbox on every message.
- #5: forwarded messages show an italic 'Forwarded from <origin>' label; new fwd_from
  column preserves the true origin across re-forwards.
- #6: image-preview arrows are smaller (32px) chevron icons, clear of the image
  (image max 82vw); icons.js?v=5.
- #10: image preview now opens ABOVE the media modal (z 9900) and Esc/close returns
  to the media view instead of closing everything.
- #11: Media/Links/Docs active tab underline is brand-blue, not green.
- #2: chat-list + conversation scrollbars 6px, stepper arrows hidden; pagination adds
  a cooldown + overflow-anchor:none so scrolling up no longer sticks/jumps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:11:02 +05:30
Sravan 346361da8a feat(chat): dismiss a chat's notifications on other devices once seen (#13)
When you open/read a conversation, the server now also pushes a notif-clear to your
OWN other sockets. Each device tags notifications by conversation (kind:id), so on
notif-clear it closes the matching page Notification + any Service-Worker (Web Push)
notifications, and drops matching activity-center entries so the bell badge stays in
sync. (Desktop Electron native toasts are transient/auto-expire; the web+PWA surface
is covered.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:20:18 +05:30
Sravan a9b3533f7a feat(chat): forward messages with multi-select (#1)
- Message action pill gains a Forward button; tapping it enters selection mode
  (tap bubbles to multi-select, footer bar shows count + Forward/Cancel, Esc exits).
- Forward picker lists EXISTING conversations only (DMs + groups from the sidebar),
  searchable, multi-target. POST /api/messages/forward copies body+attachment into
  each target (authorized as participant/member), live-pushed like a normal send.
- /files auth now accepts ANY message carrying an attachment (allByAttachment), so
  forwarded images stay viewable for the new recipients.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:09:02 +05:30
Sravan 3a976d58ab feat(chat): reply-jump (#8), image prev/next nav (#3), multi-file send (#5)
- #8: reply quotes are now clickable — jump to the original message (paging older
  history in if needed), then flash it. reply DTO carries the target's timestamp.
- #3: image lightbox now has ← / → arrows + keyboard nav to flip through all images
  in the conversation.
- #5: the composer queues MULTIPLE files (file input is multiple; paste still works);
  each is shown as a removable chip and sent as its own message (first carries the
  typed text as caption), in order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 16:02:15 +05:30
Sravan eb79823fd2 feat(chat): older-history pagination + full-thread server-side search
Older messages (beyond the newest 500) now load as you scroll to the top — the
thread endpoint takes a ?before=<created_at> cursor, the client prepends the older
page and preserves scroll position (renderThread keepScroll). _hasMoreOlder stops
paging when a short page returns.

Search now covers the ENTIRE thread, not just the loaded window: new
/api/messages/search (DM + group, LIKE with escaped wildcards) returns all matching
message ids; the client debounces the query, and jumping to a hit older than the
loaded window pages history back (ensureLoadedBack) until the match is in view, then
highlights + flashes it. Cap raised to 500.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:21:14 +05:30
Sravan 12be747609 fix(chat): resolve merged-away user ids so DMs to merged contacts don't vanish
Root cause of 'messages to some contacts disappear (gone after reopen)': the #2
account merge deletes the merged-away user row. Any lingering reference to that old
id — a cached contact, an in-flight DM — then saved against a dead recipient / 404'd
on thread fetch, so messages silently vanished.

- New user_aliases table records old_id -> survivor on every merge (mergeInto).
- users.resolve(id) follows the redirect.
- DM send (recipient), thread fetch (with), and read now resolve() the peer id, so a
  stale id transparently routes to the surviving account.

Fixes future merges fully. Contacts merged BEFORE this (no alias recorded) may need a
one-off data check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:23:49 +05:30
Sravan f1dbcd0f86 feat(meetings): LiveKit SFU — phase 1 (server plumbing, config-gated)
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>
2026-07-06 13:11:37 +05:30
Sravan 30e354d58f feat(chat): per-message group read receipts (seen-by + read-by-all ticks)
Thread: every group message I send now carries a receipt tick — sent (1 grey)
-> seen-by-some (2 grey) -> seen-by-all (2 yellow) — computed from the per-message
seenBy the server already returns and kept live by onGroupRead. Tap the tick to see
exactly who ('Seen by X, Y') with an 'N of M' tooltip. Replaces the old last-message-
only 'Seen by' line with a universal, tappable per-message receipt.

Sidebar: the group row tick now reflects real read state (read/delivered/sent) via
memberReads vs member count, instead of a hardcoded 'sent' — and refreshes live when
the open group is read by all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:57:08 +05:30
Sravan a1887064eb feat(chat/notif): single hover action-pill; sender DP in desktop+mobile notifications
- Message actions (reply/react/edit/delete) consolidated into ONE hover pill anchored to the
  bubble's top-right, overlapping it so on short messages the icons no longer float off into
  empty space and vanish before you can click.
- Desktop toast + mobile(FCM)/web-background push now show the sender's real DP:
  * renderer passes the DP URL through; desktop shell downloads it for the toast icon
    (canvas-drawing an external DP tainted it → initials). Desktop bumped to 0.1.4.
  * DM push payload carries icon=sender avatar; sw.js already uses it (web background),
    sendFcm sets notification.image (Android).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:01:32 +05:30
Sravan 51e206279b feat(chat): edit message (#4)
Sender can edit their own text messages: a pencil action on the bubble loads the text into the
composer in an 'Editing' mode; saving updates the body, marks it 'edited', and pushes the change
live to the other side/tabs (chat-edited, mirroring delete). Adds messages.edited_at + editBody().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 17:45:20 +05:30
Sravan dc1915bb43 feat: live presence, delivery-tick sync, brand rollout, call fixes, desktop 0.1.3
Live presence (fixes stale in-call/status until refresh — impossible in apps):
- server broadcasts a user's status over the chat socket on connect/disconnect, call
  join/leave, and status change (chat.js broadcastPresence; signaling + routes hooks).
- client onPresence() updates the contact dot + open-chat header live.

Chat delivery ticks (#6): chat-list row now mirrors the thread (delivered→double grey,
read→blue) via a new 'with' field on the delivered relay + onChatRead/onChatDelivered.

Call fixes: no bogus 'host handed over' when a 1:1 call ends (leaveMeeting forced);
branded call-connecting + chat-thread loaders; header subtitle tracks live call state.

Notifications: web notify + sw.js use sender/group DP + brand icon (not old wordmark);
desktop shell drops Web Push so only the single native toast fires (#5).

Brand: master icon/splash/loaders wired everywhere (PWA/favicon/apple-touch/.ico),
branded login (blue + gold CTA), branded toasts (BZToast) on all pages, Electron splash.

Desktop: dev auto-targets localhost (packaged→prod); version 0.1.3 with new multi-size
icon; dropped unused node-notifier; removed home-mockup.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:43:02 +05:30
Sravan 04db586dc3 feat(auth): merge mobile+email logins into one account via BizGaze person-id (#2)
Provisioning now keys the Biz Connect account on bz.bizgazeUserId (the same
value whether the person signs in with email or mobile) instead of the typed
identifier, so both logins resolve to a single contact. Legacy rows get the
person-id stamped on next login; an existing duplicate created under the same
identifier is folded in via a transactional users.mergeInto() that reassigns
all messages/memberships/reactions/votes/favorites/ownership to the survivor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:35:29 +05:30
Sravan 462a167438 feat(chat): self-chat 'You' — note-to-self (#4)
Pinned 'You' chat at the top of the list (always available); messaging yourself works
(to===me), with no self push/echo notification and a 'Message yourself' header. Self is
filtered from the normal contacts and shows no status dot. build batch24.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:05:32 +05:30
Sravan 15350c691b feat(chat): delivery ticks on the last message in the chat list (#3)
Conversations DTO now returns last_status (sent/delivered/read for DMs; sent for groups) for
my last message. The list row shows the matching tick (single/double, blue when read) and a
red 'Draft:' indicator when there's unsent text. Live 'sent' on send/receive; upgrades to
delivered/read on refresh. e2e 119.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:59:53 +05:30
Sravan 899770ed02 feat: Windows download link + self-served update feed + install tracking
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>
2026-07-01 21:28:53 +05:30
Sravan 4c75db2029 feat(push): native device-token registration + FCM/APNs senders
- /api/v1/devices (register) + /api/v1/devices/remove — auth-required, validates
  platform (ios|android), upserts by token; e2e covers register/validation/auth/remove.
- db device_tokens table + deviceTokens repo.
- push.js: FCM HTTP v1 (Android) and APNs token-based over HTTP/2 (iOS) folded into
  the single push.sendToUser path alongside Web Push; each transport independently
  config-gated and a silent no-op without creds. Dead tokens pruned on 404/410.
- docs: CLIENTS.md Phase B updated; DEPLOY.md env table adds FCM/APNs vars.

e2e 117/117.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:23:10 +05:30
Sravan 06f0b08a18 feat(chat): rich shared-media view, status selector, drag-drop upload + fixes
Chat / shared media:
- Media/Docs/Links: clean underline tabs (green active), audio & video now
  classified as Media and rendered as tiles (download + headphone/play +
  duration) instead of broken-image glyphs; image thumbnails -> lightbox
- Drag-and-drop a file/video/image onto a conversation to send it
- Fix: removed #chatPanel{position:relative} override that collapsed the
  conversation pane (messages spilled into a clipped right-edge strip)
- "Media, links & docs" row cleaned up (no folder/placeholder icon); media
  popup keeps the back arrow, drops the redundant close button

Presence / status:
- Single current-status row with an arrow that expands Available/Away/On leave
- On leave = circle with minus, In a call = solid red indicators
- Fix: selected-status tick now follows the chosen option

Icons: added headphones + play; bumped icons.js cache-bust to v4

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:01:15 +05:30
Sravan 1272b81cee feat(push): Web Push notifications for backgrounded/closed/mobile tabs
Page-level Notifications can't fire when a tab is frozen/closed (and never on
mobile), which is why recipients on another tab/app got nothing. Adds a
notification-only service worker (sw.js, no caching) + Web Push:

- push.js: optional web-push wrapper (no-op unless web-push installed AND
  VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY set -> app unaffected if unconfigured).
- push_subscriptions table + R.pushSubs repo (upsert by endpoint, prune dead).
- /api/push/vapid|subscribe|unsubscribe; DM + group message routes also send a
  Web Push to recipients.
- Client registers /sw.js, subscribes when permission granted; hidden-tab popups
  are left to push to avoid double-notifying (pushActive flag); SW suppresses the
  OS popup when a tab is visible. Removes the old code that unregistered SWs.

Requires (prod, once): npm install + VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_SUBJECT env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 21:58:49 +05:30
Sravan bda63b6f0a Merge origin/master (TURN/coturn + BizGaze-only login) into feature tree
Resolved conflicts in routes.js and share.html: kept the dev tree's superset
(ALLOW_LOCAL_LOGIN dev escape, avatar sync, richer login errors) which already
includes the incoming production BizGaze-only behavior; took the more descriptive
incoming comments. Restored 5 untracked modules (chat, calls, directory,
reminders, webhooks) that were missing from disk — required by routes/signaling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:27:59 +05:30
Sravan 27355cec76 BizGaze Connect: chat, meetings, recordings, mobile, directory + UI fixes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:15:29 +05:30
Sravan 54b74d5db1 feat(turn): self-hosted coturn support + time-limited creds + failure UX
- /api/ice: when TURN_SECRET is set, mint short-lived HMAC credentials
  (coturn use-auth-secret) so no permanent password is exposed and the relay
  can't be abused. Static TURN_USERNAME/CREDENTIAL still supported.
- share.html: connection watchdog + clear "couldn't connect on this network"
  message instead of a blank screen when no path can be established.
- deploy/coturn: ready-to-run turnserver.conf + docker-compose + README for
  hosting our own TURN on a VM we own (flat cost, no per-GB billing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:36:05 +05:30
Sravan 5448cf0614 fix(auth): BizGaze-only login + admin sees all sessions
When BIZGAZE_LOGIN_URL is configured, verify credentials ONLY against BizGaze
(no local-password fallback) so stale in-app accounts can't shadow a BizGaze
login. Everyone is then provisioned into the same tenant, restoring the admin's
team-scoped "see all sessions" report.

- login: BizGaze-only when the IdP is configured; local path kept for dev/tests
- provisionFromBizgaze: keep role in sync with BizGaze (isAdmin) on every login;
  optional ADMIN_EMAILS allowlist as a lockout safety net
- block POST /api/users (add local agent) when BizGaze is the IdP — this is what
  previously split tenants
- scripts/migrate-bizgaze-only.js: one-time, dry-run-by-default cleanup that
  deletes pre-BizGaze local accounts (no sso_user_created audit entry)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 19:02:08 +05:30
Sravan d045847a59 added Username or password do not match" + lockout warning 2026-06-12 01:13:31 +05:30
Sravan ba8bfc3f46 feat: BizGaze Connect home, BizGaze login, modular backend, /api/v1
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>
2026-06-12 00:40:07 +05:30