mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
slopreporting
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// Fleet Reports Card
|
||||
// Purpose: Shows a dense current fleet summary at the bottom of the Activities tab.
|
||||
// Scope: Self-gates from session.features and links to the dedicated read-only fullscreen report.
|
||||
// Purpose: Shows every rover's key battery and efficiency metrics at the bottom of Activities.
|
||||
// Scope: Self-gates through session.features and links to the all-rovers fullscreen report.
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
@@ -8,34 +8,19 @@ import { isFeatureEnabled } from '../../lib/features.js';
|
||||
import useFleetReport from '../../hooks/useFleetReport.js';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
function formatMah(value) {
|
||||
return Number.isFinite(Number(value)) ? `${Math.round(Number(value))} mAh` : '--';
|
||||
function value(number, digits = 1) {
|
||||
return Number.isFinite(Number(number))
|
||||
? Number(number).toLocaleString(undefined, { maximumFractionDigits: digits })
|
||||
: '--';
|
||||
}
|
||||
|
||||
function formatDuration(ms) {
|
||||
const minutes = Math.round((Number(ms) || 0) / 60000);
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
}
|
||||
|
||||
function formatDistance(mm) {
|
||||
const value = Number(mm) || 0;
|
||||
return value >= 1000000 ? `${(value / 1000000).toFixed(2)} km` : `${(value / 1000).toFixed(1)} m`;
|
||||
}
|
||||
|
||||
function Metric({ label, value }) {
|
||||
return (
|
||||
<div className="surface flex min-w-0 items-center justify-between gap-1 px-1 py-0.5 text-sm">
|
||||
<span className="text-slate-400">{label}</span>
|
||||
<span className="truncate text-right font-medium text-slate-100">{value}</span>
|
||||
</div>
|
||||
);
|
||||
function distance(millimeters) {
|
||||
return Number(millimeters) >= 1e6
|
||||
? `${value(Number(millimeters) / 1e6, 2)} km`
|
||||
: `${value(Number(millimeters) / 1000, 1)} m`;
|
||||
}
|
||||
|
||||
function EnabledFleetReportsCard() {
|
||||
// State gives each request a stable range endpoint while allowing the user
|
||||
// to advance the 24-hour window explicitly without calling time APIs during
|
||||
// render.
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const { report, loading, error } = useFleetReport({
|
||||
since: now - 24 * 60 * 60 * 1000,
|
||||
@@ -43,60 +28,30 @@ function EnabledFleetReportsCard() {
|
||||
compact: true,
|
||||
includeEvents: false,
|
||||
});
|
||||
|
||||
const actions = (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button type="button" className="button-dark px-1 py-0.25 text-[0.75rem]" onClick={() => setNow(Date.now())}>Refresh</button>
|
||||
<Link className="button-dark px-1 py-0.25 text-[0.75rem]" to="/reports">Open full report</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<CardFrame title="Fleet report" meta="Last 24 hours" actions={actions} bodyClassName="space-y-0.5 p-0.5 text-sm">
|
||||
{loading && !report ? <p className="text-slate-400">Loading fleet report…</p> : null}
|
||||
<CardFrame
|
||||
title="Fleet battery and efficiency"
|
||||
meta="Last 24 hours"
|
||||
actions={<div className="flex gap-0.5"><button type="button" className="button-dark px-1 py-0.25 text-[0.75rem]" onClick={() => setNow(Date.now())}>Refresh</button><Link className="button-dark px-1 py-0.25 text-[0.75rem]" to="/reports">Open full report</Link></div>}
|
||||
bodyClassName="space-y-0.5 p-0.5 text-sm"
|
||||
>
|
||||
{loading && !report ? <p className="text-slate-400">Loading fleet metrics…</p> : null}
|
||||
{error ? <p className="text-red-300">{error}</p> : null}
|
||||
{report ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-0.5 md:grid-cols-4">
|
||||
<Metric label="Online" value={`${report.totals.onlineRoverCount}/${report.totals.roverCount}`} />
|
||||
<Metric label="Coverage" value={formatDuration(report.totals.coverageMs)} />
|
||||
<Metric label="Discharged" value={formatMah(report.totals.dischargedMah)} />
|
||||
<Metric label="Charged" value={formatMah(report.totals.chargedMah)} />
|
||||
<Metric label="Sensor samples" value={report.totals.sampleCount.toLocaleString()} />
|
||||
<Metric label="Telemetry gaps" value={report.totals.telemetryGapCount.toLocaleString()} />
|
||||
<Metric label="Distance" value={formatDistance(report.totals.distanceMm)} />
|
||||
<Metric label="Overcurrent episodes" value={report.totals.overcurrentEpisodeCount.toLocaleString()} />
|
||||
<Metric label="Warnings" value={report.totals.warningFindingCount.toLocaleString()} />
|
||||
<Metric label="Critical" value={report.totals.criticalFindingCount.toLocaleString()} />
|
||||
</div>
|
||||
{report.findings.length ? (
|
||||
<div className="surface space-y-0.5 px-1 py-0.5">
|
||||
<p className="text-xs font-semibold text-slate-200">Needs attention</p>
|
||||
{report.findings.slice(0, 5).map((finding) => (
|
||||
<div key={finding.key} className="flex items-start justify-between gap-1 text-xs">
|
||||
<span className="text-slate-200">{finding.roverId ? `${finding.roverId}: ` : ''}{finding.title}</span>
|
||||
<span className={finding.severity === 'critical' ? 'text-red-300' : finding.severity === 'warning' ? 'text-amber-300' : 'text-slate-400'}>
|
||||
{finding.severity}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="surface px-1 py-0.5 text-xs text-emerald-300">No report findings in this range.</p>}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="text-slate-400"><tr><th>Rover</th><th>State</th><th>Samples</th><th>Used</th><th>Temp max</th><th>Gaps</th></tr></thead>
|
||||
<tbody>
|
||||
{report.rovers.map((rover) => (
|
||||
<tr key={rover.roverId} className="border-t border-neutral-700/70 text-slate-200">
|
||||
<td>{rover.name}</td><td>{rover.online ? 'online' : 'offline'}</td>
|
||||
<td>{rover.sampleCount.toLocaleString()}</td><td>{formatMah(rover.dischargedMah)}</td>
|
||||
<td>{rover.maximumTemperatureC == null ? '--' : `${rover.maximumTemperatureC}°C`}</td><td>{rover.gapCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full whitespace-nowrap text-left text-xs">
|
||||
<thead className="text-slate-400"><tr><th>Rover</th><th>State</th><th>Health</th><th>Confidence</th><th>Wh/km</th><th>Distance</th><th>Used</th><th>Temperature</th></tr></thead>
|
||||
<tbody>{report.rovers.map((rover) => (
|
||||
<tr key={rover.roverId} className="border-t border-neutral-700/70 text-slate-200">
|
||||
<td>{rover.name}</td><td>{rover.online ? 'online' : 'offline'}</td>
|
||||
<td>{rover.batteryHealth.capacityRetentionPercent == null ? '--' : `${value(rover.batteryHealth.capacityRetentionPercent)}%`}</td>
|
||||
<td>{rover.batteryHealth.confidence}</td><td>{value(rover.overallWhPerKm)}</td>
|
||||
<td>{distance(rover.distanceMm)}</td><td>{value(rover.dischargedWh, 2)} Wh</td>
|
||||
<td>{value(rover.maximumTemperatureC)} °C</td>
|
||||
</tr>
|
||||
))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : null}
|
||||
</CardFrame>
|
||||
);
|
||||
@@ -104,9 +59,10 @@ function EnabledFleetReportsCard() {
|
||||
|
||||
export default function FleetReportsCard() {
|
||||
const enabled = useSessionSelector((state) => isFeatureEnabled(state, 'fleetReports'));
|
||||
// The outer component owns the optional feature gate, while the enabled
|
||||
// child owns data hooks. This avoids opening report socket requests at all
|
||||
// when the server has disabled the feature and still lets layout stacks stay
|
||||
// completely unaware of feature branching.
|
||||
/*
|
||||
Keeping the hook inside the enabled child ensures a disabled optional
|
||||
feature creates no socket traffic and leaves the Activities layout unaware
|
||||
of reporting internals.
|
||||
*/
|
||||
return enabled ? <EnabledFleetReportsCard /> : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user