Added Record screen, transcribe, mobile screen share bug fix.

This commit is contained in:
2026-06-10 16:46:03 +05:30
parent 3e24cacc39
commit 28f616d829
5 changed files with 320 additions and 32 deletions
+97
View File
@@ -11,6 +11,10 @@ const A = require('./auth');
const PORT = process.env.PORT || 8090;
const HTTPS_PORT = process.env.HTTPS_PORT || 8443;
const PUBLIC_DIR = path.join(__dirname, 'public');
const REC_DIR = path.join(__dirname, 'recordings');
try { fs.mkdirSync(REC_DIR, { recursive: true }); } catch (e) {}
const TRANS_DIR = path.join(__dirname, 'transcripts');
try { fs.mkdirSync(TRANS_DIR, { recursive: true }); } catch (e) {}
const SESSION_TTL = 1000 * 60 * 60 * 24; // 24h auto-logout
// ---------- helpers ----------
@@ -322,6 +326,51 @@ route('GET', '/api/audit', async (req, res) => {
json(res, 200, rows);
});
// ---------- session recording: upload (agent) + download (team) ----------
const MAX_REC_BYTES = 500 * 1024 * 1024; // 500 MB safety cap
route('POST', '/api/recording', async (req, res) => {
const u = currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const sid = new URLSearchParams(req.url.split('?')[1] || '').get('sessionId');
if (!sid) return json(res, 400, { error: 'sessionId required' });
const row = db.prepare('SELECT * FROM sessions_log WHERE id=? AND team_id=?').get(sid, u.team_id);
if (!row) return json(res, 404, { error: 'no such session' });
const chunks = []; let total = 0, aborted = false;
req.on('data', (c) => { total += c.length; if (total > MAX_REC_BYTES) { aborted = true; req.destroy(); return; } chunks.push(c); });
req.on('end', () => {
if (aborted) return json(res, 413, { error: 'recording too large' });
const fname = sid + '.webm';
try {
fs.writeFileSync(path.join(REC_DIR, fname), Buffer.concat(chunks));
db.prepare('UPDATE sessions_log SET recording=? WHERE id=?').run(fname, sid);
audit({ team_id: u.team_id, user_id: u.id, user_email: u.email, action: 'recording_saved', detail: 'session ' + sid });
json(res, 200, { ok: true });
} catch (e) { json(res, 500, { error: 'could not save recording' }); }
});
req.on('error', () => { try { res.end(); } catch (e) {} });
});
route('POST', '/api/transcript', async (req, res) => {
const u = currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const sid = new URLSearchParams(req.url.split('?')[1] || '').get('sessionId');
if (!sid) return json(res, 400, { error: 'sessionId required' });
const row = db.prepare('SELECT * FROM sessions_log WHERE id=? AND team_id=?').get(sid, u.team_id);
if (!row) return json(res, 404, { error: 'no such session' });
const chunks = []; let total = 0, aborted = false;
req.on('data', (c) => { total += c.length; if (total > 5 * 1024 * 1024) { aborted = true; req.destroy(); return; } chunks.push(c); });
req.on('end', () => {
if (aborted) return json(res, 413, { error: 'transcript too large' });
const fname = sid + '.txt';
try {
fs.writeFileSync(path.join(TRANS_DIR, fname), Buffer.concat(chunks));
db.prepare('UPDATE sessions_log SET transcript=? WHERE id=?').run(fname, sid);
json(res, 200, { ok: true });
} catch (e) { json(res, 500, { error: 'could not save transcript' }); }
});
req.on('error', () => { try { res.end(); } catch (e) {} });
});
// ---------- static + router ----------
const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' };
function serveStatic(req, res) {
@@ -343,6 +392,40 @@ function serveStatic(req, res) {
const server = http.createServer(async (req, res) => {
const key = `${req.method} ${req.url.split('?')[0]}`;
if (routes[key]) return routes[key](req, res);
if (req.method === 'GET' && req.url.split('?')[0].startsWith('/transcripts/')) {
const u = currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const name = path.basename(decodeURIComponent(req.url.split('?')[0]));
const sid = name.replace(/\.txt$/i, '');
const row = db.prepare('SELECT * FROM sessions_log WHERE id=? AND team_id=?').get(sid, u.team_id);
if (!row || !row.transcript) return json(res, 404, { error: 'not found' });
const fp = path.join(TRANS_DIR, row.transcript);
if (!fp.startsWith(TRANS_DIR)) return json(res, 403, { error: 'forbidden' });
return fs.stat(fp, (err, st) => {
if (err) return json(res, 404, { error: 'not found' });
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': st.size, 'Content-Disposition': 'attachment; filename="transcript-' + sid + '.txt"', 'Cache-Control': 'no-store' });
const rs = fs.createReadStream(fp);
rs.on('error', () => { try { res.destroy(); } catch (e) {} });
rs.pipe(res);
});
}
if (req.method === 'GET' && req.url.split('?')[0].startsWith('/recordings/')) {
const u = currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
const name = path.basename(decodeURIComponent(req.url.split('?')[0]));
const sid = name.replace(/\.webm$/i, '');
const row = db.prepare('SELECT * FROM sessions_log WHERE id=? AND team_id=?').get(sid, u.team_id);
if (!row || !row.recording) return json(res, 404, { error: 'not found' });
const fp = path.join(REC_DIR, row.recording);
if (!fp.startsWith(REC_DIR)) return json(res, 403, { error: 'forbidden' });
return fs.stat(fp, (err, st) => {
if (err) return json(res, 404, { error: 'not found' });
res.writeHead(200, { 'Content-Type': 'video/webm', 'Content-Length': st.size, 'Content-Disposition': 'attachment; filename="session-' + sid + '.webm"', 'Cache-Control': 'no-store', 'Accept-Ranges': 'bytes' });
const rs = fs.createReadStream(fp);
rs.on('error', () => { try { res.destroy(); } catch (e) {} });
rs.pipe(res);
});
}
if (req.method === 'GET') return serveStatic(req, res);
json(res, 404, { error: 'not found' });
});
@@ -465,6 +548,20 @@ function handle(ws, m, req) {
if (peer && peer.readyState === 1) peer.send(JSON.stringify(m));
break;
}
case 'transcript': {
const sess = liveSessions.get(m.sessionId || ws.sessionId);
if (!sess) return;
const peer = ws === sess.agentWs ? sess.viewerWs : sess.agentWs;
if (peer && peer.readyState === 1) peer.send(JSON.stringify(m));
break;
}
case 'recording': {
const sess = liveSessions.get(m.sessionId || ws.sessionId);
if (!sess) return;
const peer = ws === sess.agentWs ? sess.viewerWs : sess.agentWs;
if (peer && peer.readyState === 1) peer.send(JSON.stringify(m));
break;
}
case 'end-session': {
endSession(ws.sessionId, m.reason || null);
break;