mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
153 lines
5.3 KiB
JavaScript
153 lines
5.3 KiB
JavaScript
// Fleet Report Service
|
|
// Purpose: Owns the replaceable collection/report runtime and its stable browser API.
|
|
// Scope: Applies the complete fleetReports section without restarting the Node process.
|
|
const { loadConfig, registerConfigurationHandler } = require('../../configuration');
|
|
const logger = require('../../globals/logger').child('fleetReportService');
|
|
const { subscribeAll } = require('../eventBus');
|
|
const roverManager = require('../roverManager');
|
|
const { commandEvents } = require('../commandService');
|
|
const { odometerEvents } = require('../odometerService');
|
|
const { createStorage } = require('./storage');
|
|
const { createCollector } = require('./collector');
|
|
const { createReportBuilder } = require('./reportBuilder');
|
|
const { registerSocketGateway } = require('./socketGateway');
|
|
|
|
let runtime = null;
|
|
let storage = null;
|
|
|
|
function retentionDays(value, fallback) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
}
|
|
|
|
function stopRuntime() {
|
|
if (!runtime) return;
|
|
runtime.unsubscribeEvents();
|
|
if (runtime.batteryEnabled) roverManager.managerEvents.off('sensor', runtime.collector.collectSensor);
|
|
commandEvents.off('observation', runtime.collector.collectCommand);
|
|
odometerEvents.off('update', runtime.collector.collectOdometer);
|
|
runtime.managerEventHandlers.forEach((handler, kind) => roverManager.managerEvents.off(kind, handler));
|
|
clearInterval(runtime.flushTimer);
|
|
clearInterval(runtime.retentionTimer);
|
|
runtime.collector.flushMinutes();
|
|
runtime = null;
|
|
}
|
|
|
|
function startRuntime(config = {}) {
|
|
stopRuntime();
|
|
if (!config.enabled) {
|
|
logger.info('Fleet reporting disabled by config');
|
|
return;
|
|
}
|
|
|
|
const batteryConfig = config.battery || {};
|
|
const retentionConfig = config.retention || {};
|
|
const maximumIntegrationGapMs = Math.max(
|
|
250,
|
|
(Number(batteryConfig.maximumIntegrationGapSeconds) || 5) * 1000,
|
|
);
|
|
const minimumCapacityTestDepthPercent = Math.max(
|
|
10,
|
|
Math.min(100, Number(batteryConfig.minimumCapacityTestDepthPercent) || 60),
|
|
);
|
|
const batteryEnabled = Boolean(batteryConfig.enabled);
|
|
|
|
// Keep one SQLite connection for the process lifetime. Configuration reloads
|
|
// replace collectors and timers, not the durable database they share.
|
|
if (!storage) {
|
|
storage = createStorage({ logger });
|
|
storage.open();
|
|
}
|
|
const collector = createCollector({
|
|
storage,
|
|
logger,
|
|
maximumIntegrationGapMs,
|
|
minimumCapacityTestDepthPercent,
|
|
});
|
|
const reportBuilder = createReportBuilder({ storage, collector, roverManager });
|
|
const unsubscribeEvents = subscribeAll(collector.collectEvent);
|
|
if (batteryEnabled) roverManager.managerEvents.on('sensor', collector.collectSensor);
|
|
commandEvents.on('observation', collector.collectCommand);
|
|
odometerEvents.on('update', collector.collectOdometer);
|
|
const managerEventKinds = ['rover', 'hostStats', 'driver', 'switch', 'lock', 'private', 'privateSafety'];
|
|
const managerEventHandlers = new Map(managerEventKinds.map((kind) => {
|
|
const handler = (event) => collector.collectManagerEvent(kind, event);
|
|
roverManager.managerEvents.on(kind, handler);
|
|
return [kind, handler];
|
|
}));
|
|
|
|
const flushTimer = setInterval(() => collector.flushMinutes(), 30 * 1000);
|
|
flushTimer.unref?.();
|
|
function pruneNow() {
|
|
const now = Date.now();
|
|
const detailedDays = retentionDays(retentionConfig.detailedDays, 0);
|
|
const minuteDays = retentionDays(retentionConfig.minuteSamplesDays, 0);
|
|
storage.prune({
|
|
detailedBefore: detailedDays === 0 ? 0 : now - detailedDays * 86400000,
|
|
minuteBefore: minuteDays === 0 ? 0 : now - minuteDays * 86400000,
|
|
});
|
|
}
|
|
pruneNow();
|
|
const retentionTimer = setInterval(pruneNow, 6 * 60 * 60 * 1000);
|
|
retentionTimer.unref?.();
|
|
|
|
runtime = {
|
|
batteryEnabled,
|
|
storage,
|
|
collector,
|
|
reportBuilder,
|
|
unsubscribeEvents,
|
|
managerEventHandlers,
|
|
flushTimer,
|
|
retentionTimer,
|
|
};
|
|
logger.info('Fleet reporting enabled', {
|
|
databaseAvailable: storage.getDiagnostics().available,
|
|
maximumIntegrationGapMs,
|
|
minimumCapacityTestDepthPercent,
|
|
batteryEnabled,
|
|
});
|
|
}
|
|
|
|
registerSocketGateway({ roverManager, getRuntime: () => runtime, logger });
|
|
startRuntime(loadConfig().fleetReports || {});
|
|
registerConfigurationHandler('fleetReports', startRuntime);
|
|
|
|
module.exports = {
|
|
get enabled() {
|
|
return Boolean(runtime);
|
|
},
|
|
getDailyReport({ since, until, roverIds } = {}) {
|
|
if (!runtime) return null;
|
|
const end = Number(until) || Date.now();
|
|
return runtime.reportBuilder.build({
|
|
since: Number(since) || end - 24 * 60 * 60 * 1000,
|
|
until: end,
|
|
roverIds: Array.isArray(roverIds) ? roverIds : undefined,
|
|
includeEvents: false,
|
|
});
|
|
},
|
|
get collector() {
|
|
return runtime?.collector || null;
|
|
},
|
|
get storage() {
|
|
return runtime?.storage || storage;
|
|
},
|
|
get reportBuilder() {
|
|
return runtime?.reportBuilder || null;
|
|
},
|
|
backupDatabase(destinationPath) {
|
|
/*
|
|
Backups include reporting history even when collection is currently
|
|
disabled. Lazily opening the existing store keeps this one operation
|
|
behind the report service's normal database ownership boundary.
|
|
*/
|
|
if (!storage) {
|
|
storage = createStorage({ logger });
|
|
storage.open();
|
|
}
|
|
return storage.backupDatabase(destinationPath);
|
|
},
|
|
stop: stopRuntime,
|
|
};
|