BizGaze Connect: chat, meetings, recordings, mobile, directory + UI fixes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:15:29 +05:30
parent d045847a59
commit 27355cec76
21 changed files with 3952 additions and 208 deletions
+206
View File
@@ -81,4 +81,210 @@ CREATE TABLE IF NOT EXISTS sessions_log (
try { db.exec('ALTER TABLE sessions_log ADD COLUMN recording TEXT'); } catch (e) { /* exists */ }
try { db.exec('ALTER TABLE sessions_log ADD COLUMN transcript TEXT'); } catch (e) { /* exists */ }
// Refresh tokens for native (desktop/mobile) clients: long-lived, rotated on use,
// stored as a SHA-256 hash so a DB leak doesn't expose usable tokens.
db.exec(`
CREATE TABLE IF NOT EXISTS refresh_tokens (
token_hash TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
revoked INTEGER NOT NULL DEFAULT 0
);
`);
// API keys for third-party / system integrations (machine-to-machine, no human login).
// Scoped per tenant; the key is stored as a SHA-256 hash (plaintext shown once at creation).
db.exec(`
CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
name TEXT,
key_hash TEXT NOT NULL UNIQUE,
scopes TEXT NOT NULL DEFAULT '',
created_by TEXT,
created_at INTEGER NOT NULL,
last_used_at INTEGER,
revoked INTEGER NOT NULL DEFAULT 0
);
`);
// Outbound webhook subscriptions: per-tenant endpoints that receive signed event
// callbacks (session.started / session.ended). Each has its own signing secret.
db.exec(`
CREATE TABLE IF NOT EXISTS webhooks (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
url TEXT NOT NULL,
secret TEXT NOT NULL,
events TEXT NOT NULL DEFAULT '',
active INTEGER NOT NULL DEFAULT 1,
created_by TEXT,
created_at INTEGER NOT NULL,
last_status INTEGER,
last_error TEXT,
last_at INTEGER
);
`);
// Persistent 1:1 chat between users in the same team.
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
recipient_id TEXT NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL,
read_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_messages_pair ON messages(team_id, sender_id, recipient_id, created_at);
CREATE INDEX IF NOT EXISTS idx_messages_unread ON messages(team_id, recipient_id, sender_id, read_at);
`);
// Migration: a message can quote/reply to another message.
try { db.exec('ALTER TABLE messages ADD COLUMN reply_to TEXT'); } catch (e) { /* exists */ }
// Emoji reactions on messages (one row per user+message+emoji; toggling adds/removes).
db.exec(`
CREATE TABLE IF NOT EXISTS message_reactions (
message_id TEXT NOT NULL,
user_id TEXT NOT NULL,
emoji TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (message_id, user_id, emoji)
);
`);
// File attachments for chat messages (file bytes stored on disk at uploads/<id>).
db.exec(`
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
uploader_id TEXT NOT NULL,
name TEXT NOT NULL,
mime TEXT,
size INTEGER,
created_at INTEGER NOT NULL
);
`);
try { db.exec('ALTER TABLE messages ADD COLUMN attachment_id TEXT'); } catch (e) { /* exists */ }
// Group conversations + membership. (1:1 DMs keep using sender_id/recipient_id directly;
// group messages set conversation_id instead, with recipient_id left blank.)
db.exec(`
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'group',
name TEXT,
created_by TEXT,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS conversation_members (
conversation_id TEXT NOT NULL,
user_id TEXT NOT NULL,
last_read_at INTEGER NOT NULL DEFAULT 0,
joined_at INTEGER NOT NULL,
PRIMARY KEY (conversation_id, user_id)
);
`);
try { db.exec('ALTER TABLE messages ADD COLUMN conversation_id TEXT'); } catch (e) { /* exists */ }
try { db.exec('CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at)'); } catch (e) {}
// Group admins: 1 = this member is an admin (multiple admins allowed). Creator seeded as admin.
try { db.exec('ALTER TABLE conversation_members ADD COLUMN admin INTEGER NOT NULL DEFAULT 0'); } catch (e) { /* exists */ }
try { db.exec('UPDATE conversation_members SET admin=1 WHERE user_id IN (SELECT created_by FROM conversations WHERE conversations.id=conversation_members.conversation_id) AND admin=0'); } catch (e) {}
// Avatars: a user's profile picture (BizGaze photo URL) and a group's uploaded image
// (an attachment id, served via /files/<id> with group-membership auth).
try { db.exec('ALTER TABLE users ADD COLUMN avatar_url TEXT'); } catch (e) { /* exists */ }
try { db.exec('ALTER TABLE conversations ADD COLUMN avatar_id TEXT'); } catch (e) { /* exists */ }
// @mentions on a (group) message: JSON array of mentioned user ids, and/or the literal
// "everyone" for @everyone/@all. Used to highlight and notify mentioned members.
try { db.exec('ALTER TABLE messages ADD COLUMN mentions TEXT'); } catch (e) { /* exists */ }
// Delivered receipt for DMs (double tick): set when the recipient's client acknowledges.
try { db.exec('ALTER TABLE messages ADD COLUMN delivered_at INTEGER'); } catch (e) { /* exists */ }
// Group setting: when 1, only the creator can add/remove members.
try { db.exec('ALTER TABLE conversations ADD COLUMN admin_only INTEGER NOT NULL DEFAULT 0'); } catch (e) { /* exists */ }
// Polls live within a group conversation, attached to a message (the poll's question is
// the message body). options is a JSON array of option strings; votes are one row each.
try { db.exec('ALTER TABLE messages ADD COLUMN poll_id TEXT'); } catch (e) { /* exists */ }
// Activity/event lines (e.g. 'call-start','call-end') render as centered system messages.
try { db.exec('ALTER TABLE messages ADD COLUMN msg_type TEXT'); } catch (e) { /* exists */ }
db.exec(`
CREATE TABLE IF NOT EXISTS polls (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
conversation_id TEXT NOT NULL,
message_id TEXT,
question TEXT NOT NULL,
options TEXT NOT NULL,
multi INTEGER NOT NULL DEFAULT 0,
closed INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS poll_votes (
poll_id TEXT NOT NULL,
user_id TEXT NOT NULL,
option_idx INTEGER NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (poll_id, user_id, option_idx)
);
`);
// Scheduled meetings/calls. Each carries a stable room_code so a scheduled call can be
// joined later (the live mesh room is created on first join). group_id is optional — a
// scheduled meeting may target a specific group conversation or be standalone.
db.exec(`
CREATE TABLE IF NOT EXISTS scheduled_meetings (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
group_id TEXT,
room_code TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
scheduled_at INTEGER NOT NULL,
created_by TEXT NOT NULL,
created_at INTEGER NOT NULL,
ended_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_sched_team ON scheduled_meetings(team_id, scheduled_at);
CREATE INDEX IF NOT EXISTS idx_sched_code ON scheduled_meetings(room_code);
`);
// Invited participants (JSON array of user ids) + a one-shot "10-min reminder sent" flag.
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN participants TEXT'); } catch (e) { /* exists */ }
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN reminded INTEGER NOT NULL DEFAULT 0'); } catch (e) { /* exists */ }
// Cancelled meetings are kept (shown as "Cancelled"), not deleted.
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN cancelled INTEGER NOT NULL DEFAULT 0'); } catch (e) { /* exists */ }
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN duration_mins INTEGER'); } catch (e) { /* exists */ }
// Weekly recurrence: JSON array of weekdays (0=Sun..6=Sat), or null for a one-off.
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN recurrence TEXT'); } catch (e) { /* exists */ }
// Meeting recordings & transcripts. Video bytes live in recordings/m_<id>.webm, transcript text
// in transcripts/m_<id>.txt. Tied to a room (and group/scheduled meeting when applicable) so they
// surface under "Past meetings". kind = 'video' | 'transcript'.
db.exec(`
CREATE TABLE IF NOT EXISTS recordings (
id TEXT PRIMARY KEY,
team_id TEXT NOT NULL,
room TEXT,
group_id TEXT,
meeting_id TEXT,
title TEXT,
kind TEXT NOT NULL,
file TEXT,
mime TEXT,
size INTEGER,
duration_ms INTEGER,
created_by TEXT,
created_by_name TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_rec_team ON recordings(team_id, created_at);
CREATE INDEX IF NOT EXISTS idx_rec_room ON recordings(room);
`);
module.exports = db;