MOREE!!!!

This commit is contained in:
legop3
2026-06-19 01:22:42 -04:00
parent ea8e027be9
commit c7f5cb68f4
22 changed files with 429 additions and 138 deletions
@@ -9,6 +9,7 @@ import OverseerControlPanel from './OverseerControlPanel.jsx';
import ReplaySnapshotHealth from './ReplaySnapshotHealth.jsx';
import AdminIpLogPanel from './AdminIpLogPanel.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
const MODES = [
{ key: 'open', label: 'Open' },
@@ -77,17 +78,23 @@ export default function AdminPanelContent() {
const handleModeChange = async (event) => {
const mode = event.target.value;
trackAnalyticsEvent('admin_mode_change', { mode, status: 'started' });
try {
await setMode(mode);
trackAnalyticsEvent('admin_mode_change', { mode, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('admin_mode_change', { mode, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
}
};
const handleForceControl = async (roverId) => {
trackAnalyticsEvent('admin_force_control', { roverId, status: 'started' });
try {
await requestControl(roverId, { force: true });
trackAnalyticsEvent('admin_force_control', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('admin_force_control', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
}
};
@@ -97,9 +104,12 @@ export default function AdminPanelContent() {
const ok = window.confirm(`Reboot rover "${rover.name || rover.id}" now?`);
if (!ok) return;
setRebootStates((prev) => ({ ...prev, [rover.id]: true }));
trackAnalyticsEvent('rover_reboot_click', { roverId: rover.id, scope: 'admin' });
try {
await rebootRover(rover.id);
trackAnalyticsEvent('rover_reboot_result', { roverId: rover.id, scope: 'admin', status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('rover_reboot_result', { roverId: rover.id, scope: 'admin', status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setRebootStates((prev) => ({ ...prev, [rover.id]: false }));
@@ -113,6 +123,7 @@ export default function AdminPanelContent() {
);
if (!ok) return;
setUpdateStates((prev) => ({ ...prev, [rover.id]: true }));
trackAnalyticsEvent('rover_update_click', { roverId: rover.id });
try {
// The rover acknowledges once the privileged self-update helper has been
// launched, not when the full install finishes. That is deliberate: a
@@ -120,7 +131,9 @@ export default function AdminPanelContent() {
// the same websocket would make the button look failed even when the
// update is doing exactly what it should.
await updateRover(rover.id);
trackAnalyticsEvent('rover_update_result', { roverId: rover.id, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('rover_update_result', { roverId: rover.id, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setUpdateStates((prev) => ({ ...prev, [rover.id]: false }));
@@ -131,9 +144,12 @@ export default function AdminPanelContent() {
const ok = window.confirm('Reboot the server host now? This will disconnect all users.');
if (!ok) return;
setServerRebooting(true);
trackAnalyticsEvent('server_reboot_click', { status: 'started' });
try {
await rebootServer();
trackAnalyticsEvent('server_reboot_click', { status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('server_reboot_click', { status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
setServerRebooting(false);
}
@@ -1,7 +1,7 @@
// Barcode Games Panel
// Purpose: Shows barcode game selection, current game status, and player points in the Activities tab.
// Scope: Keeps the driver-side UI compact but informative; game-specific rules stay in server game modules.
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import useBarcodeGameState from '../../barcodeGames/useBarcodeGameState.js';
import CardFrame from '../CardFrame/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
@@ -192,6 +192,8 @@ function Leaderboard({ players, ownPlayer }) {
export default function BarcodeGamesPanel() {
const { state, connectionState, voteForGame } = useBarcodeGameState();
const [pendingGameId, setPendingGameId] = useState(null);
const activeSignatureRef = useRef('');
const participantCountRef = useRef(0);
const activeGame = state.activeGame;
const display = activeGame?.display || {};
// Voting should stop once the shared game system has moved past selection.
@@ -218,6 +220,42 @@ export default function BarcodeGamesPanel() {
});
}, [connectionState.connected, connectionState.lastReceivedAt, connectionState.stale, state.phase]);
useEffect(() => {
const signature = `${activeGame?.id || 'none'}:${activeGame?.status || state.phase || 'unknown'}`;
if (activeSignatureRef.current === signature) return;
activeSignatureRef.current = signature;
/*
Game lifecycle events are derived from server state instead of button
clicks so they still report games started by another user or by a scanner
workflow outside this panel.
*/
trackAnalyticsEvent('barcode_game_active_change', {
gameId: activeGame?.id || '',
status: activeGame?.status || '',
phase: state.phase || 'unknown',
});
if (activeGame?.status === 'ending' || state.phase === 'ending') {
trackAnalyticsEvent('barcode_game_round_end', {
gameId: activeGame?.id || '',
participantCount: participants.length,
});
}
}, [activeGame?.id, activeGame?.status, participants.length, state.phase]);
useEffect(() => {
if (participants.length <= participantCountRef.current) {
participantCountRef.current = participants.length;
return;
}
participantCountRef.current = participants.length;
trackAnalyticsEvent('barcode_game_player_join', {
gameId: activeGame?.id || '',
participantCount: participants.length,
});
}, [activeGame?.id, participants.length]);
const handleVote = async (gameId) => {
setPendingGameId(gameId);
trackAnalyticsEvent('barcode_game_vote', {
@@ -8,6 +8,7 @@ import { dockTelemetryEqual, selectDockTelemetry } from '../../context/telemetry
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { deriveDriveDockStateFromTelemetry } from './driveDockState.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
function StatusRow({ value, tone = 'neutral' }) {
const toneClasses =
@@ -126,11 +127,14 @@ export default function DriveDockAction({
const handleReturnToDrive = async () => {
if (!roverId || pending) return;
setPending('drive');
trackAnalyticsEvent('drive_return', { roverId, source: 'drive_dock_action' });
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
trackAnalyticsEvent('drive_return_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('drive_return_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setPending(null);
@@ -142,11 +146,14 @@ export default function DriveDockAction({
setConfirmOpen(false);
setShowModal(false);
setPending('drive');
trackAnalyticsEvent('drive_start', { roverId, source: 'drive_dock_action' });
try {
dockAssist.exitAssist();
actions.setMode('drive');
await actions.runMacro('drive-sequence');
trackAnalyticsEvent('drive_start_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('drive_start_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setPending(null);
@@ -156,9 +163,12 @@ export default function DriveDockAction({
const handleConfirmDock = async () => {
if (!roverId || pending) return;
setPending('dock');
trackAnalyticsEvent('dock_assist_start', { roverId });
try {
dockAssist.enterAssist();
trackAnalyticsEvent('dock_assist_start_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('dock_assist_start_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
alert(err.message);
} finally {
setPending(null);
@@ -171,10 +181,12 @@ export default function DriveDockAction({
if (dockDisabled) return;
if (manualAssistActive) {
dockAssist.exitAssist();
trackAnalyticsEvent('dock_assist_exit', { roverId });
return;
}
setShowModal(true);
setConfirmOpen(true);
trackAnalyticsEvent('dock_assist_open', { roverId });
};
// The drive/dock card appears inside the mobile controls and can be held or
@@ -9,6 +9,7 @@ import NightVisionControl from '../NightVisionControl/index.jsx';
import { AUX_ZERO } from './constants.js';
import VacuumControls from './VacuumControls.jsx';
import VerticalCameraTilt from './VerticalCameraTilt.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
function AuxColumnContent() {
const roverId = useControlSelector((control) => control.state.roverId);
@@ -39,11 +40,17 @@ function AuxColumnContent() {
const handleNightVisionToggle = useCallback(
(nextOn) => {
if (!nightVisionAvailable) return;
trackAnalyticsEvent('night_vision_toggle', { roverId, source: 'mobile_control', enabled: Boolean(nextOn) });
setNightVision(nextOn);
},
[nightVisionAvailable, setNightVision],
[nightVisionAvailable, roverId, setNightVision],
);
const handleHornStart = useCallback(() => {
trackAnalyticsEvent('horn_start', { roverId, source: 'mobile_control' });
return startHorn();
}, [roverId, startHorn]);
const handleAuxPress = useCallback(
(id, values) => {
if (disabled) return;
@@ -99,7 +106,7 @@ function AuxColumnContent() {
{hornAvailable ? (
<HornControl
disabled={disabled || hornBlocked}
onStart={startHorn}
onStart={handleHornStart}
onStop={stopHorn}
active={horn?.active}
heat={horn?.heat}
+18 -4
View File
@@ -29,7 +29,7 @@ import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
import BarcodeGamesPanel from '../BarcodeGamesPanel/index.jsx';
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useManualDockAssist } from '../../features/manualDockAssist/useManualDockAssist.js';
import { themeGapClass, themeStackClass } from '../../themeFlags.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
@@ -85,6 +85,20 @@ function DriveDockPanel() {
const upLabel = formatKeyLabel(keymap?.cameraUp?.[0]);
const downLabel = formatKeyLabel(keymap?.cameraDown?.[0]);
const cameraDisabled = Boolean(!roverId || dockAssist.cameraLocked);
const trackedControls = useMemo(
() => ({
setNightVision: (nextOn) => {
trackAnalyticsEvent('night_vision_toggle', { roverId, source: 'desktop_control', enabled: Boolean(nextOn) });
setNightVision(nextOn);
},
startHorn: () => {
trackAnalyticsEvent('horn_start', { roverId, source: 'desktop_control' });
return startHorn();
},
stopHorn,
}),
[roverId, setNightVision, startHorn, stopHorn],
);
return (
<CardFrame
@@ -100,15 +114,15 @@ function DriveDockPanel() {
<NightVisionControl
nightVisionOn={nightVisionState?.nightVisionOn}
disabled={!roverId}
onToggle={setNightVision}
onToggle={trackedControls.setNightVision}
keyLabel={nightVisionLabel}
/>
)}
{hornAvailable && (
<HornControl
disabled={!roverId || hornBlocked}
onStart={startHorn}
onStop={stopHorn}
onStart={trackedControls.startHorn}
onStop={trackedControls.stopHorn}
keyLabel={hornLabel}
active={horn?.active}
heat={horn?.heat}
@@ -10,6 +10,7 @@ import { useVideoRequests } from '../../hooks/useVideoRequests.js';
import { useRoverSnapshots } from '../../hooks/useRoverSnapshots.js';
import { useSettingsNamespace } from '../../settings/index.js';
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { trackAnalyticsEventThrottled } from '../../analytics/index.js';
import {
RESTART_DELAY_MS,
UNMUTE_RETRY_MS,
@@ -323,6 +324,20 @@ export default function RoverMediaPlayer({
ensurePlayback();
}
if (['error', 'failed', 'disconnected', 'closed'].includes(nextStatus)) {
/*
WHEP status changes can repeat during automatic restart attempts. The
throttled event keeps media reliability visible in analytics without
reporting every retry loop as a separate user action.
*/
trackAnalyticsEventThrottled(
'whep_player_error',
{
roverId: effectiveRoverId,
status: nextStatus,
detail: info || '',
},
{ key: `${effectiveRoverId || 'unknown'}:${nextStatus}:${info || ''}`, throttleMs: 60 * 1000 },
);
scheduleRestart();
}
};
@@ -339,6 +354,15 @@ export default function RoverMediaPlayer({
if (!active) return;
setStatus('error');
setDetail(err.message);
trackAnalyticsEventThrottled(
'whep_player_error',
{
roverId: effectiveRoverId,
status: 'start_failed',
detail: err.message,
},
{ key: `${effectiveRoverId || 'unknown'}:start_failed:${err.message}`, throttleMs: 60 * 1000 },
);
scheduleRestart();
});
@@ -356,6 +380,7 @@ export default function RoverMediaPlayer({
ensurePlayback,
hasDedicatedAudio,
logAudio,
effectiveRoverId,
]);
useEffect(() => {
@@ -6,6 +6,7 @@ import { useSessionActions, useSessionSelector } from '../../context/SessionCont
import { useSharedClock } from '../../hooks/useSharedClock.js';
import CardFrame from '../CardFrame/index.jsx';
import RoverLabel from '../RoverLabel/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
function classNames(...values) {
return values.filter(Boolean).join(' ');
@@ -90,9 +91,22 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
async function handleRequest(targetRoverId) {
if (!targetRoverId) return;
setPending((prev) => ({ ...prev, [targetRoverId]: true }));
trackAnalyticsEvent('rover_queue_join', {
roverId: targetRoverId,
alreadyAssigned: assignedRoverId === String(targetRoverId),
});
try {
await requestControl(targetRoverId);
trackAnalyticsEvent('rover_queue_join_result', {
roverId: targetRoverId,
status: 'accepted',
});
} catch (err) {
trackAnalyticsEvent('rover_queue_join_result', {
roverId: targetRoverId,
status: 'failed',
reason: err?.message || 'unknown',
});
alert(err.message);
} finally {
setPending((prev) => ({ ...prev, [targetRoverId]: false }));
@@ -107,10 +121,18 @@ export default function RoverQueuesPanel({ title = 'Rovers' }) {
const ok = window.confirm(`Reboot your rover "${assignedRoverName}" now?`);
if (!ok) return;
setRebootPending(true);
trackAnalyticsEvent('rover_reboot_click', { roverId: assignedRoverId, scope: 'own_rover' });
try {
await rebootOwnRover();
trackAnalyticsEvent('rover_reboot_result', { roverId: assignedRoverId, scope: 'own_rover', status: 'accepted' });
alert('Reboot command sent.');
} catch (err) {
trackAnalyticsEvent('rover_reboot_result', {
roverId: assignedRoverId,
scope: 'own_rover',
status: 'failed',
reason: err?.message || 'unknown',
});
alert(err.message);
} finally {
setRebootPending(false);
@@ -18,6 +18,7 @@ import { useSettingsNamespace } from '../../settings/index.js';
import { useSocket } from '../../context/SocketContext.jsx';
import { AUDIO_SETTINGS_DEFAULTS, VIDEO_SETTINGS_DEFAULTS } from '../../settings/namespaces.js';
import { formatKeyLabel } from '../../controls/keymapUtils.js';
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../analytics/index.js';
const manualTabs = [
{ key: 'start', label: 'Start OI' },
@@ -142,6 +143,7 @@ export default function SettingsPanel() {
const handleTransportChange = (event) => {
const next = event.target.value;
savePageSettings((current) => ({ ...(current ?? {}), connectionTransport: next }));
trackAnalyticsEvent('settings_change', { setting: 'connection_transport', value: next });
if (!socket?.io?.opts) return;
socket.io.opts.transports = next === 'polling' ? ['polling'] : ['websocket', 'polling'];
socket.disconnect();
@@ -152,21 +154,30 @@ export default function SettingsPanel() {
const raw = Number(event.target.value);
const next = Number.isFinite(raw) ? Math.max(0, Math.min(1, raw)) : 0;
saveAudioSettings((current) => ({ ...(current ?? {}), [key]: next }));
trackAnalyticsEventThrottled(
'settings_change',
{ setting: key, value: next },
{ key: `audio:${key}`, throttleMs: 3 * 1000 },
);
};
const handleMainBrushDuckEnabled = (event) => {
const checked = Boolean(event.target.checked);
saveAudioSettings((current) => ({ ...(current ?? {}), mainBrushDuckEnabled: checked }));
trackAnalyticsEvent('settings_change', { setting: 'mainBrushDuckEnabled', value: checked });
};
const handleSwapMobileControlColumns = (event) => {
const checked = Boolean(event.target.checked);
savePageSettings((current) => ({ ...(current ?? {}), swapMobileControlColumns: checked }));
trackAnalyticsEvent('mobile_controls_swap', { enabled: checked });
trackAnalyticsEvent('settings_change', { setting: 'swapMobileControlColumns', value: checked });
};
const handleDriveMacroBackoffEnabled = (event) => {
const checked = Boolean(event.target.checked);
savePageSettings((current) => ({ ...(current ?? {}), driveMacroBackoffEnabled: checked }));
trackAnalyticsEvent('settings_change', { setting: 'driveMacroBackoffEnabled', value: checked });
};
const handleVideoFilterChange = (event) => {
@@ -178,6 +189,7 @@ export default function SettingsPanel() {
...(current ?? {}),
colorFilter: nextFilter,
}));
trackAnalyticsEvent('settings_change', { setting: 'videoColorFilter', value: nextFilter });
};
return (
@@ -5,6 +5,7 @@ import * as FaIcons from 'react-icons/fa';
import { FaLink } from 'react-icons/fa';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import { getSocialById } from '../../lib/socials.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
function sanitizeCssColor(value) {
if (typeof value !== 'string') return null;
@@ -38,6 +39,16 @@ export default function SocialButton({ id = null, label, url, icon, color, layou
target="_blank"
rel="noopener noreferrer"
aria-label={text}
onClick={() => {
trackAnalyticsEvent('social_link_click', {
id: id || '',
label: text,
layout,
});
if (id === 'discord') {
trackAnalyticsEvent('discord_click', { layout });
}
}}
className={`grid h-full min-h-0 w-full place-items-center rounded-md bg-slate-700 px-0.5 ${isInline ? 'py-0.5' : 'pb-3'} text-center text-sm font-medium text-white transition hover:opacity-90 ${className}`}
style={bgColor ? { backgroundColor: bgColor } : undefined}
>
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { useSocket } from '../../context/SocketContext.jsx';
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../analytics/index.js';
const CONNECTED_FADE_DELAY_MS = 4000;
@@ -41,23 +42,35 @@ export default function SocketConnectionPill() {
setLastReason('');
show();
scheduleFade();
trackAnalyticsEvent('socket_connect');
};
const onDisconnect = (reason) => {
setConnected(false);
setLastReason(typeof reason === 'string' ? reason : 'disconnected');
show();
trackAnalyticsEventThrottled(
'socket_disconnect',
{ reason: typeof reason === 'string' ? reason : 'disconnected' },
{ key: `disconnect:${reason || 'unknown'}`, throttleMs: 30 * 1000 },
);
};
const onConnectError = (err) => {
setConnected(false);
setLastReason(err?.message || 'connect error');
show();
trackAnalyticsEventThrottled(
'socket_connect_error',
{ message: err?.message || 'connect error' },
{ key: `connect_error:${err?.message || 'unknown'}`, throttleMs: 30 * 1000 },
);
};
const onReconnectAttempt = () => {
setConnected(false);
show();
trackAnalyticsEventThrottled('socket_reconnect_attempt', {}, { key: 'socket_reconnect_attempt', throttleMs: 30 * 1000 });
};
socket.on('connect', onConnect);
@@ -18,6 +18,7 @@ import { mergeFloatChunks, encodeWavMono16 } from './audioCodec.js';
import StatusIndicator from './StatusIndicator.jsx';
import KeyPill from './KeyPill.jsx';
import CardFrame from '../../CardFrame/index.jsx';
import { trackAnalyticsEvent, trackAnalyticsEventThrottled } from '../../../analytics/index.js';
export default function VipAudioUploadCard({
ownRoverId = '',
@@ -87,8 +88,9 @@ export default function VipAudioUploadCard({
(nextMode) => {
const mode = nextMode === 'clip' ? 'clip' : 'live';
saveVipAudio((current) => ({ ...(current || {}), pttMode: mode }));
trackAnalyticsEvent('vip_ptt_mode_change', { mode, roverId });
},
[saveVipAudio],
[roverId, saveVipAudio],
);
const handleUploadPlay = async () => {
@@ -107,6 +109,11 @@ export default function VipAudioUploadCard({
setWorking(true);
setMessage('');
trackAnalyticsEvent('vip_upload_play', {
roverId,
size: selectedUpload.size,
mime: selectedUpload.type || '',
});
try {
const buffer = await selectedUpload.arrayBuffer();
const dataBase64 = bytesToBase64(new Uint8Array(buffer));
@@ -117,7 +124,9 @@ export default function VipAudioUploadCard({
dataBase64,
});
setMessage('Upload playback started.');
trackAnalyticsEvent('vip_upload_play_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('vip_upload_play_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
setMessage(err?.message || 'Failed to play upload.');
} finally {
setWorking(false);
@@ -132,10 +141,13 @@ export default function VipAudioUploadCard({
setWorking(true);
setMessage('');
trackAnalyticsEvent('vip_upload_stop', { roverId });
try {
await stopUploadedAudio?.(roverId);
setMessage('Upload playback stopped.');
trackAnalyticsEvent('vip_upload_stop_result', { roverId, status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('vip_upload_stop_result', { roverId, status: 'failed', reason: err?.message || 'unknown' });
setMessage(err?.message || 'Failed to stop upload.');
} finally {
setWorking(false);
@@ -366,20 +378,24 @@ export default function VipAudioUploadCard({
if (!send) {
setClipState('idle');
trackAnalyticsEvent('vip_clip_record_cancel', { roverId });
return;
}
if (!roverId) {
setClipState('idle');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'skipped_no_rover' });
return;
}
if (!chunks.length) {
setClipState('idle');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'skipped_empty' });
return;
}
setClipState('sending');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'started' });
try {
const merged = mergeFloatChunks(chunks);
const wavBytes = encodeWavMono16(merged, clipSampleRateRef.current || TARGET_SAMPLE_RATE);
@@ -391,8 +407,10 @@ export default function VipAudioUploadCard({
dataBase64,
});
setClipState('idle');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'accepted' });
} catch (err) {
setClipState('error');
trackAnalyticsEvent('vip_clip_send', { roverId, status: 'failed', reason: err?.message || 'unknown' });
setMessage(err?.message || 'Failed to send PTT clip.');
}
},
@@ -408,6 +426,7 @@ export default function VipAudioUploadCard({
clipChunksRef.current = [];
clipRecordingRef.current = true;
setClipState('recording');
trackAnalyticsEvent('vip_clip_record_start', { roverId });
}, [ensureClipPipeline, roverId]);
useEffect(() => {
@@ -434,11 +453,17 @@ export default function VipAudioUploadCard({
await startWhipMic(roverId);
if (!cancelled) {
setMicState('live');
trackAnalyticsEvent('vip_live_mic_start', { roverId });
}
} catch (err) {
if (!cancelled) {
setMicState('error');
setMessage(err?.message || 'Failed to start mic forwarding.');
trackAnalyticsEventThrottled(
'vip_live_mic_error',
{ roverId, reason: err?.message || 'unknown' },
{ key: `${roverId}:${err?.message || 'unknown'}`, throttleMs: 60 * 1000 },
);
}
await stopMicCapture(roverId);
}
@@ -623,9 +648,11 @@ export default function VipAudioUploadCard({
type="checkbox"
checked={openMicEnabled}
disabled={!roverId || clipMode}
onChange={(event) =>
saveVipAudio((current) => ({ ...(current || {}), openMicEnabled: Boolean(event.target.checked) }))
}
onChange={(event) => {
const enabled = Boolean(event.target.checked);
saveVipAudio((current) => ({ ...(current || {}), openMicEnabled: enabled }));
trackAnalyticsEvent('vip_open_mic_change', { roverId, enabled });
}}
/>
<span>{clipMode ? 'Open mic (live mode only)' : 'Open mic'}</span>
</label>
@@ -5,6 +5,7 @@ import { useMemo, useState } from 'react';
import CardFrame from '../CardFrame/index.jsx';
import { flowWrapClass, innerFlowClass } from './constants.js';
import RoverLabel from '../RoverLabel/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
export default function VipPrivateRoverAccessCard({
requestableRovers = [],
@@ -29,14 +30,24 @@ export default function VipPrivateRoverAccessCard({
if (!roverId) return;
setPendingByRover((prev) => ({ ...prev, [roverId]: true }));
onMessage?.('');
trackAnalyticsEvent('vip_private_rover_request', { roverId, status: 'started' });
try {
const response = await requestPrivateRoverAccess?.(roverId);
trackAnalyticsEvent('vip_private_rover_request', {
roverId,
status: response?.existing ? 'existing' : 'accepted',
});
if (response?.existing) {
onMessage?.('You already have a pending request for that rover.');
} else {
onMessage?.('Private rover access request sent to lockdown admins.');
}
} catch (err) {
trackAnalyticsEvent('vip_private_rover_request', {
roverId,
status: 'failed',
reason: err?.message || 'unknown',
});
onMessage?.(err.message || 'Failed to send request.');
} finally {
setPendingByRover((prev) => ({ ...prev, [roverId]: false }));
@@ -5,6 +5,7 @@ import { useState } from 'react';
import NicknameForm from '../NicknameForm/index.jsx';
import CardFrame from '../CardFrame/index.jsx';
import { flowWrapClass, innerFlowClass, fieldClass } from './constants.js';
import { trackAnalyticsEvent } from '../../analytics/index.js';
export default function VipVerificationCard({
pendingRequestId,
@@ -25,6 +26,7 @@ export default function VipVerificationCard({
setRequestKeyInput(currentStoredKey);
setConfirmNickname(false);
onMessage?.('');
trackAnalyticsEvent('vip_verify_flow_start', { hasNickname: Boolean(nickname) });
};
const cancelRequestFlow = () => {
@@ -44,6 +46,10 @@ export default function VipVerificationCard({
}
setWorking(true);
onMessage?.('');
trackAnalyticsEvent('vip_verify_attempt', {
hasNickname: Boolean(nickname),
hasIdentityKey: Boolean(requestKeyInput),
});
try {
const applied = await applyIdentityKey(requestKeyInput);
await requestVerification?.();
@@ -51,7 +57,9 @@ export default function VipVerificationCard({
setRequestFlowStep(0);
setConfirmNickname(false);
onMessage?.('Verification request sent to lockdown admins.');
trackAnalyticsEvent('vip_verify_result', { status: 'accepted' });
} catch (err) {
trackAnalyticsEvent('vip_verify_result', { status: 'failed', reason: err?.message || 'unknown' });
onMessage?.(err.message || 'Failed to submit request.');
} finally {
setWorking(false);