Replace the dated editorial fallback positions baked into the registry
with a one-shot seed file + persistent observation cache. The user's
runtime cache now reflects what THIS install has actually observed,
not what USNI published on March 9, 2026. A year from now, the cache
holds a year of observations and the seed is irrelevant.
== #244: dated editorial coordinates out of the registry ==
CARRIER_REGISTRY no longer carries fallback_lat/lng/heading/desc.
Those fields are deleted. The registry is now identity + homeport
only.
New file: backend/data/carrier_seed.json
- Read-only, shipped with every release.
- Used ONCE on first-ever startup to bootstrap carrier_cache.json.
- Each entry stamped with position_confidence="seed" and the actual
as-of date (2026-03-09), NOT now().
== #245: approximate confidence for headline-derived positions ==
_parse_carrier_positions_from_news() now stamps every GDELT-derived
entry with position_confidence="approximate" so the UI knows the
coordinate is a region-centroid match, not a precise observation.
After the freshness window the label rolls over to
"stale_approximate" so old-and-imprecise is distinguishable from
recent-and-imprecise.
The article's actual seendate is used as position_source_at instead
of now(), so the "last reported X days ago" badge is honest.
== #246: freshness is labelling, not eviction ==
The cache always preserves the last position the system observed,
forever. What changes is the position_confidence label:
- within configurable window (default 14d, env-overridable via
SHADOWBROKER_CARRIER_FRESHNESS_DAYS) -> "recent"
- older -> "stale"
- seed-bootstrap entries that were never refreshed -> "seed"
- homeport defaults (carrier added post-install) -> "homeport_default"
- headline-derived (any age, fresh) -> "approximate"
- headline-derived (older than window) -> "stale_approximate"
The position itself never reverts to the seed or the registry. The
user always sees the last position the system observed. Per the
user's explicit guidance: "from there have it be the last position
the user has logged the carriers that way a year from now it doesnt
revert to where the ships are today".
== Other improvements ==
- CACHE_FILE moved to backend/data/carrier_cache.json so it lives in
the volume-mounted dir under Docker compose. Previously it was at
/app/carrier_cache.json which got wiped on every container restart
(pre-existing bug).
- Atomic cache write (temp + os.replace) so a crash mid-write does
not leave a truncated cache file.
== Public API shape ==
Every carrier object the API emits now includes:
- position_confidence: seed | recent | stale | approximate |
stale_approximate | homeport_default
- position_source_at: ISO timestamp of when the underlying source
was observed (NOT now())
- is_fallback: convenience boolean for the UI; true when the
confidence is seed/stale/stale_approximate/
homeport_default
Existing fields (estimated, source, source_url, last_osint_update,
name, type, lat, lng, country, desc, wiki) are preserved exactly so
the current ShipPopup frontend renders unchanged. last_osint_update
now reflects position_source_at instead of now(), which is what the
existing "last reported MM/DD" badge always meant to show.
Tests: backend/tests/test_carrier_tracker_quality.py — 17 tests
covering seed bootstrap, subsequent-startup ignoring seed, no-seed/
no-cache homeport fallback, registry no longer has fallback fields,
freshness window labelling + env override, "year-old cache entry keeps
its position, only the label flips" regression, approximate
confidence for headline matches, GDELT seendate ISO parser, public
response shape backward compat.
Credit: tg12 (external security audit, three P1/P2 issues).