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
@@ -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,
};
}