This commit is contained in:
legop3
2026-03-17 18:54:31 -04:00
parent 0e4954fadf
commit f63db3fdd7
5 changed files with 51 additions and 37 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-D3RnimBF.js"></script> <script type="module" crossorigin src="/assets/index-CIRCS_69.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-WqYsCIRI.css"> <link rel="stylesheet" crossorigin href="/assets/index-WqYsCIRI.css">
</head> </head>
<body> <body>
+2 -3
View File
@@ -6,7 +6,6 @@ const io = require('../globals/io');
const logger = require('../globals/logger').child('audioForwardService'); const logger = require('../globals/logger').child('audioForwardService');
const { loadConfig } = require('../helpers/configLoader'); const { loadConfig } = require('../helpers/configLoader');
const roverManager = require('./roverManager'); const roverManager = require('./roverManager');
const { isAdmin } = require('./roleService');
const { isVerified } = require('./verificationService'); const { isVerified } = require('./verificationService');
const audioForwardEvents = new EventEmitter(); const audioForwardEvents = new EventEmitter();
@@ -92,8 +91,8 @@ function ensureVipVerified(socket) {
function ensureAudioForwardPermission(socket, roverId) { function ensureAudioForwardPermission(socket, roverId) {
ensureVipVerified(socket); ensureVipVerified(socket);
if (!isAdmin(socket) && !roverManager.canDrive(roverId, socket)) { if (!roverManager.isDriver(roverId, socket)) {
throw new Error('Not your turn or no control'); throw new Error('Audio forwarding is only allowed on your own rover');
} }
} }
+7 -1
View File
@@ -16,6 +16,11 @@ export default function VipPanel() {
const isVerified = Boolean(session?.isVerified); const isVerified = Boolean(session?.isVerified);
const pendingRequestId = session?.verification?.pendingRequestId || null; const pendingRequestId = session?.verification?.pendingRequestId || null;
const roster = useMemo(() => session?.roster ?? [], [session?.roster]); const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
const ownRoverId = String(session?.assignment?.roverId || '').trim();
const ownRoverRoster = useMemo(
() => (ownRoverId ? roster.filter((rover) => rover.id === ownRoverId) : []),
[ownRoverId, roster],
);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const applyIdentityKey = async (nextRaw) => { const applyIdentityKey = async (nextRaw) => {
@@ -35,7 +40,8 @@ export default function VipPanel() {
<section className="panel-section space-y-0.5 text-base"> <section className="panel-section space-y-0.5 text-base">
{isVerified ? ( {isVerified ? (
<VipAudioForwardingCard <VipAudioForwardingCard
roster={roster} roster={ownRoverRoster}
ownRoverId={ownRoverId}
audioForwardByRover={session?.audioForward || {}} audioForwardByRover={session?.audioForward || {}}
playUploadedAudio={playUploadedAudio} playUploadedAudio={playUploadedAudio}
stopUploadedAudio={stopUploadedAudio} stopUploadedAudio={stopUploadedAudio}
@@ -3,6 +3,7 @@ import { MAX_UPLOAD_BYTES, bytesToBase64, fieldClass } from './constants.js';
export default function VipAudioForwardingCard({ export default function VipAudioForwardingCard({
roster = [], roster = [],
ownRoverId = '',
audioForwardByRover = {}, audioForwardByRover = {},
playUploadedAudio, playUploadedAudio,
stopUploadedAudio, stopUploadedAudio,
@@ -11,15 +12,17 @@ export default function VipAudioForwardingCard({
const [selectedRoverId, setSelectedRoverId] = useState(''); const [selectedRoverId, setSelectedRoverId] = useState('');
const [selectedUpload, setSelectedUpload] = useState(null); const [selectedUpload, setSelectedUpload] = useState(null);
const [working, setWorking] = useState(false); const [working, setWorking] = useState(false);
const singleRoverId = roster.length === 1 ? roster[0].id : '';
const targetRoverId = String(selectedRoverId || singleRoverId || ownRoverId || '').trim();
const selectedForwardState = useMemo( const selectedForwardState = useMemo(
() => (selectedRoverId ? audioForwardByRover?.[selectedRoverId] || null : null), () => (targetRoverId ? audioForwardByRover?.[targetRoverId] || null : null),
[audioForwardByRover, selectedRoverId], [audioForwardByRover, targetRoverId],
); );
const handleUploadPlay = async () => { const handleUploadPlay = async () => {
const roverId = String(selectedRoverId || '').trim(); const roverId = targetRoverId;
if (!roverId) { if (!roverId) {
onMessage?.('Select a rover first.'); onMessage?.('Take control of a rover first.');
return; return;
} }
if (!selectedUpload) { if (!selectedUpload) {
@@ -50,9 +53,9 @@ export default function VipAudioForwardingCard({
}; };
const handleUploadStop = async () => { const handleUploadStop = async () => {
const roverId = String(selectedRoverId || '').trim(); const roverId = targetRoverId;
if (!roverId) { if (!roverId) {
onMessage?.('Select a rover first.'); onMessage?.('Take control of a rover first.');
return; return;
} }
setWorking(true); setWorking(true);
@@ -70,29 +73,35 @@ export default function VipAudioForwardingCard({
return ( return (
<section className="surface space-y-0.5 text-sm text-slate-200"> <section className="surface space-y-0.5 text-sm text-slate-200">
<p className="text-slate-300">VIP Audio Forwarding</p> <p className="text-slate-300">VIP Audio Forwarding</p>
<label className="grid gap-0.5 text-xs text-slate-300"> {roster.length > 1 ? (
<span>Target rover</span> <label className="grid gap-0.5 text-xs text-slate-300">
<select <span>Target rover</span>
className={fieldClass} <select
value={selectedRoverId} className={fieldClass}
onChange={(event) => setSelectedRoverId(event.target.value)} value={selectedRoverId}
disabled={working} onChange={(event) => setSelectedRoverId(event.target.value)}
> disabled={working}
<option value="">Select rover</option> >
{roster.map((rover) => ( <option value="">Select rover</option>
<option key={rover.id} value={rover.id}> {roster.map((rover) => (
{rover.name || rover.id} <option key={rover.id} value={rover.id}>
</option> {rover.name || rover.id}
))} </option>
</select> ))}
</label> </select>
</label>
) : (
<div className="surface-muted text-xs text-slate-300 text-center">
Rover: {roster[0]?.name || roster[0]?.id || 'none selected'}
</div>
)}
<label className="grid gap-0.5 text-xs text-slate-300"> <label className="grid gap-0.5 text-xs text-slate-300">
<span>Audio file (mp3 / wav / ogg)</span> <span>Audio file (mp3 / wav / ogg)</span>
<input <input
className={fieldClass} className={fieldClass}
type="file" type="file"
accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg" accept=".mp3,.wav,.ogg,audio/mpeg,audio/wav,audio/ogg"
disabled={working} disabled={working || !targetRoverId}
onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)} onChange={(event) => setSelectedUpload(event.target.files?.[0] || null)}
/> />
</label> </label>
@@ -102,10 +111,10 @@ export default function VipAudioForwardingCard({
</div> </div>
) : null} ) : null}
<div className="flex justify-center gap-0.5"> <div className="flex justify-center gap-0.5">
<button type="button" className="button-dark text-sm" disabled={working} onClick={handleUploadPlay}> <button type="button" className="button-dark text-sm" disabled={working || !targetRoverId} onClick={handleUploadPlay}>
{working ? 'Working...' : 'Play Upload'} {working ? 'Working...' : 'Play Upload'}
</button> </button>
<button type="button" className="button-dark text-sm" disabled={working} onClick={handleUploadStop}> <button type="button" className="button-dark text-sm" disabled={working || !targetRoverId} onClick={handleUploadStop}>
Stop Stop
</button> </button>
</div> </div>