Skip to content

2.0.2

Choose a tag to compare

@MattFalahe MattFalahe released this 30 May 22:45
· 3 commits to main since this release

Structure Manager v2.0.2

Patch release on top of v2.0.1. Three independent bug-fix tracks that
surfaced once v2.0.1 was running in production: a SeAT corp-assets refresh
race causing false Metenox CRITICAL alerts, a metadata-encoding regression
that started breaking the structure detail page once the related write-side
fix landed, and a Tier 1 fuel-event classification path that was
over-flagging legitimate consumption as fuel theft with wide forensics
suspect-list fanout. Plus a Fuel Economics admin scope selector and a new
diagnostic surface that makes the underlying SeAT data-quality signal
visible.

One forward-only cleanup migration runs automatically on container restart.
No schema changes, no settings changes, no breaking changes to plugin
contracts. Upgrades cleanly from v2.0.1.

Bug Fixes

SeAT corp-assets refresh race guards. SeAT polls corporation_assets
via a non-atomic DELETE-then-INSERT per corporation. SM's fuel-tracking
code reads that table live every 10 minutes (notifications) and hourly
(snapshots), and polls landing inside the refresh window were reading 0
for one fuel type while the other read correctly. For Metenox this caused
false CRITICAL embeds saying "Fuel Blocks [LIMITING] 0 blocks (0h)" even
when the structure had 100+ days of fuel left. Six sites now apply a
race-detection guard that skips the snapshot or notification when an
asymmetric 0 or a contradictory cs.fuel_expires is detected: the
Metenox and Upwell paths in NotifyUpwellLowFuel::getStructureFuelData,
NotifyUpwellLowFuel::processCynoReagents (Liquid Ozone and Strontium
Clathrate), the FuelAlertController Metenox page section, and the
TrackFuelConsumption::trackMetenoxFuel and trackStructureFuel
snapshot writers. Suspected races now land as warnings in laravel.log
instead of false embeds in Discord.

Metadata double-encoding on snapshot writes. The Metenox and Upwell
snapshot writers were passing json_encode($metadata) into the
structure_fuel_history.metadata column while the model casts the
column to array. The cast then encoded the already-encoded string a
second time, leaving the column with a JSON string wrapping a JSON
object. JSON_EXTRACT returned NULL in MariaDB and Eloquent's array
cast returned a string on read, eventually surfacing as a TypeError
on the structure detail page. Now passes the array straight through.
Forward-only cleanup migration 2026_05_01_000007 unwraps existing
double-encoded rows in structure_fuel_history and
starbase_fuel_history via JSON_UNQUOTE. Idempotent: re-runs are
no-ops on already-clean rows.

Defensive metadata decode at read sites. Fixing the write side
exposed a hidden bug at four read sites that did json_decode on what
was now an array. Patched with the is_array() ? as-is : decode()
ternary pattern other sites in the codebase already used.

Tier 1 fuel-event classification false-positive fixes. The
fuel-theft detection path was over-flagging legitimate consumption as
withdrawal_bay, with Tier 2 forensics fanning out wide suspect lists
on top. A 2026-05-30 production audit on a single Azbel surfaced 25
false withdrawal_bay rows over 7 days, every one with gap_minutes
118-120 and reserves_delta = 0. Root cause was a Carbon::diffInHours
integer-floor: a 1h 59m gap reads as 1 whole hour, so
expected_consumption is computed against half the actual elapsed time
and the classifier sees a 2x over-burn. Three surgical gates now stack
on the withdrawal_bay promotion path:

  • Minute-precision elapsed-time math. Four sites that used
    diffInHours for math (not for >= N integer threshold checks)
    switched to diffInMinutes / 60.0.
  • Reserves-delta sanity check. The classifier now requires
    reserves_delta < 0 before promoting to withdrawal_bay. Real theft
    moves fuel somewhere; a quiet reserves column with a bay over-burn is
    service-spike-shaped, not theft-shaped. Downgrades to
    consumption_anomaly instead: still visible on the panel, no
    forensics dispatch.
  • Minimum-magnitude gate. New THEFT_MAGNITUDE_HOURS = 12
    constant. Promotes to withdrawal_bay only if unexplained_delta is
    at least 12 hours of expected consumption. Real fuel theft moves
    meaningful quantities; the operational cost makes anything smaller
    not worth the risk. Auto-scales by the structure's burn rate:
    Astrahus needs at least 144 unexplained blocks, Azbel 324, Keepstar
    1440.

Walking the 7-day production audit through all three gates: zero false
positives survive. Genuine thefts (large unexplained drop plus reserves
drop) still classify as withdrawal_bay and dispatch forensics
unchanged.

Critical Alerts page Metenox layout. When a Metenox's assets reads
are mid-refresh, the page now falls back to the existing "AWAITING FUEL
DATA" placeholder and cs.fuel_expires time display instead of
rendering misleading 0s and overriding the time. Pure UX guard.

Enhancements

Fuel Economics admin corp-scope selector. New header dropdown on the
Fuel Economics page, gated to admins, toggles between "My Corporations"
(the new default) and "All Corporations". Non-admins always see only
their own corps and do not see the dropdown. Auto-submits on selection.
The Force refresh link preserves the active scope in the URL.

Snapshot poll coverage panel on Diagnostic Data Integrity. New
group surfaces how many fuel-history snapshots each tracked structure
or active POS has accumulated in the past 24h vs the expected hourly
(Upwell) or 10-minute (POS) cadence. Pairs with the v2.0.2 race guards:
gaps that come from the guards skipping a poll during SeAT's
asset-refresh race are visible here so operators can tell rare healthy
noise from sustained upstream problems (queue worker lag, ESI scope
gaps). Lazy-loads with the existing 300s Data Integrity cache pattern.

Upgrade

The one forward-only migration
(2026_05_01_000007_unwrap_double_encoded_fuel_history_metadata) runs
automatically on container restart, unwraps existing double-encoded
rows, and logs the affected count. Idempotent.

ESI User-Agent bumps to SeAT-StructureManager/2.0.2.

Breaking Changes

None. Upgrades from v2.0.1 cleanly preserve all settings, webhooks, fuel
history, POS data, structure presence tracking, and notification dedup
state.