This commit is contained in:
legop3
2026-09-12 16:16:59 -04:00
parent 99d2a7689f
commit fb31ff52bd
28 changed files with 545 additions and 88 deletions
@@ -133,6 +133,7 @@ const {
getRoster,
getRosterForSocket,
broadcastRoster,
setNeedsHelp,
setToggleState,
handleHostStats,
canSeeRover,
@@ -281,6 +282,7 @@ module.exports = {
getRoster,
getRosterForSocket,
broadcastRoster,
setNeedsHelp,
setToggleState,
handleHostStats,
handleSensorFrame,
@@ -46,6 +46,10 @@ function createRosterLifecycle(deps) {
room: `rover:${id}`,
lastSeen: Date.now(),
lastMovementAt: Date.now(),
// Help state belongs to the live rover record so every roster consumer
// sees one server-authoritative answer. The monitoring service owns why
// the flag changes; roverManager only owns publishing the roster field.
needsHelp: false,
private: { enabled: false },
privateOpen: true,
privateSafety: { ...DEFAULT_PRIVATE_SAFETY },
@@ -252,6 +256,7 @@ function createRosterLifecycle(deps) {
: record.meta?.laser,
locked: record.locked || (isPrivateRecord(record) && !isPrivateOpen(record)),
lockReason: record.lockReason || (isPrivateRecord(record) && !isPrivateOpen(record) ? 'private' : null),
needsHelp: Boolean(record.needsHelp),
lastSeen: record.lastSeen,
private: isPrivateRecord(record)
? { enabled: true, open: isPrivateOpen(record), safety: getPrivateSafety(record) }
@@ -299,6 +304,21 @@ function createRosterLifecycle(deps) {
managerEvents.emit('rover', { roverId, action: device, record });
}
function setNeedsHelp(roverId, needsHelp) {
const record = rovers.get(String(roverId));
if (!record) return false;
const next = Boolean(needsHelp);
if (record.needsHelp === next) return false;
// Emit both roster fanout forms used by the application. The lightweight
// `rovers` event updates direct roster listeners immediately, while the
// manager event asks sessionService to rebuild complete session snapshots.
record.needsHelp = next;
broadcastRoster();
managerEvents.emit('help', { roverId: record.id, needsHelp: next });
return true;
}
function handleHostStats(roverId, msg = {}) {
const record = rovers.get(roverId);
if (!record) return;
@@ -345,6 +365,7 @@ function createRosterLifecycle(deps) {
getRosterForSocket,
syncSpectatorRooms,
broadcastRoster,
setNeedsHelp,
setToggleState,
handleHostStats,
canSeeRover,
@@ -329,6 +329,7 @@ function createSensorPipeline(deps) {
function stopDockGuard(roverId) {
const state = dockGuardStates.get(roverId);
if (!state) return;
const wasActive = state.active;
if (state.timer) clearInterval(state.timer);
state.active = false;
state.reason = null;
@@ -336,6 +337,11 @@ function createSensorPipeline(deps) {
state.timer = null;
state.idleUndockedSince = null;
state.passiveUndockedSince = null;
// The help monitor must clear its docking timer at the same ownership
// boundary that stops dock guard. Inferring this from OI mode would be
// incorrect because a 600-series rover can enter passive mode for reasons
// other than the server's automatic docking workflow.
if (wasActive) managerEvents.emit('dockGuard', { roverId, active: false });
}
function attemptDockGuard(roverId) {
@@ -360,6 +366,12 @@ function createSensorPipeline(deps) {
state.active = true;
state.reason = reason;
state.startedAt = Date.now();
managerEvents.emit('dockGuard', {
roverId: record.id,
active: true,
reason,
startedAt: state.startedAt,
});
const reasonText = reason === 'passive' ? 'passive mode' : 'idle and undocked';
sendAlert({
color: ALERT_COLOR,