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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-Dk7k-sHL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CCqAmxiE.css">
<script type="module" crossorigin src="/assets/index-5m4BdscJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D0Nncfze.css">
</head>
<body>
<div id="root"></div>
+6 -1
View File
@@ -5,7 +5,7 @@ const { getMode, modeEvents } = require('./modeManager');
const roverManager = require('./roverManager');
const { managerEvents } = roverManager;
const assignmentService = require('./assignmentService');
const { getActiveDrivers, turnEvents } = require('./turnService');
const { getActiveDrivers, getTurnQueues, turnEvents } = require('./turnService');
const { getRoomCameras, roomCameraEvents } = require('./roomCameraService');
const { getState: getHomeAssistantState, homeAssistantEvents } = require('./homeAssistantService');
const { getNickname, nicknameEvents } = require('./nicknameService');
@@ -34,6 +34,7 @@ function buildSession(socket) {
roster: roverManager.getRoster(),
assignment: assignmentService.describeAssignment(socket?.id || ''),
activeDrivers: getActiveDrivers(),
turnQueues: getTurnQueues(),
roomCameras: getRoomCameras(),
homeAssistant: getHomeAssistantState(),
users,
@@ -99,6 +100,10 @@ turnEvents.on('activeDriver', () => {
logger.info('Active driver change; syncing all clients');
syncAll();
});
turnEvents.on('queue', () => {
logger.info('Turn queue change; syncing all clients');
syncAll();
});
roomCameraEvents.on('update', () => {
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 TURN_DURATION_MS = 60 * 1000;
const turnEvents = new EventEmitter();
const turnDeadlines = new Map(); // roverId -> timestamp when current driver expires
function driverAdded(roverId, socketId, force) {
const queue = ensureQueue(roverId);
@@ -35,6 +36,7 @@ function cleanupRover(roverId) {
}
driverQueues.delete(roverId);
activeDrivers.delete(roverId);
turnDeadlines.delete(roverId);
}
function canDrive(roverId, socket) {
@@ -60,6 +62,8 @@ function syncState(roverId) {
queue.current = queue.queue[0] || null;
setActiveDriver(roverId, queue.current);
clearTimeout(queue.timer);
turnDeadlines.delete(roverId);
turnEvents.emit('queue', { roverId });
return;
}
if (!queue.current) {
@@ -67,13 +71,17 @@ function syncState(roverId) {
}
setActiveDriver(roverId, queue.current);
scheduleNextTurn(roverId);
turnEvents.emit('queue', { roverId });
}
function scheduleNextTurn(roverId) {
const queue = driverQueues.get(roverId);
if (!queue) return;
clearTimeout(queue.timer);
const deadline = Date.now() + TURN_DURATION_MS;
turnDeadlines.set(roverId, deadline);
queue.timer = setTimeout(() => advanceTurn(roverId), TURN_DURATION_MS);
turnEvents.emit('queue', { roverId });
}
function advanceTurn(roverId) {
@@ -82,6 +90,8 @@ function advanceTurn(roverId) {
if (queue.queue.length === 0) {
clearTimeout(queue.timer);
setActiveDriver(roverId, null);
turnDeadlines.delete(roverId);
turnEvents.emit('queue', { roverId });
return;
}
const mode = getMode();
@@ -89,6 +99,8 @@ function advanceTurn(roverId) {
queue.current = queue.queue[0] || null;
setActiveDriver(roverId, queue.current);
clearTimeout(queue.timer);
turnDeadlines.delete(roverId);
turnEvents.emit('queue', { roverId });
return;
}
const idx = queue.queue.findIndex((id) => id === queue.current);
@@ -117,6 +129,20 @@ function getActiveDrivers() {
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) {
try {
const { issueCommand } = require('./commandService');
@@ -138,4 +164,5 @@ module.exports = {
canDrive,
getActiveDrivers,
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 { useSettingsNamespace } from '../settings/index.js';
import { useSocket } from '../context/SocketContext.jsx';
@@ -35,6 +35,9 @@ export default function UserListPanel() {
const canSetNickname = session?.role !== 'spectator';
const users = session?.users ?? [];
const selfId = session?.socketId || null;
const isTurnsMode = session?.mode === 'turns';
const turnQueues = session?.turnQueues || {};
const roster = session?.roster || [];
useEffect(() => {
setNicknameInput(value.nickname || '');
@@ -75,6 +78,23 @@ export default function UserListPanel() {
[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) {
event.preventDefault();
if (!canSetNickname) return;
@@ -113,11 +133,61 @@ export default function UserListPanel() {
<div className="space-y-0.5">
<div className="flex items-center justify-between text-sm text-slate-400">
<span>Users</span>
<span className="text-xs text-slate-500">{sorted.length}</span>
<span>{isTurnsMode ? 'Turn queues' : 'Users'}</span>
<span className="text-xs text-slate-500">
{isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
</span>
</div>
<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>
) : (
sorted.map((user) => {