Added Record screen, transcribe, mobile screen share bug fix.
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user