add turns queue to user list panel

This commit is contained in:
legop3
2025-11-22 13:37:30 -05:00
parent ef8d1f021b
commit 2d07a3ca4c
7 changed files with 121 additions and 19 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title> <title>webui</title>
<script type="module" crossorigin src="/assets/index-Dk7k-sHL.js"></script> <script type="module" crossorigin src="/assets/index-5m4BdscJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CCqAmxiE.css"> <link rel="stylesheet" crossorigin href="/assets/index-D0Nncfze.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+6 -1
View File
@@ -5,7 +5,7 @@ const { getMode, modeEvents } = require('./modeManager');
const roverManager = require('./roverManager'); const roverManager = require('./roverManager');
const { managerEvents } = roverManager; const { managerEvents } = roverManager;
const assignmentService = require('./assignmentService'); const assignmentService = require('./assignmentService');
const { getActiveDrivers, turnEvents } = require('./turnService'); const { getActiveDrivers, getTurnQueues, turnEvents } = require('./turnService');
const { getRoomCameras, roomCameraEvents } = require('./roomCameraService'); const { getRoomCameras, roomCameraEvents } = require('./roomCameraService');
const { getState: getHomeAssistantState, homeAssistantEvents } = require('./homeAssistantService'); const { getState: getHomeAssistantState, homeAssistantEvents } = require('./homeAssistantService');
const { getNickname, nicknameEvents } = require('./nicknameService'); const { getNickname, nicknameEvents } = require('./nicknameService');
@@ -34,6 +34,7 @@ function buildSession(socket) {
roster: roverManager.getRoster(), roster: roverManager.getRoster(),
assignment: assignmentService.describeAssignment(socket?.id || ''), assignment: assignmentService.describeAssignment(socket?.id || ''),
activeDrivers: getActiveDrivers(), activeDrivers: getActiveDrivers(),
turnQueues: getTurnQueues(),
roomCameras: getRoomCameras(), roomCameras: getRoomCameras(),
homeAssistant: getHomeAssistantState(), homeAssistant: getHomeAssistantState(),
users, users,
@@ -99,6 +100,10 @@ turnEvents.on('activeDriver', () => {
logger.info('Active driver change; syncing all clients'); logger.info('Active driver change; syncing all clients');
syncAll(); syncAll();
}); });
turnEvents.on('queue', () => {
logger.info('Turn queue change; syncing all clients');
syncAll();
});
roomCameraEvents.on('update', () => { roomCameraEvents.on('update', () => {
logger.info('Room camera change detected; syncing all clients'); logger.info('Room camera change detected; syncing all clients');
+27
View File
@@ -7,6 +7,7 @@ const driverQueues = new Map(); // roverId -> { queue: [], current: socketId, ti
const activeDrivers = new Map(); const activeDrivers = new Map();
const TURN_DURATION_MS = 60 * 1000; const TURN_DURATION_MS = 60 * 1000;
const turnEvents = new EventEmitter(); const turnEvents = new EventEmitter();
const turnDeadlines = new Map(); // roverId -> timestamp when current driver expires
function driverAdded(roverId, socketId, force) { function driverAdded(roverId, socketId, force) {
const queue = ensureQueue(roverId); const queue = ensureQueue(roverId);
@@ -35,6 +36,7 @@ function cleanupRover(roverId) {
} }
driverQueues.delete(roverId); driverQueues.delete(roverId);
activeDrivers.delete(roverId); activeDrivers.delete(roverId);
turnDeadlines.delete(roverId);
} }
function canDrive(roverId, socket) { function canDrive(roverId, socket) {
@@ -60,6 +62,8 @@ function syncState(roverId) {
queue.current = queue.queue[0] || null; queue.current = queue.queue[0] || null;
setActiveDriver(roverId, queue.current); setActiveDriver(roverId, queue.current);
clearTimeout(queue.timer); clearTimeout(queue.timer);
turnDeadlines.delete(roverId);
turnEvents.emit('queue', { roverId });
return; return;
} }
if (!queue.current) { if (!queue.current) {
@@ -67,13 +71,17 @@ function syncState(roverId) {
} }
setActiveDriver(roverId, queue.current); setActiveDriver(roverId, queue.current);
scheduleNextTurn(roverId); scheduleNextTurn(roverId);
turnEvents.emit('queue', { roverId });
} }
function scheduleNextTurn(roverId) { function scheduleNextTurn(roverId) {
const queue = driverQueues.get(roverId); const queue = driverQueues.get(roverId);
if (!queue) return; if (!queue) return;
clearTimeout(queue.timer); clearTimeout(queue.timer);
const deadline = Date.now() + TURN_DURATION_MS;
turnDeadlines.set(roverId, deadline);
queue.timer = setTimeout(() => advanceTurn(roverId), TURN_DURATION_MS); queue.timer = setTimeout(() => advanceTurn(roverId), TURN_DURATION_MS);
turnEvents.emit('queue', { roverId });
} }
function advanceTurn(roverId) { function advanceTurn(roverId) {
@@ -82,6 +90,8 @@ function advanceTurn(roverId) {
if (queue.queue.length === 0) { if (queue.queue.length === 0) {
clearTimeout(queue.timer); clearTimeout(queue.timer);
setActiveDriver(roverId, null); setActiveDriver(roverId, null);
turnDeadlines.delete(roverId);
turnEvents.emit('queue', { roverId });
return; return;
} }
const mode = getMode(); const mode = getMode();
@@ -89,6 +99,8 @@ function advanceTurn(roverId) {
queue.current = queue.queue[0] || null; queue.current = queue.queue[0] || null;
setActiveDriver(roverId, queue.current); setActiveDriver(roverId, queue.current);
clearTimeout(queue.timer); clearTimeout(queue.timer);
turnDeadlines.delete(roverId);
turnEvents.emit('queue', { roverId });
return; return;
} }
const idx = queue.queue.findIndex((id) => id === queue.current); const idx = queue.queue.findIndex((id) => id === queue.current);
@@ -117,6 +129,20 @@ function getActiveDrivers() {
return map; return map;
} }
function getTurnQueues() {
const mode = getMode();
const payload = {};
driverQueues.forEach((queue, roverId) => {
payload[roverId] = {
mode,
queue: Array.from(queue.queue),
current: queue.current,
deadline: turnDeadlines.get(roverId) || null,
};
});
return payload;
}
function stopRover(roverId) { function stopRover(roverId) {
try { try {
const { issueCommand } = require('./commandService'); const { issueCommand } = require('./commandService');
@@ -138,4 +164,5 @@ module.exports = {
canDrive, canDrive,
getActiveDrivers, getActiveDrivers,
turnEvents, turnEvents,
getTurnQueues,
}; };
+74 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
import { useSettingsNamespace } from '../settings/index.js'; import { useSettingsNamespace } from '../settings/index.js';
import { useSocket } from '../context/SocketContext.jsx'; import { useSocket } from '../context/SocketContext.jsx';
@@ -35,6 +35,9 @@ export default function UserListPanel() {
const canSetNickname = session?.role !== 'spectator'; const canSetNickname = session?.role !== 'spectator';
const users = session?.users ?? []; const users = session?.users ?? [];
const selfId = session?.socketId || null; const selfId = session?.socketId || null;
const isTurnsMode = session?.mode === 'turns';
const turnQueues = session?.turnQueues || {};
const roster = session?.roster || [];
useEffect(() => { useEffect(() => {
setNicknameInput(value.nickname || ''); setNicknameInput(value.nickname || '');
@@ -75,6 +78,23 @@ export default function UserListPanel() {
[selfId, users], [selfId, users],
); );
const rosterName = useCallback(
(roverId) => roster.find((r) => String(r.id) === String(roverId))?.name || roverId,
[roster],
);
const lookupUser = useCallback(
(socketId) => users.find((u) => u.socketId === socketId) || { socketId, nickname: null, role: null },
[users],
);
const secondsRemaining = useCallback((deadline) => {
if (!deadline) return null;
const ms = deadline - Date.now();
if (ms <= 0) return 0;
return Math.ceil(ms / 1000);
}, []);
async function handleSave(event) { async function handleSave(event) {
event.preventDefault(); event.preventDefault();
if (!canSetNickname) return; if (!canSetNickname) return;
@@ -113,11 +133,61 @@ export default function UserListPanel() {
<div className="space-y-0.5"> <div className="space-y-0.5">
<div className="flex items-center justify-between text-sm text-slate-400"> <div className="flex items-center justify-between text-sm text-slate-400">
<span>Users</span> <span>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
<span className="text-xs text-slate-500">{sorted.length}</span> <span className="text-xs text-slate-500">
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
</span>
</div> </div>
<div className="surface h-48 overflow-y-auto space-y-0.25"> <div className="surface h-48 overflow-y-auto space-y-0.25">
{sorted.length === 0 ? ( {isTurnsMode ? (
Object.keys(turnQueues || {}).length === 0 ? (
<p className="text-sm text-slate-500">No turn queues yet.</p>
) : (
Object.entries(turnQueues).map(([roverId, info]) => {
const queue = info?.queue || [];
const deadline = info?.deadline || null;
const remaining = secondsRemaining(deadline);
return (
<div key={roverId} className="surface-muted flex flex-col gap-0.25 text-sm">
<div className="flex items-center gap-1">
<p className="font-semibold text-slate-200">{rosterName(roverId)}</p>
{remaining != null && (
<span className="rounded bg-slate-800 px-1 text-[0.7rem]">
{remaining}s left
</span>
)}
</div>
{queue.length === 0 ? (
<p className="text-[0.75rem] text-slate-500">No drivers queued.</p>
) : (
<div className="flex flex-wrap items-center gap-1">
{queue.map((socketId, idx) => {
const user = lookupUser(socketId);
const isCurrent = socketId === info?.current;
const isAdmin =
user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin';
return (
<span
key={`${roverId}-${socketId}-${idx}`}
className={`flex items-center gap-0.5 rounded px-1 text-[0.8rem] ${
isCurrent ? 'bg-sky-700 text-white' : 'bg-slate-800 text-slate-200'
}`}
>
<span className={`${roleColors(user.role)} font-semibold`}>
{formatLabel(user, selfId)}
</span>
{isAdmin && <span className="text-[0.7rem] text-amber-200"></span>}
{isCurrent && <span className="text-[0.7rem] text-slate-200">now</span>}
</span>
);
})}
</div>
)}
</div>
);
})
)
) : sorted.length === 0 ? (
<p className="text-sm text-slate-500">Waiting for users</p> <p className="text-sm text-slate-500">Waiting for users</p>
) : ( ) : (
sorted.map((user) => { sorted.map((user) => {