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>
#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>
Adds an ephemeral chat inside meetings (relayed over the meeting signaling socket,
not persisted). A chat button in the meeting bar (with an unread badge) opens a
brand-styled panel: pick "Everyone" or a specific participant (private), send with
Enter/Send. Direct messages are marked private on both sides. Works for guests too.
Chat state resets when you leave the call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
- REGRESSION (More#1): removed overflow-anchor:none (it stopped the view from staying
at the bottom as images settle) + renderThread re-asserts scroll-to-bottom after late
content, unless a 'New messages' divider is shown → chats open at the latest message.
- #9: dynacast off — a screen-share layer was being paused unless a camera track was
also flowing (blank / only-with-camera / slow). Now every published track flows.
- More#7/#9: 1:1 calls track 'answered'; unanswered calls auto-end after ~40s (caller
no longer stuck ringing) and post 'Missed call' instead of a duration; answered calls
show duration from the answer time. meeting-ended reason 'unanswered' → 'No answer'.
- More#6: selected chat now has a thin yellow (brand) boundary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ephemeral over the chat WebSocket (no DB). Composer emits chat-typing on/off
(throttled 2.5s, auto-stop after 4s idle / on send / on leaving the chat).
Server relays to the DM peer or fans out to group members (membership-checked).
Receiver shows 'typing…' in the conversation header subtitle and the sidebar
row preview (brand-blue italic), with per-sender auto-expiry so a dropped 'off'
can't stick. Group shows names ('Alice is typing…', 'N people are typing…').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>