mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-28 10:01:31 +02:00
feat(flights): cumulative fuel burned + CO2 emitted per flight
Pre-fix the emissions tooltip only showed the per-hour *rate* — what most users actually want is the cumulative *amount* burned. This adds running totals computed by multiplying the model-based rate by the elapsed observation time since we first saw the airframe. New module ``flight_observations.py``: * Tracks first_seen_at + last_seen_at per icao24 hex. * Re-opens a fresh session when an aircraft is unseen for > 15 min (treated as a new flight — landed and took off, or transited a dead zone). Prevents the cumulative counter from resetting mid-flight if the trail-rendering cache prunes the trail. * Clamps elapsed time to 24h max so clock skew can't produce comically large numbers. * Pruned every 5 min via a new scheduler job (mirrors ais_prune cadence). flights.py + military.py emission enrichment now also attaches: * observed_seconds — how long we've been tracking this airframe. * fuel_gallons_burned — rate * elapsed_h. * co2_kg_emitted — rate * elapsed_h. The existing per-hour rate fields stay in the dict for backward compat and are shown as small secondary context in the tooltip. Frontend EmissionsEstimateBlock (NewsFeed.tsx) now prominently shows the cumulative totals with the rate as smaller context underneath plus "Observed in flight for Xh Ym". When observed_seconds is 0 (first refresh) it renders "Just observed · totals will appear on next refresh" instead of a misleading "0 gal". 12 backend tests cover record/accumulate/reset, the 24h clamp, prune, case-insensitive key normalization, and end-to-end emission integration in _classify_and_publish. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -249,34 +249,70 @@ const VESSEL_TYPE_WIKI: Record<string, string> = {
|
||||
|
||||
type FlightTrailPoint = { lat?: number; lng?: number; alt?: number; ts?: number } | number[];
|
||||
|
||||
function formatObservedDuration(seconds: number): string {
|
||||
// Compact "1h 14m" / "23m" / "45s" — matches the density of the rest
|
||||
// of the flight tooltip. < 60s is shown as "<1m" so the user knows
|
||||
// we've JUST started observing this hex (cumulative will still be 0).
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return '<1m';
|
||||
if (seconds < 60) return '<1m';
|
||||
const totalMinutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
function EmissionsEstimateBlock({ flight }: { flight: any }) {
|
||||
const emissions = flight?.emissions;
|
||||
const context = emissions ? 'Model-based cruise estimate' : null;
|
||||
// Cumulative fuel/CO2 since the backend first saw this hex this
|
||||
// flight session. Prefer these big numbers — the user explicitly
|
||||
// wanted "the actual fuel that has been burned", not the rate.
|
||||
// Rates are still shown below as smaller context.
|
||||
const observedSec = Number(emissions?.observed_seconds ?? 0);
|
||||
const fuelBurned = Number(emissions?.fuel_gallons_burned ?? 0);
|
||||
const co2Emitted = Number(emissions?.co2_kg_emitted ?? 0);
|
||||
const haveCumulative = emissions && observedSec > 0;
|
||||
|
||||
return (
|
||||
<div className="border-b border-[var(--border-primary)] pb-2">
|
||||
<span className="text-[var(--text-muted)] text-[10px] block mb-1.5">EMISSIONS ESTIMATE</span>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1 bg-[var(--bg-primary)]/50 border border-[var(--border-primary)] px-2 py-1.5">
|
||||
<div className="text-[11px] text-[var(--text-muted)] tracking-widest">FUEL RATE</div>
|
||||
<div className="text-xs font-bold text-orange-400">
|
||||
{emissions ? (
|
||||
<>{emissions.fuel_gph} <span className="text-[11px] text-[var(--text-muted)] font-normal">GPH</span></>
|
||||
<div className="text-[11px] text-[var(--text-muted)] tracking-widest">FUEL BURNED</div>
|
||||
<div className="text-sm font-bold text-orange-400">
|
||||
{haveCumulative ? (
|
||||
<>{fuelBurned.toLocaleString(undefined, { maximumFractionDigits: 1 })} <span className="text-[11px] text-[var(--text-muted)] font-normal">gal</span></>
|
||||
) : emissions ? (
|
||||
<span className="text-[var(--text-muted)] font-normal text-xs">—</span>
|
||||
) : 'UNKNOWN'}
|
||||
</div>
|
||||
{emissions && (
|
||||
<div className="text-[10px] text-[var(--text-muted)] mt-0.5">
|
||||
@ {emissions.fuel_gph} gph
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 bg-[var(--bg-primary)]/50 border border-[var(--border-primary)] px-2 py-1.5">
|
||||
<div className="text-[11px] text-[var(--text-muted)] tracking-widest">CO2 RATE</div>
|
||||
<div className="text-xs font-bold text-red-400">
|
||||
{emissions ? (
|
||||
<>{emissions.co2_kg_per_hour.toLocaleString()} <span className="text-[11px] text-[var(--text-muted)] font-normal">KG/HR</span></>
|
||||
<div className="text-[11px] text-[var(--text-muted)] tracking-widest">CO2 EMITTED</div>
|
||||
<div className="text-sm font-bold text-red-400">
|
||||
{haveCumulative ? (
|
||||
<>{co2Emitted.toLocaleString(undefined, { maximumFractionDigits: 1 })} <span className="text-[11px] text-[var(--text-muted)] font-normal">kg</span></>
|
||||
) : emissions ? (
|
||||
<span className="text-[var(--text-muted)] font-normal text-xs">—</span>
|
||||
) : 'UNKNOWN'}
|
||||
</div>
|
||||
{emissions && (
|
||||
<div className="text-[10px] text-[var(--text-muted)] mt-0.5">
|
||||
@ {emissions.co2_kg_per_hour.toLocaleString()} kg/hr
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{context && (
|
||||
{emissions && (
|
||||
<div className="mt-1.5 text-[10px] text-[var(--text-muted)] leading-relaxed">
|
||||
{context}
|
||||
{haveCumulative
|
||||
? `Observed in flight for ${formatObservedDuration(observedSec)} · model-based cruise estimate`
|
||||
: 'Just observed · totals will appear on next refresh'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user