mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
103 lines
4.7 KiB
JavaScript
103 lines
4.7 KiB
JavaScript
// Fleet Report Socket Gateway
|
|
// Purpose: Exposes read-only, visibility-filtered fleet history to browser clients.
|
|
// Scope: Owns query validation and existing private/lockdown access boundaries; it performs no collection or analysis.
|
|
const io = require('../../globals/io');
|
|
const crypto = require('crypto');
|
|
const { isAdmin, isLockdownAdmin } = require('../roleService');
|
|
|
|
const MAX_RANGE_MS = 366 * 24 * 60 * 60 * 1000;
|
|
|
|
function normalizeRange(payload = {}) {
|
|
const now = Date.now();
|
|
const until = Number.isFinite(Number(payload.until)) ? Number(payload.until) : now;
|
|
const requestedSince = Number.isFinite(Number(payload.since))
|
|
? Number(payload.since)
|
|
: until - 24 * 60 * 60 * 1000;
|
|
const since = Math.max(0, Math.max(requestedSince, until - MAX_RANGE_MS));
|
|
return { since, until: Math.max(since + 1, until) };
|
|
}
|
|
|
|
function registerSocketGateway({ roverManager, getRuntime, logger }) {
|
|
io.on('connection', (socket) => {
|
|
socket.on('fleetReports:get', (payload = {}, cb = () => {}) => {
|
|
try {
|
|
const runtime = getRuntime();
|
|
if (!runtime) throw new Error('Fleet reports are disabled');
|
|
const { reportBuilder } = runtime;
|
|
const { since, until } = normalizeRange(payload);
|
|
// getRosterForSocket is the canonical live private-rover visibility
|
|
// resolver. Historical queries use precisely those currently visible
|
|
// rover IDs so fleet totals cannot indirectly disclose a private rover.
|
|
const visibleRoverIds = roverManager.getRosterForSocket(socket).map((rover) => String(rover.id));
|
|
const requestedIds = Array.isArray(payload.roverIds)
|
|
? payload.roverIds.map(String).filter((id) => visibleRoverIds.includes(id))
|
|
: visibleRoverIds;
|
|
const report = reportBuilder.build({
|
|
since,
|
|
until,
|
|
roverIds: requestedIds,
|
|
includeEvents: payload.includeEvents !== false,
|
|
eventLimit: payload.eventLimit,
|
|
});
|
|
// Lockdown-only events are deliberately removed after query assembly.
|
|
// They are global rather than rover-scoped, so rover filtering alone is
|
|
// insufficient to preserve the pre-existing lockdown privacy boundary.
|
|
if (!isLockdownAdmin(socket)) {
|
|
report.events = report.events.filter((event) => event.visibility !== 'lockdown');
|
|
report.totals.eventCountReturned = report.events.length;
|
|
}
|
|
if (payload.compact === true) {
|
|
// The Activities card now consumes the same all-rovers metric rows
|
|
// as fullscreen. The builder no longer attaches minute/session
|
|
// evidence by default, so compacting only removes archival metadata.
|
|
report.events = [];
|
|
report.dailyReportHistory = [];
|
|
}
|
|
cb({ ok: true, report });
|
|
} catch (err) {
|
|
logger.warn('Fleet report query failed', { socketId: socket.id, error: err.message });
|
|
cb({ error: 'Fleet report query failed' });
|
|
}
|
|
});
|
|
|
|
socket.on('fleetReports:replaceBattery', (payload = {}, cb = () => {}) => {
|
|
try {
|
|
const runtime = getRuntime();
|
|
if (!runtime) throw new Error('Fleet reports are disabled');
|
|
const { storage, collector } = runtime;
|
|
if (!isAdmin(socket)) throw new Error('Admin access required');
|
|
const roverId = String(payload.roverId || '').trim();
|
|
if (!roverId || !roverManager.rovers.has(roverId)) throw new Error('Known online rover required');
|
|
const ratedCapacityMah = Number(payload.ratedCapacityMah);
|
|
if (!Number.isFinite(ratedCapacityMah) || ratedCapacityMah <= 0 || ratedCapacityMah > 65535) {
|
|
throw new Error('Rated capacity must be between 1 and 65535 mAh');
|
|
}
|
|
const installedAt = Number.isFinite(Number(payload.installedAt)) ? Number(payload.installedAt) : Date.now();
|
|
const entry = storage.replaceBattery({
|
|
roverId,
|
|
batteryKey: `battery:${roverId}:${installedAt}:${crypto.randomUUID().slice(0, 8)}`,
|
|
chemistry: String(payload.chemistry || '').trim() || null,
|
|
ratedCapacityMah: Math.round(ratedCapacityMah),
|
|
installedAt,
|
|
notes: String(payload.notes || '').trim() || null,
|
|
});
|
|
if (!entry) throw new Error('Battery registry write failed');
|
|
collector.refreshBatteryIdentity(roverId);
|
|
collector.collectEvent({
|
|
source: 'fleetReportService',
|
|
type: 'battery.replaced',
|
|
payload: { roverId, battery: entry },
|
|
});
|
|
cb({ ok: true, battery: entry });
|
|
} catch (err) {
|
|
logger.warn('Fleet battery replacement rejected', { socketId: socket.id, error: err.message });
|
|
cb({ error: err.message });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
registerSocketGateway,
|
|
};
|