kick everyone and set update

This commit is contained in:
legop3
2026-09-15 20:19:25 -04:00
parent 609eb6c35e
commit 8fb9278109
2 changed files with 29 additions and 0 deletions
+1
View File
@@ -699,6 +699,7 @@ Implemented on 2026-09-15:
- Added fixed status, update-check, restart, and update operations. Update checks pull the configured moving image and compare Docker image IDs. Updates retain the previous image ID, recreate the application with its existing Compose host contract, wait for the image health check, and restore the previous image when replacement health fails.
- Persisted the current operation and result in the private lifecycle Unix-socket volume. This survives ordinary application replacement and browser reconnection without mounting the root lifecycle controller into the application's `/data` volume, leaving all application runtime paths owned by the non-root server.
- Connected the existing lockdown-administrator password confirmation and audit history to the lifecycle operations. The Administration overview polls persisted progress, reports update and rollback results, and keeps the legacy process-level restart only when no controller socket exists.
- Accepted self-updates, administrator restarts, and backup-restore restarts share a helper that sets the persistent admin reason to "server is restarting" and removes every current rover driver with the same notice. This does not change server mode or automatically clear the reason after startup.
- Defined the deployment image once through a Compose YAML anchor. Both services and the controller target reuse that exact value, so production stays on `ghcr.io/legop3/multiroombarover:latest` and development requires changing only the single visible selector line to a branch tag.
## 15. GHCR publishing automation
@@ -6,6 +6,9 @@ const logger = require('../../globals/logger').child('serverControlService');
const { getConfigurationDatabase } = require('../../configuration');
const { requireLockdownAdministrator, requireRecentPassword } = require('../adminConfigurationService');
const lifecycleClient = require('./lifecycleClient');
const { setAdminReason } = require('../adminReasonService');
const roverManager = require('../roverManager');
const assignmentService = require('../assignmentService');
const database = getConfigurationDatabase();
let restartPending = false;
@@ -14,6 +17,25 @@ function actorFor(socket) {
return socket?.data?.user?.username || socket.id;
}
function notifyDriversOfRestart(actor) {
// All requested restart paths share this persistent explanation. Updating
// the reason does not change server mode or arrange to clear it at startup.
const message = 'Server is restarting...';
setAdminReason(message, { by: actor });
for (const [roverId, rover] of roverManager.rovers) {
// Releasing control mutates the set, so snapshot the current drivers and
// send each the existing removal notice before their assignment changes.
for (const socketId of [...rover.drivers]) {
assignmentService.forceReleaseWithNotice(roverId, socketId, {
title: message,
message,
reasonCode: 'application-restart',
actor,
});
}
}
}
function scheduleApplicationRestart() {
if (restartPending) throw new Error('Application restart already pending.');
restartPending = true;
@@ -38,6 +60,7 @@ function scheduleApplicationRestart() {
function requestApplicationRestart({ actor, reason = 'administrator-requested' }) {
if (restartPending) throw new Error('Application restart already pending.');
database.recordAuditEvent(actor, 'application.restart-requested', { reason });
notifyDriversOfRestart(actor);
scheduleApplicationRestart();
logger.warn('Application restart requested', { actor, reason });
// Restore and ordinary admin restarts share this one browser contract, so
@@ -70,6 +93,8 @@ io.on('connection', (socket) => {
.then(() => requireRecentPassword(socket))
.then(() => lifecycleClient.updateApplication())
.then((lifecycle) => {
// Wait for controller acceptance so rejected updates do not kick users.
notifyDriversOfRestart(actorFor(socket));
database.recordAuditEvent(actorFor(socket), 'application.update-requested', {});
io.emit('server:restarting', { reason: 'application-update' });
cb({ success: true, lifecycle });
@@ -85,6 +110,9 @@ io.on('connection', (socket) => {
try {
const lifecycle = await lifecycleClient.restartApplication();
restartPending = true;
// Container restarts bypass the process-signal path, but need the same
// persistent reason and removal notices once the controller accepts.
notifyDriversOfRestart(actor);
database.recordAuditEvent(actor, 'application.restart-requested', { reason: 'administrator-requested' });
logger.warn('Application container restart requested', { actor });
io.emit('server:restarting', { reason: 'administrator-requested' });