mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
add turns queue to user list panel
This commit is contained in:
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
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user