overseer voting stuff yay!

This commit is contained in:
legop3
2026-05-19 21:08:06 -04:00
parent ab32b0416c
commit 8c2162d918
12 changed files with 142 additions and 21 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
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DUTZ-8ek.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-91zM9j6A.css">
<script type="module" crossorigin src="/assets/index-BD2vTGND.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DTyQMQGe.css">
</head>
<body>
<div id="root"></div>
@@ -5,6 +5,7 @@ const logger = require('../../globals/logger').child('overseerControl');
const { loadConfig } = require('../../helpers/configLoader');
const { getRole, roleEvents } = require('../roleService');
const { getMode, MODES, modeEvents } = require('../modeManager');
const { verificationEvents } = require('../verificationService');
const homeAssistantService = require('../homeAssistantService');
const neatoService = require('../neatoService');
const liftService = require('../liftService');
@@ -89,6 +90,7 @@ let status = {
lastLiveToolCalls: null,
lastOutcome: null,
lastReason: null,
voteStatus: null,
lastError: null,
lastErrorDetails: null,
lastFailedAt: null,
@@ -98,6 +100,26 @@ let status = {
updatedAt: Date.now(),
};
function buildVoteStatus() {
const sockets = Array.from(io.sockets.sockets.values());
let yesCount = 0;
let noCount = 0;
sockets.forEach((socket) => {
const pref = socket?.data?.overseerEnabled;
if (typeof pref === 'boolean' ? pref : true) yesCount += 1;
else noCount += 1;
});
const onlineCount = yesCount + noCount;
const gatePassed = yesCount > noCount;
return {
yesCount,
noCount,
onlineCount,
gatePassed,
running: Boolean(enabled && gatePassed && getMode() !== MODES.LOCKDOWN),
};
}
function updateStatus(patch = {}) {
status = { ...status, ...patch, updatedAt: Date.now() };
const payload = buildAdminState(status, runtime.runHistory);
@@ -460,8 +482,29 @@ function startScheduler(reason = null) {
runtime.timer = setTimeout(tick, gateIntervalMs);
}
function evaluateSchedulerGate(reason = 'gate reevaluated') {
const voteStatus = buildVoteStatus();
const runningAllowed = Boolean(enabled && voteStatus.gatePassed && getMode() !== MODES.LOCKDOWN);
updateStatus({ voteStatus });
if (!enabled) {
stopScheduler('overseerControl.enabled is false');
return;
}
if (getMode() === MODES.LOCKDOWN) {
stopScheduler('paused during lockdown');
return;
}
if (!voteStatus.gatePassed) {
stopScheduler('paused by user vote');
return;
}
startScheduler(reason);
}
io.on('connection', (socket) => {
emitStateToSocket(socket);
evaluateSchedulerGate('online vote update');
socket.on('disconnect', () => evaluateSchedulerGate('online vote update'));
socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => {
if (!isAdminRole(getRole(socket))) return cb({ error: 'Not authorized' });
const action = controls?.action || null;
@@ -474,6 +517,7 @@ io.on('connection', (socket) => {
});
roleEvents.on('change', ({ socket }) => emitStateToSocket(socket));
verificationEvents.on('change', () => evaluateSchedulerGate('online vote update'));
homeAssistantEvents.on('update', () => updateStatus({ phase: status.phase }));
neatoEvents.on('update', () => updateStatus({ phase: status.phase }));
liftEvents.on('update', () => updateStatus({ phase: status.phase }));
@@ -485,7 +529,7 @@ modeEvents.on('change', (mode) => {
logger.info('overseerControl paused due to lockdown mode');
return;
}
startScheduler(observeOnly ? 'observe-only mode' : null);
evaluateSchedulerGate(observeOnly ? 'observe-only mode' : null);
});
if (!enabled) {
@@ -496,9 +540,11 @@ if (!enabled) {
stopScheduler('paused during lockdown');
logger.info('overseerControl paused on startup due to lockdown mode');
} else {
startScheduler(observeOnly ? 'observe-only mode' : null);
evaluateSchedulerGate(observeOnly ? 'observe-only mode' : null);
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, heartbeatMs, observeOnly });
}
}
module.exports = {};
module.exports = {
getVoteStatus: () => status.voteStatus || buildVoteStatus(),
};
@@ -13,6 +13,7 @@ const { getRoomCameras, roomCameraEvents } = require('../roomCameraService');
const { getState: getHomeAssistantState, homeAssistantEvents } = require('../homeAssistantService');
const { getState: getNeatoState, neatoEvents } = require('../neatoService');
const { getState: getLiftState, liftEvents } = require('../liftService');
const { getVoteStatus: getOverseerVoteStatus } = require('../overseerControlService');
const { getNickname, nicknameEvents } = require('../nicknameService');
const {
getVerificationStateForSocket,
@@ -66,6 +67,7 @@ function buildUserEntry(socket) {
}
function buildSession(socket) {
const overseerVote = getOverseerVoteStatus();
const users = Array.from(io.sockets.sockets.values())
.map((sock) => buildUserEntry(sock))
.filter(Boolean)
@@ -124,6 +126,10 @@ function buildSession(socket) {
audioForward: getAudioForwardState(),
audioLevels: getAudioLevels(),
buttonBox: getButtonBoxState(),
overseerVote: {
...overseerVote,
preference: typeof socket?.data?.overseerEnabled === 'boolean' ? socket.data.overseerEnabled : true,
},
};
}
@@ -111,11 +111,18 @@ function identifySocket(socket, payload = {}) {
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'overseerEnabled')) {
data.overseerEnabled = Boolean(payload.overseerEnabled);
} else if (typeof data.overseerEnabled !== 'boolean') {
data.overseerEnabled = true;
}
const verification = reevaluateSocketVerification(socket);
const deterrence = reevaluateSocketDeterrence(socket);
emitChange('identify', { socketId: socket.id });
return {
cookieUserId: data.cookieUserId,
overseerEnabled: Boolean(data.overseerEnabled),
isVerified: verification.isVerified,
isDeterred: deterrence.isDeterred,
reason: verification.reason,
@@ -97,6 +97,7 @@ function createVerificationFlow(deps) {
return {
cookieUserId: normalizeCookieUserId(data.cookieUserId) || null,
nickname: getNickname(socket) || null,
overseerEnabled: typeof data.overseerEnabled === 'boolean' ? data.overseerEnabled : true,
};
}