/* ============================================================ page-screenshot.jsx — Captures admin management (admin only) + silent user-side auto-capture poller ============================================================ */ const { useState, useEffect } = React; /* ---------- USER SIDE: silent auto-capture poller ---------- */ // Runs in EVERY logged-in browser session. Polls /api/screenshot/pending // every 60s; for any approved capture, immediately calls // navigator.mediaDevices.getDisplayMedia() — the browser will prompt the // user to pick a screen/window (per Chrome/Edge/Firefox/Safari spec, this // permission cannot be bypassed by web technology). After the user picks, // a single frame is grabbed + uploaded. function startCaptureAutoPoller() { if (window.__captureAutoPollerStarted) return; window.__captureAutoPollerStarted = true; const SEEN = new Set(); const tick = async () => { try { const r = await fetch('/api/screenshot/pending', { credentials: 'include' }); if (!r.ok) return; const data = await r.json(); for (const cap of (data.items || [])) { if (cap.status !== 'approved' && cap.status !== 'requested') continue; if (SEEN.has(cap.id)) continue; SEEN.add(cap.id); await captureAndUpload(cap.id); } } catch (_) {} }; tick(); setInterval(tick, 60_000); } async function captureAndUpload(captureId) { if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { return; } let stream; try { stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: 'monitor', frameRate: 1 } }); } catch (e) { // User dismissed the browser prompt; nothing to do. return; } try { const track = stream.getVideoTracks()[0]; let bitmap; try { const imageCapture = new ImageCapture(track); bitmap = await imageCapture.grabFrame(); } catch (_) { // Safari fallback — draw the video element to canvas const video = document.createElement('video'); video.srcObject = stream; await video.play(); await new Promise(r => setTimeout(r, 200)); bitmap = video; // canvas.drawImage accepts video element } const canvas = document.createElement('canvas'); canvas.width = bitmap.width || (bitmap.videoWidth || 1920); canvas.height = bitmap.height || (bitmap.videoHeight || 1080); canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height); track.stop(); const blob = await new Promise(r => canvas.toBlob(r, 'image/png')); const buf = await blob.arrayBuffer(); const u8 = new Uint8Array(buf); let bin = ''; const CHUNK = 0x8000; for (let i = 0; i < u8.length; i += CHUNK) { bin += String.fromCharCode.apply(null, u8.subarray(i, i + CHUNK)); } const b64 = btoa(bin); await fetch(`/api/screenshot/${captureId}/upload`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image_b64: b64, mime: 'image/png' }), }); } finally { try { stream.getTracks().forEach(t => t.stop()); } catch (_) {} } } // Kick off the poller as soon as this file loads. try { startCaptureAutoPoller(); } catch (_) {} /* ---------- ADMIN SIDE: Captures management ---------- */ function CapturesAdminPage() { const D = window.STATUS_DATA || {}; const meRole = String((D.ME || {}).org_role || '').toLowerCase(); const isAdmin = meRole === 'admin' || meRole === 'owner'; const employees = D.EMPLOYEES || []; const [captures, setCaptures] = useState([]); const [loading, setLoading] = useState(false); const [targetId, setTargetId] = useState(employees[0]?.id || null); const [note, setNote] = useState(''); const [filter, setFilter] = useState('all'); const [imageOf, setImageOf] = useState(null); // capture row for preview const toast = useToast ? useToast() : { push: () => {} }; const load = async () => { setLoading(true); try { const url = '/api/screenshot/captures' + (filter !== 'all' ? `?status=${filter}` : ''); const r = await fetch(url, { credentials: 'include' }); if (r.ok) { const d = await r.json(); setCaptures(d.items || []); } } catch (_) {} finally { setLoading(false); } }; useEffect(() => { if (isAdmin) load(); }, [filter]); const requestCapture = async () => { if (!targetId) { toast.push({ msg: 'Pick a user', kind: 'warn' }); return; } try { const r = await fetch('/api/screenshot/request', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_id: targetId, note }), }); if (!r.ok) throw new Error(`${r.status}`); toast.push({ msg: 'Capture requested (auto-approved). Their browser will prompt to share screen.', kind: 'ok' }); setNote(''); await load(); } catch (e) { toast.push({ msg: 'Request failed: ' + e.message, kind: 'err' }); } }; if (!isAdmin) { return (
Admin only. Screen capture management is restricted to administrators.
); } const empById = Object.fromEntries(employees.map(e => [e.id, e])); return (
} /> {/* Request card */}
setNote(e.target.value)} style={{ flex: 1, minWidth: 200 }} />
{/* Captures list */}
setFilter(e.target.value)} style={{ fontSize: 12 }}> } /> {captures.length === 0 && !loading ? ( ) : captures.map(c => ( ))}
UserTriggerRequestedCaptured StatusRegionRetentionNote
No captures yet.
{(empById[c.user_id] || {}).display_name || `#${c.user_id}`} {c.trigger} {c.requested_at ? new Date(c.requested_at).toLocaleString() : '—'} {c.captured_at ? new Date(c.captured_at).toLocaleString() : '—'} {c.status} {c.region_code_at_request || '—'} {c.retention_until ? c.retention_until.slice(0, 10) : '—'} {c.note || ''} {c.has_image && ( )}
{imageOf && (
setImageOf(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, }}> {`Capture e.stopPropagation()} />
)}
); } window.CapturesAdminPage = CapturesAdminPage;