mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
assignment adjustments and ui tweakings
This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
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
@@ -12,8 +12,8 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<!-- site-metadata:inject -->
|
<!-- site-metadata:inject -->
|
||||||
<!-- analytics:inject -->
|
<!-- analytics:inject -->
|
||||||
<script type="module" crossorigin src="/assets/index-2kYHs9Qn.js"></script>
|
<script type="module" crossorigin src="/assets/index-CJaywZ5_.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CX--Qo38.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D2qTDWX0.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ function pickRover(socket, options = {}) {
|
|||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Eligibility is resolved above, while this shared comparator owns only the
|
Eligibility is resolved above, while this shared comparator owns only the
|
||||||
requested placement order: undocked-and-empty, battery, then driver count.
|
requested placement order: empty, undocked when empty, driver count, then
|
||||||
|
battery percentage.
|
||||||
Keeping those concerns separate prevents a ranking change from weakening
|
Keeping those concerns separate prevents a ranking change from weakening
|
||||||
lock, private-rover, role, or mode access checks.
|
lock, private-rover, role, or mode access checks.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,13 +36,36 @@ function batteryPercentage(rover) {
|
|||||||
|
|
||||||
function compareRoversForAssignment(left, right) {
|
function compareRoversForAssignment(left, right) {
|
||||||
/*
|
/*
|
||||||
An undocked rover with nobody assigned is the most useful placement because
|
Spread drivers across the fleet before adding another person to an existing
|
||||||
it starts a fresh driving session without adding another user to a queue.
|
rover queue. This comparison is deliberately independent of battery: a
|
||||||
Both conditions must be true to receive this first-priority rank.
|
small battery-percentage difference should never concentrate users on one
|
||||||
|
rover while another eligible rover has nobody assigned.
|
||||||
*/
|
*/
|
||||||
const leftReadyAndEmpty = readDockedState(left) === false && driverCount(left) === 0;
|
const leftDrivers = driverCount(left);
|
||||||
const rightReadyAndEmpty = readDockedState(right) === false && driverCount(right) === 0;
|
const rightDrivers = driverCount(right);
|
||||||
if (leftReadyAndEmpty !== rightReadyAndEmpty) return leftReadyAndEmpty ? -1 : 1;
|
const leftEmpty = leftDrivers === 0;
|
||||||
|
const rightEmpty = rightDrivers === 0;
|
||||||
|
if (leftEmpty !== rightEmpty) return leftEmpty ? -1 : 1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
When both choices are empty, prefer the rover that is already away from its
|
||||||
|
dock. Docking state does not separate occupied rovers because queue balance
|
||||||
|
is more useful there, and an existing driver may already be handling the
|
||||||
|
rover's physical state. Unknown docking telemetry receives no undocked
|
||||||
|
preference rather than being guessed as ready.
|
||||||
|
*/
|
||||||
|
if (leftEmpty && rightEmpty) {
|
||||||
|
const leftUndocked = readDockedState(left) === false;
|
||||||
|
const rightUndocked = readDockedState(right) === false;
|
||||||
|
if (leftUndocked !== rightUndocked) return leftUndocked ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
For occupied rovers, queue length is the primary balancing signal. This is
|
||||||
|
intentionally evaluated before battery so a one-percent battery advantage
|
||||||
|
cannot cause every later user to pile onto the same rover.
|
||||||
|
*/
|
||||||
|
if (leftDrivers !== rightDrivers) return leftDrivers - rightDrivers;
|
||||||
|
|
||||||
const leftBattery = batteryPercentage(left);
|
const leftBattery = batteryPercentage(left);
|
||||||
const rightBattery = batteryPercentage(right);
|
const rightBattery = batteryPercentage(right);
|
||||||
@@ -52,12 +75,11 @@ function compareRoversForAssignment(left, right) {
|
|||||||
if (leftHasBattery && leftBattery !== rightBattery) return rightBattery - leftBattery;
|
if (leftHasBattery && leftBattery !== rightBattery) return rightBattery - leftBattery;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Battery-equivalent rovers are balanced by current assignment load. Returning
|
Returning zero is intentional. assignmentService randomly selects from the
|
||||||
zero after this comparison is intentional: assignmentService randomly picks
|
complete best tier so stable Map insertion order cannot permanently favor a
|
||||||
within that exact best tier so stable Map insertion order does not create a
|
rover whose emptiness, docking state, load, and battery are all equivalent.
|
||||||
permanent favorite rover.
|
|
||||||
*/
|
*/
|
||||||
return driverCount(left) - driverCount(right);
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -22,21 +22,38 @@ function rankedIds(entries) {
|
|||||||
return entries.sort(compareRoversForAssignment).map((entry) => entry.id);
|
return entries.sort(compareRoversForAssignment).map((entry) => entry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('an undocked empty rover outranks every rover that is docked or occupied', () => {
|
test('an empty rover outranks an occupied rover regardless of battery or docking state', () => {
|
||||||
const result = rankedIds([
|
const result = rankedIds([
|
||||||
rover({ id: 'occupied-high', docked: false, battery: 100, drivers: 1 }),
|
rover({ id: 'occupied-high', docked: false, battery: 100, drivers: 1 }),
|
||||||
rover({ id: 'docked-high', docked: true, battery: 100 }),
|
rover({ id: 'docked-empty', docked: true, battery: 20 }),
|
||||||
rover({ id: 'ready-empty', docked: false, battery: 20 }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(result[0], 'ready-empty');
|
assert.deepEqual(result, ['docked-empty', 'occupied-high']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('battery percentage ranks rovers after undocked-and-empty readiness', () => {
|
test('an undocked rover is preferred when both rovers are empty', () => {
|
||||||
const result = rankedIds([
|
const result = rankedIds([
|
||||||
rover({ id: 'low', docked: false, battery: 35 }),
|
rover({ id: 'docked-high', docked: true, battery: 100 }),
|
||||||
rover({ id: 'high', docked: false, battery: 90 }),
|
rover({ id: 'undocked-low', docked: false, battery: 20 }),
|
||||||
rover({ id: 'middle', docked: false, battery: 60 }),
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ['undocked-low', 'docked-high']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lowest driver count ranks occupied rovers before battery percentage', () => {
|
||||||
|
const result = rankedIds([
|
||||||
|
rover({ id: 'busy-high', docked: false, battery: 100, drivers: 4 }),
|
||||||
|
rover({ id: 'quieter-low', docked: false, battery: 20, drivers: 1 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ['quieter-low', 'busy-high']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('battery percentage ranks rovers after availability and load are equal', () => {
|
||||||
|
const result = rankedIds([
|
||||||
|
rover({ id: 'low', docked: false, battery: 35, drivers: 1 }),
|
||||||
|
rover({ id: 'high', docked: false, battery: 90, drivers: 1 }),
|
||||||
|
rover({ id: 'middle', docked: false, battery: 60, drivers: 1 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.deepEqual(result, ['high', 'middle', 'low']);
|
assert.deepEqual(result, ['high', 'middle', 'low']);
|
||||||
@@ -51,15 +68,6 @@ test('known battery percentage outranks missing battery telemetry', () => {
|
|||||||
assert.deepEqual(result, ['known', 'unknown']);
|
assert.deepEqual(result, ['known', 'unknown']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('driver count breaks a battery-percentage tie', () => {
|
|
||||||
const result = rankedIds([
|
|
||||||
rover({ id: 'busy', docked: false, battery: 70, drivers: 3 }),
|
|
||||||
rover({ id: 'less-busy', docked: false, battery: 70, drivers: 1 }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert.deepEqual(result, ['less-busy', 'busy']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exactly equivalent rovers remain tied for random selection by assignmentService', () => {
|
test('exactly equivalent rovers remain tied for random selection by assignmentService', () => {
|
||||||
const left = rover({ id: 'left', docked: false, battery: 80, drivers: 1 });
|
const left = rover({ id: 'left', docked: false, battery: 80, drivers: 1 });
|
||||||
const right = rover({ id: 'right', docked: false, battery: 80, drivers: 1 });
|
const right = rover({ id: 'right', docked: false, battery: 80, drivers: 1 });
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Purpose: Combines the battery/current gauge with a compact attached advanced-power expansion.
|
// Purpose: Combines the battery/current gauge with a compact attached advanced-power expansion.
|
||||||
import { createElement, useMemo } from 'react';
|
import { createElement, useMemo } from 'react';
|
||||||
import { FaArrowDown, FaArrowUp, FaBatteryHalf, FaBolt, FaExclamationTriangle, FaMemory, FaThermometerHalf, FaWifi } from 'react-icons/fa';
|
import { FaArrowDown, FaArrowUp, FaBatteryHalf, FaBolt, FaExclamationTriangle, FaMemory, FaThermometerHalf, FaWifi } from 'react-icons/fa';
|
||||||
import { useControlSelector } from '../../../../controls/index.js';
|
|
||||||
import { useSessionSelector } from '../../../../context/SessionContext.jsx';
|
import { useSessionSelector } from '../../../../context/SessionContext.jsx';
|
||||||
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
|
import { useTelemetrySelector } from '../../../../context/TelemetryContext.jsx';
|
||||||
import { hostStatsEqual, selectHostStats, selectSpectatorTelemetry, spectatorTelemetryEqual } from '../../../../context/telemetryViews.js';
|
import { hostStatsEqual, selectHostStats, selectSpectatorTelemetry, spectatorTelemetryEqual } from '../../../../context/telemetryViews.js';
|
||||||
@@ -70,7 +69,6 @@ function SpeedTile({ icon, label, value, colorClass }) {
|
|||||||
export default function TopRightPod({ roverId }) {
|
export default function TopRightPod({ roverId }) {
|
||||||
const [batteryOpen, setBatteryOpen] = usePodVisibility('battery', true);
|
const [batteryOpen, setBatteryOpen] = usePodVisibility('battery', true);
|
||||||
const [powerOpen, setPowerOpen] = usePodVisibility('advancedPower', false);
|
const [powerOpen, setPowerOpen] = usePodVisibility('advancedPower', false);
|
||||||
const dockAssistActive = useControlSelector((control) => Boolean(control.state.manualDockAssist?.active));
|
|
||||||
const batteryState = useSessionSelector((state) => {
|
const batteryState = useSessionSelector((state) => {
|
||||||
const rover = (state.session?.roster || []).find((entry) => String(entry.id) === String(roverId));
|
const rover = (state.session?.roster || []).find((entry) => String(entry.id) === String(roverId));
|
||||||
return rover?.batteryState || null;
|
return rover?.batteryState || null;
|
||||||
@@ -104,26 +102,11 @@ export default function TopRightPod({ roverId }) {
|
|||||||
const download = finite(wifi.downloadMbps);
|
const download = finite(wifi.downloadMbps);
|
||||||
const upload = finite(wifi.uploadMbps);
|
const upload = finite(wifi.uploadMbps);
|
||||||
const docked = Boolean(electrical?.homeBase);
|
const docked = Boolean(electrical?.homeBase);
|
||||||
const chargingLabel = String(electrical?.chargingStateLabel || '').toLowerCase();
|
const warningMessage = urgentBattery ? 'BATTERY CRITICAL, DOCK NOW' : 'Battery low, please dock soon.';
|
||||||
const charging = docked && chargingLabel !== '' && chargingLabel !== 'not charging';
|
|
||||||
const autoDocking = !docked && !dockAssistActive && String(electrical?.oiModeLabel || '').toLowerCase() === 'passive';
|
|
||||||
|
|
||||||
// The warning describes the next useful fact instead of blindly telling every user to dock.
|
|
||||||
// This matters during assist, autonomous docking, and charging, where the old overlay's generic
|
|
||||||
// instruction was either redundant or actively misleading.
|
|
||||||
let warningMessage = urgentBattery ? 'Battery critical · Dock now' : 'Battery low · Dock soon';
|
|
||||||
if (charging) {
|
|
||||||
warningMessage = urgentBattery ? 'Battery critical · Charging' : 'Battery low · Charging';
|
|
||||||
} else if (docked) {
|
|
||||||
warningMessage = urgentBattery ? 'Battery critical · On dock' : 'Battery low · On dock';
|
|
||||||
} else if (dockAssistActive) {
|
|
||||||
warningMessage = urgentBattery ? 'Battery critical · Continue docking' : 'Battery low · Continue docking';
|
|
||||||
} else if (autoDocking) {
|
|
||||||
warningMessage = urgentBattery ? 'Battery critical · Returning to dock' : 'Battery low · Returning to dock';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-auto absolute right-0 top-0 z-20 flex flex-col items-end">
|
<>
|
||||||
|
<div className="pointer-events-auto absolute right-0 top-0 z-20 flex flex-col items-end">
|
||||||
{batteryOpen ? (
|
{batteryOpen ? (
|
||||||
<div className="relative flex h-[8.5rem] w-[8.5rem] items-center justify-center rounded-bl-[4.25rem] bg-black/60">
|
<div className="relative flex h-[8.5rem] w-[8.5rem] items-center justify-center rounded-bl-[4.25rem] bg-black/60">
|
||||||
{/* Let the gauge geometry define the visible inset so this pod does not carry an
|
{/* Let the gauge geometry define the visible inset so this pod does not carry an
|
||||||
@@ -150,22 +133,6 @@ export default function TopRightPod({ roverId }) {
|
|||||||
<CornerPodToggle corner="top-right" expanded={false} label="Show battery pod" onClick={() => setBatteryOpen(true)} />
|
<CornerPodToggle corner="top-right" expanded={false} label="Show battery pod" onClick={() => setBatteryOpen(true)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* This is status, not another docking control. Keeping it attached to the battery pod
|
|
||||||
preserves one canonical Dock action while still making the reason for urgency obvious. */}
|
|
||||||
{lowBattery ? (
|
|
||||||
<div
|
|
||||||
className={`absolute top-12 flex items-center gap-1.5 whitespace-nowrap rounded-l px-2.5 py-1.5 text-xs font-bold text-white transition-[right,background-color] ${
|
|
||||||
batteryOpen ? 'right-[8.5rem]' : 'right-0'
|
|
||||||
} ${
|
|
||||||
urgentBattery ? 'bg-red-950' : 'bg-amber-950'
|
|
||||||
}`}
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<FaExclamationTriangle className={urgentBattery ? 'text-red-300' : 'text-amber-300'} aria-hidden="true" />
|
|
||||||
<span>{warningMessage}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Advanced power is an independently persisted right-edge expansion. Its own arrow is
|
{/* Advanced power is an independently persisted right-edge expansion. Its own arrow is
|
||||||
retained when closed, and the whole panel moves into the corner if the pod closes. */}
|
retained when closed, and the whole panel moves into the corner if the pod closes. */}
|
||||||
{powerOpen ? (
|
{powerOpen ? (
|
||||||
@@ -187,6 +154,22 @@ export default function TopRightPod({ roverId }) {
|
|||||||
<ExpansionToggle direction="left" label="Show power and computer" onClick={() => setPowerOpen(true)} />
|
<ExpansionToggle direction="left" label="Show power and computer" onClick={() => setPowerOpen(true)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Battery danger is a stage-level warning, so it belongs near the user's focus instead
|
||||||
|
of beside the corner gauge. It deliberately has only two stable messages and no
|
||||||
|
animation; severity comes from its size and solid color rather than visual noise. */}
|
||||||
|
{lowBattery && !docked ? (
|
||||||
|
<div
|
||||||
|
className={`pointer-events-none absolute left-1/2 top-[58%] z-[55] flex -translate-x-1/2 -translate-y-1/2 items-center gap-2 whitespace-nowrap px-4 py-2 font-bold text-white shadow-xl ${
|
||||||
|
urgentBattery ? 'bg-red-950 text-xl' : 'bg-amber-950 text-base'
|
||||||
|
}`}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<FaExclamationTriangle className={urgentBattery ? 'text-red-300' : 'text-amber-300'} aria-hidden="true" />
|
||||||
|
<span>{warningMessage}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
// Purpose: Keeps the narrow gap geometry used to reveal page themes consistent across layouts.
|
// Purpose: Keeps the narrow gap geometry used to reveal page themes consistent across layouts.
|
||||||
// Scope: Layout-only values; selecting a theme never changes panel dimensions or input placement.
|
// Scope: Layout-only values; selecting a theme never changes panel dimensions or input placement.
|
||||||
|
|
||||||
export const themeGapClass = 'gap-0.5';
|
export const themeGapClass = 'gap-1';
|
||||||
export const themeStackClass = 'space-y-0.5';
|
export const themeStackClass = 'space-y-1';
|
||||||
|
|||||||
Reference in New Issue
Block a user