Skip to content

feat: open tracker to non-krill CPWF missions — whaling first (Kangei Maru, Hvalur 8/9) - #15

Merged
breaching merged 1 commit into
mainfrom
feat/whaling-mission-kangei-maru
Apr 28, 2026
Merged

feat: open tracker to non-krill CPWF missions — whaling first (Kangei Maru, Hvalur 8/9)#15
breaching merged 1 commit into
mainfrom
feat/whaling-mission-kangei-maru

Conversation

@breaching

Copy link
Copy Markdown
Owner

Context

The user asked: "is it possible to add other missions than Krill Wars, are there other sensitive zones on Earth to track?" This PR is the first step — opens the data model to multi-mission and onboards 3 commercial-whaling targets as a sibling campaign, demonstrating that the existing pipeline (zones, AIS gaps, severity, events) is generic and not krill-specific.

What changes

Vessel model (src/vessel_db.py + config/krill_fleet.json)

  • New mission field on Vessel: "krill" (default, existing behaviour) | "whaling" | "iuu_fishing"
  • Three new vessels added with mission="whaling":
    • Kangei Maru — Japanese factory whaler launched May 2024 in Shimonoseki by Kyodo Senpaku, replaces the retired Nisshin Maru. 113 m, fin-whale capable
    • Hvalur 8 / Hvalur 9 — Icelandic fin-whaling catchers operating June–September from Hvalfjörður
  • All three have IMO/MMSI marked PENDING — operator must populate from MarineTraffic / Equasis before deployment

Zones (config/ccamlr_areas.geojson)

Three new polygons tagged category="whale_sanctuary":

  • IWC Southern Ocean Sanctuary (1994, IWC Schedule 7(b)) — split E/W to avoid the 180° antimeridian
  • IWC Indian Ocean Sanctuary (1979, IWC Schedule 7(a)) — north of 55°S between 20°E and 130°E

The existing severity rule ais_gap_in_mpa (hard_critical=true) automatically fires CRITICAL when a tracked whaler goes dark inside one of these polygons. No new code path needed — this is the pipeline's existing zone-event detection working on a new dataset.

API (server/app.py)

  • /api/vessels now exposes mission per vessel (no schema break — it's an additive field)

Web (web/)

  • data/vessels.ts: adds the three whaling vessels with bilingual notes
  • app/[locale]/vessels/page.tsx: groups catalogue by mission (Krill / Whaling / IUU), each section with a coloured heading. Whaling cards get a danger-tinted border + explicit "Commercial whaling (IWC sanctuaries)" badge
  • lib/i18n.ts: adds mission labels in EN/FR; renames page title from "Antarctic krill fleet""Tracked vessels"

Why it's small on purpose

We deliberately did NOT abstract a "campaign" type yet. With one new mission in production, the duplication cost is zero and the abstraction shape is unclear. The right time to refactor config/loaders into per-campaign units is when the third mission lands (Galápagos Chinese fleet / West Africa super-trawlers / Vaquita), not now.

Verification

  • python -m pytest tests/ -q110 passed (added test_vessels_includes_whaling_mission)
  • python -m ruff checkAll checks passed
  • cd web && npx tsc --noEmit → exit 0
  • cd web && npx next build → 6 new SSG pages generated (/{en,fr}/vessels/{kangei_maru,hvalur_8,hvalur_9})

Operator's checklist before deployment

  • Populate Kangei Maru imo and mmsi from MarineTraffic / Equasis
  • Populate Hvalur 8 / Hvalur 9 imo and mmsi from Icelandic vessel registry
  • Verify IWC sanctuary polygon coordinates match the official IWC Schedule (current MVP is a simplified bounding-box approximation that errs on the side of over-inclusion)
  • Decide if a separate webhook channel is wanted for whaling alerts (currently they will share the same DISCORD_WEBHOOK_URL / SLACK_WEBHOOK_URL as krill alerts)

What's next (if this lands)

Two strong candidates for the next mission:

  1. Galápagos Chinese fleet — ~340 vessels at the EEZ edge, datasets public via GFW
  2. West African super-trawlers (Senegal/Mauritania/Guinea) — Chinese + Russian fleets in coastal EEZs

After mission #3, the abstraction shape will be obvious and we refactor.

Adds a `mission` field to the vessel model and onboards 3 commercial-whaling
targets as a sibling campaign. Demonstrates that the underlying pipeline
(zones, AIS gaps, severity, events) is generic and not krill-specific.

Backend
- src/vessel_db.py: new VesselMission literal ("krill" | "whaling" | "iuu_fishing"),
  added to Vessel model with default "krill" so existing entries are unchanged
- server/app.py: vessel summary now exposes the mission field
- config/krill_fleet.json: adds Kangei Maru (Japanese factory whaler, replaces
  Nisshin Maru, launched 2024), Hvalur 8 and Hvalur 9 (Icelandic fin-whaling
  catchers). All three have IMO/MMSI marked PENDING — operator must populate
  from MarineTraffic / Equasis before going live
- config/ccamlr_areas.geojson: adds three whale-sanctuary polygons (IWC
  Schedule 7) — Southern Ocean Sanctuary split E/W to avoid the antimeridian,
  plus the Indian Ocean Sanctuary. Tagged category="whale_sanctuary" so the
  existing severity rules (ais_gap_in_mpa) automatically fire CRITICAL when
  a whaler enters one and goes dark

Web
- web/data/vessels.ts: adds the three whaling vessels with bilingual notes
- web/app/[locale]/vessels/page.tsx: groups the catalogue by mission with a
  coloured heading per mission (krill = accent, whaling = danger). Whaling
  cards get a danger-tinted border + an explicit mission badge so the user
  sees at a glance these are not krill-fishery vessels
- web/lib/i18n.ts: adds mission labels in EN and FR; renames the page title
  from "Antarctic krill fleet" to "Tracked vessels" since the catalogue now
  spans multiple campaigns

Tests
- tests/test_api.py: locks in that /api/vessels exposes the mission field
  and includes the whaling mission with Kangei Maru in it (110 passed)

Verification
- python -m pytest tests/ -q → 110 passed
- python -m ruff check → All checks passed
- cd web && npx tsc --noEmit → exit 0
- cd web && npx next build → 6 new SSG pages generated for whaling vessels

Why it's small on purpose
- We deliberately did NOT abstract a "campaign" type yet. With one new mission
  in production, the duplication cost is zero and the abstraction shape is
  unclear. Wait until the second non-krill mission (Galápagos / West Africa
  Chinese fleet) before refactoring config/loaders into per-campaign units

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@breaching
breaching merged commit 51f0463 into main Apr 28, 2026
4 checks passed
@breaching
breaching deleted the feat/whaling-mission-kangei-maru branch April 28, 2026 08:53
breaching added a commit that referenced this pull request Apr 28, 2026
…-trawlers (#17)

Adds two new IUU-fishing campaigns alongside krill (mission=krill) and
whaling (mission=whaling). The detector pipeline (zones, AIS gaps,
severity, events) is fully reused — only data is added.

Galápagos campaign (mission=iuu_fishing)
- 3 representative Chinese squid jiggers / reefers in fleet config:
  Fu Yuan Yu Leng 999, Long Da 001, Hai Feng 718
- 2 zone polygons added to ccamlr_areas.geojson:
    GMR-GALAPAGOS — Galápagos Marine Reserve incl. Hermandad (2022 expansion)
    EEZ-GALAPAGOS-BUFFER — Ecuador 200-nm EEZ where the squid-jigger fleet
    historically congregates each Aug-Nov
- Headline signal: incursion into GMR (foreign-flag commercial fishing
  prohibited there) or AIS gap inside the buffer

West Africa campaign (mission=iuu_fishing)
- 3 representative DWF vessels in fleet config:
  Lurong Yuanyu 956 (CN), Lurong Yuanyu 988 (CN), Saltic Atlas (RU)
- 3 zone polygons added (simplified bounding boxes):
    EEZ-SENEGAL, EEZ-MAURITANIA, EEZ-GUINEA
- Headline signal: AIS gap inside any of these EEZs (vessel went dark
  while presumably trawling)

Web (`web/data/vessels.ts`)
- All 6 new vessels added to the static catalogue with bilingual notes
  describing the campaign context (Pingtan reefer 2017 Galápagos arrest;
  300+ vessel 2020 stand-off; EJF/Greenpeace West Africa documentation)
- They surface automatically in the existing /vessels mission-grouped UI
  (PR #15) — krill / whaling / IUU fishing sections

All vessels have IMO and MMSI marked PENDING — operator must populate
from GFW Carrier Vessel Portal / Equasis before deployment. The polygon
boundaries are simplified bounding boxes that err conservatively (over-
include slightly); operator should refine with official EEZ data
(e.g. https://www.marineregions.org/) before deployment.

Verification
- python -m pytest tests/ -q → 110 passed
- python -m ruff check → All checks passed
- cd web && npx tsc --noEmit → exit 0

What's deliberately NOT done
- No "campaign" abstraction yet. With 4 missions still hanging off
  krill_fleet.json + ccamlr_areas.geojson, the duplication cost remains
  near zero. Right time for the refactor is when the 5th mission lands.

Co-authored-by: breaching <devsca@protonmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
breaching added a commit that referenced this pull request Apr 28, 2026
…19)

INVESTIGATOR_PLAYBOOK.md
- New §4 "Multi-mission monitoring" — table of 4 missions, headline signals,
  tracked zones, per-mission webhook routing, smoke-test recipe via the
  /api/_dev/test-alert endpoint
- New §5 "Weekly operational rhythm" — describes the CI cron + production
  cron + Monday-morning Discord ping flow
- "What this tool is NOT" updated to clarify the live-alerting vs offline-
  encounter latency distinction, and to flag IUU vessel IMOs as PENDING
  alongside the existing OFAC/CM-10-09 stub callouts

README.md
- Subtitle pivoted from "krill fishing fleet" to "CPWF-relevant fleets —
  Antarctic krill, commercial whaling, and IUU fishing"
- New top bullet describing the 4 missions; added "Live alerting" + cron
  bullets so the README accurately advertises what shipped in PRs #15-18

.env.example
- Generic + per-mission webhook URLs documented
- WEB_BASE_URL / WEB_DEFAULT_LOCALE for deep links
- Production weekly-report runner vars (KRILL_DB_PATH,
  KRILL_REPORT_OUT_DIR, KRILL_REPORT_PUBLIC_URL,
  DISCORD_WEBHOOK_URL_REPORTS)
- KRILL_ALLOW_DEV_ENDPOINTS for the dev test-alert endpoint

Co-authored-by: breaching <devsca@protonmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
breaching pushed a commit that referenced this pull request May 5, 2026
This commit lands the work documented in PROD_READINESS.md and
docs/AUDIT_NOTES.md as deferred at the end of the 2026-04-29 audit
batch, plus the audit-P19 'Pavillon' i18n leak.

Audit bug #14 — FR translations for curated incidents
  * config/incidents.json: 4 entries gained title_fr/summary_fr
  * src/incidents.py + web/lib/krill-watch.ts: optional locale fields
  * web/components/vessels/IncidentList.tsx: prefers locale variant,
    drops the 'Source · EN' fallback badge when a translation exists

PROD #5 — backup status surfaced on /api/health
  * scripts/backup_duckdb.py writes data/.backup_status.json after a
    successful upload (timestamp, bucket, key, size, retention counts)
  * server/app.py /api/health surfaces a `backup` block with a derived
    `age_seconds` so the frontend can render 'backup ran X hours ago'
    without re-parsing the timestamp
  * tests/test_api.py: 2 new cases lock the contract (none + present)

PROD #10 — k6 load-test scaffold
  * scripts/loadtest.k6.js covers /api/health, /api/vessels,
    /api/positions/live, /api/zones, /api/incidents with three
    profiles (smoke / press-launch / sustained), p(95) thresholds

PROD #15 — Lighthouse CI scaffold
  * web/lighthouserc.js: soft-asserted budgets (perf 0.6 warn, a11y
    0.85 error, SEO 0.9 error) against the production-built bundle
  * .github/workflows/ci.yml: new lighthouse job, continue-on-error

Audit bug #19 — Pavillon i18n leak
  * vesselDetail.flagLabel added to FR + EN dicts
  * vessel detail header now reads 'Pavillon: NOR' on /fr instead of
    leaking the EN literal

Verification: 210 pytest pass (+1 health/backup case); next tsc clean;
12 Playwright e2e specs pass; live API serves the new fields end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
breaching pushed a commit that referenced this pull request May 5, 2026
Adds the 6 specs called out in PROD_READINESS #3 + #15:

- web/e2e/live-loads.spec.ts: open /en/live, assert >=1 vessel dot,
  banner shows fleet count.
- web/e2e/vessels-loads.spec.ts: /en/vessels mini-map renders with
  dots, click first dot -> popup with severity + dossier link.
- web/e2e/vessel-detail.spec.ts: /en/vessels/antarctic_endurance,
  history map height >100px (regression for the inset-0 collapse),
  trail SVG present, scrubber controls visible.
- web/e2e/firms-toggle.spec.ts: toggle FIRMS layer, assert
  /api/firms/recent fires + label updates.
- web/e2e/operator-background.spec.ts: vessel detail page, assert
  "Aker BioMarine" + "Norway" appear (Wikidata cascade integration).
- web/e2e/a11y.spec.ts: @axe-core/playwright across 7 routes
  (home, about, vessels list, campaigns hub, privacy, terms, legal),
  WCAG 2.1 AA tags. Fails on serious/critical only — minor/moderate
  tolerated so a single tag-soup edge case doesn't block deploys.

- web/playwright.config.ts: chromium project, fullyParallel,
  CI-aware retries/workers/reporter. We deliberately don't use
  Playwright's webServer option — orchestration is owned by the root
  `npm run dev`.

- .gitignore: drop web/test-results/, web/playwright-report/,
  web/playwright/.cache/.

Closes prod-P0 #3 (5 specs) and the axe-core half of P2 #15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
breaching pushed a commit that referenced this pull request May 5, 2026
Vessel-detail and live-tracker UX improvements that were partly shipped
on disk but never committed:

- web/components/map/LiveTrackerMap.tsx (+541 lines):
  * home-port markers (subdued grey diamond, distinct from live AIS dots)
    so silent whaling/IUU vessels appear on the map with a clear
    "no AIS — last known port" marker. Uses position_source flag.
  * NASA FIRMS thermal hot-spots layer (VIIRS NOAA-20) — pulsing red
    markers for fishing-vessel "lights at sea" detections.
  * NASA GIBS true-colour imagery overlay (VIIRS daily mosaic, ~3 h
    latency) with opacity slider so the journalist can see clouds /
    sea-ice edge while keeping dot/zone overlays readable. Date defaults
    to UTC-yesterday since today's tile is often missing for first 6 h.
  * clickAction="popup" mode for /vessels (no side drawer there).
  * onAmbientCount + onFirmsCount callbacks so parent UIs can show
    "(0)" next to a toggle when the fetch found nothing.
  * Extended ALPHA3->ALPHA2 flag map (ISL, FRO, CAN, AUS, etc.).
- web/components/map/VesselsMapClient.tsx: rewire to popup mode +
  ambient count.
- web/components/vessels/{Sanctions,OperatorBackground}Section.tsx:
  optional `prefetched` prop so the parent vessel page can launch the
  three vessel-detail fetches via Promise.all and pass results in,
  skipping serial round-trips. Backwards-compatible (still self-fetches
  when the prop is undefined). OperatorBackground also migrated to
  next/image with explicit dims (no CLS) for Wikimedia thumbnails.
- web/app/[locale]/vessels/[slug]/VesselHistoryMap.tsx: small fix.
- web/app/[locale]/vessels/page.tsx (+34): MISSION_ORDER renders 4
  sections (krill / whaling / Galápagos / West Africa) so the Galápagos
  + West Africa missions stop being fused under "IUU FISHING".
- web/app/[locale]/live/{LivePageClient,page.tsx}: layer toggles
  reflect the new FIRMS/NASA opacity URL state (?firms=1&nasa=1&op=…).
- web/app/[locale]/campagnes/page.tsx: copy refresh for disaffiliation.
- web/components/live/{FleetSidebar,LiveMobileLayout,MobileFiltersSheet}:
  expose the FIRMS / NASA toggles in mobile + desktop sheets.
- web/lib/live-url-state.ts: new firms/nasa/op state knobs.
- web/components/{Acronym,Footer,SupportCTA}: small copy/style tweaks.
- web/app/globals.css: link-cascade fix wrapped in @layer base so
  Tailwind utilities still win the cascade (closes a contrast bug
  caught by axe-core on the support-CTA button).

Closes prod-P2 #16 (image opt), #17 (parallel SSR), audit #9
(Galápagos/West-Africa fused), #15 (LIVE: 0 banner backed by source
data), and most of the live-tracker polish from sprint-3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
breaching pushed a commit that referenced this pull request May 7, 2026
The desktop /live route renders LiveStatsOverlay over the map, which
swaps the green LIVE pill for a yellow "AIS FEED QUIET" warning when
the freshest position is older than 6 h (audit bug #15 resolution).
The mobile layout was missing the same overlay — so a journalist on a
phone hit "Live: 0" with no explanation while desktop visitors got the
quiet-feed context.

Importing LiveStatsOverlay into LiveMobileLayout and dropping it
inside the absolute map container (under the top bar). Same component,
same threshold, same i18n strings. 12/12 Playwright + a11y still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
breaching pushed a commit that referenced this pull request May 8, 2026
The home page card showed a green pulsing 'LIVE' pill plus the relative
'Last position' age, but never warned that the freshest AIS ping was
older than 6 h. /live already does this via LiveStatsOverlay; aligning
the home banner closes the parallel of audit #15.

Same 6 h threshold as LiveStatsOverlay so the home and the tracker can't
disagree by more than 30 s of clock skew. Yellow 'AIS FEED QUIET' /
'FLUX AIS CALME' pill, yellow border and gradient. Verified live with
the current ~10 d stale ping.
breaching pushed a commit that referenced this pull request May 8, 2026
This commit lands the work documented in PROD_READINESS.md and
docs/AUDIT_NOTES.md as deferred at the end of the 2026-04-29 audit
batch, plus the audit-P19 'Pavillon' i18n leak.

Audit bug #14 — FR translations for curated incidents
  * config/incidents.json: 4 entries gained title_fr/summary_fr
  * src/incidents.py + web/lib/krill-watch.ts: optional locale fields
  * web/components/vessels/IncidentList.tsx: prefers locale variant,
    drops the 'Source · EN' fallback badge when a translation exists

PROD #5 — backup status surfaced on /api/health
  * scripts/backup_duckdb.py writes data/.backup_status.json after a
    successful upload (timestamp, bucket, key, size, retention counts)
  * server/app.py /api/health surfaces a `backup` block with a derived
    `age_seconds` so the frontend can render 'backup ran X hours ago'
    without re-parsing the timestamp
  * tests/test_api.py: 2 new cases lock the contract (none + present)

PROD #10 — k6 load-test scaffold
  * scripts/loadtest.k6.js covers /api/health, /api/vessels,
    /api/positions/live, /api/zones, /api/incidents with three
    profiles (smoke / press-launch / sustained), p(95) thresholds

PROD #15 — Lighthouse CI scaffold
  * web/lighthouserc.js: soft-asserted budgets (perf 0.6 warn, a11y
    0.85 error, SEO 0.9 error) against the production-built bundle
  * .github/workflows/ci.yml: new lighthouse job, continue-on-error

Audit bug #19 — Pavillon i18n leak
  * vesselDetail.flagLabel added to FR + EN dicts
  * vessel detail header now reads 'Pavillon: NOR' on /fr instead of
    leaking the EN literal

Verification: 210 pytest pass (+1 health/backup case); next tsc clean;
12 Playwright e2e specs pass; live API serves the new fields end-to-end.
breaching pushed a commit that referenced this pull request May 8, 2026
Adds the 6 specs called out in PROD_READINESS #3 + #15:

- web/e2e/live-loads.spec.ts: open /en/live, assert >=1 vessel dot,
  banner shows fleet count.
- web/e2e/vessels-loads.spec.ts: /en/vessels mini-map renders with
  dots, click first dot -> popup with severity + dossier link.
- web/e2e/vessel-detail.spec.ts: /en/vessels/antarctic_endurance,
  history map height >100px (regression for the inset-0 collapse),
  trail SVG present, scrubber controls visible.
- web/e2e/firms-toggle.spec.ts: toggle FIRMS layer, assert
  /api/firms/recent fires + label updates.
- web/e2e/operator-background.spec.ts: vessel detail page, assert
  "Aker BioMarine" + "Norway" appear (Wikidata cascade integration).
- web/e2e/a11y.spec.ts: @axe-core/playwright across 7 routes
  (home, about, vessels list, campaigns hub, privacy, terms, legal),
  WCAG 2.1 AA tags. Fails on serious/critical only — minor/moderate
  tolerated so a single tag-soup edge case doesn't block deploys.

- web/playwright.config.ts: chromium project, fullyParallel,
  CI-aware retries/workers/reporter. We deliberately don't use
  Playwright's webServer option — orchestration is owned by the root
  `npm run dev`.

- .gitignore: drop web/test-results/, web/playwright-report/,
  web/playwright/.cache/.

Closes prod-P0 #3 (5 specs) and the axe-core half of P2 #15.
breaching pushed a commit that referenced this pull request May 8, 2026
Vessel-detail and live-tracker UX improvements that were partly shipped
on disk but never committed:

- web/components/map/LiveTrackerMap.tsx (+541 lines):
  * home-port markers (subdued grey diamond, distinct from live AIS dots)
    so silent whaling/IUU vessels appear on the map with a clear
    "no AIS — last known port" marker. Uses position_source flag.
  * NASA FIRMS thermal hot-spots layer (VIIRS NOAA-20) — pulsing red
    markers for fishing-vessel "lights at sea" detections.
  * NASA GIBS true-colour imagery overlay (VIIRS daily mosaic, ~3 h
    latency) with opacity slider so the journalist can see clouds /
    sea-ice edge while keeping dot/zone overlays readable. Date defaults
    to UTC-yesterday since today's tile is often missing for first 6 h.
  * clickAction="popup" mode for /vessels (no side drawer there).
  * onAmbientCount + onFirmsCount callbacks so parent UIs can show
    "(0)" next to a toggle when the fetch found nothing.
  * Extended ALPHA3->ALPHA2 flag map (ISL, FRO, CAN, AUS, etc.).
- web/components/map/VesselsMapClient.tsx: rewire to popup mode +
  ambient count.
- web/components/vessels/{Sanctions,OperatorBackground}Section.tsx:
  optional `prefetched` prop so the parent vessel page can launch the
  three vessel-detail fetches via Promise.all and pass results in,
  skipping serial round-trips. Backwards-compatible (still self-fetches
  when the prop is undefined). OperatorBackground also migrated to
  next/image with explicit dims (no CLS) for Wikimedia thumbnails.
- web/app/[locale]/vessels/[slug]/VesselHistoryMap.tsx: small fix.
- web/app/[locale]/vessels/page.tsx (+34): MISSION_ORDER renders 4
  sections (krill / whaling / Galápagos / West Africa) so the Galápagos
  + West Africa missions stop being fused under "IUU FISHING".
- web/app/[locale]/live/{LivePageClient,page.tsx}: layer toggles
  reflect the new FIRMS/NASA opacity URL state (?firms=1&nasa=1&op=…).
- web/app/[locale]/campagnes/page.tsx: copy refresh for disaffiliation.
- web/components/live/{FleetSidebar,LiveMobileLayout,MobileFiltersSheet}:
  expose the FIRMS / NASA toggles in mobile + desktop sheets.
- web/lib/live-url-state.ts: new firms/nasa/op state knobs.
- web/components/{Acronym,Footer,SupportCTA}: small copy/style tweaks.
- web/app/globals.css: link-cascade fix wrapped in @layer base so
  Tailwind utilities still win the cascade (closes a contrast bug
  caught by axe-core on the support-CTA button).

Closes prod-P2 #16 (image opt), #17 (parallel SSR), audit #9
(Galápagos/West-Africa fused), #15 (LIVE: 0 banner backed by source
data), and most of the live-tracker polish from sprint-3.
breaching pushed a commit that referenced this pull request May 8, 2026
The desktop /live route renders LiveStatsOverlay over the map, which
swaps the green LIVE pill for a yellow "AIS FEED QUIET" warning when
the freshest position is older than 6 h (audit bug #15 resolution).
The mobile layout was missing the same overlay — so a journalist on a
phone hit "Live: 0" with no explanation while desktop visitors got the
quiet-feed context.

Importing LiveStatsOverlay into LiveMobileLayout and dropping it
inside the absolute map container (under the top bar). Same component,
same threshold, same i18n strings. 12/12 Playwright + a11y still green.
breaching pushed a commit that referenced this pull request May 8, 2026
The home page card showed a green pulsing 'LIVE' pill plus the relative
'Last position' age, but never warned that the freshest AIS ping was
older than 6 h. /live already does this via LiveStatsOverlay; aligning
the home banner closes the parallel of audit #15.

Same 6 h threshold as LiveStatsOverlay so the home and the tracker can't
disagree by more than 30 s of clock skew. Yellow 'AIS FEED QUIET' /
'FLUX AIS CALME' pill, yellow border and gradient. Verified live with
the current ~10 d stale ping.
breaching added a commit that referenced this pull request May 11, 2026
Adds a `mission` field to the vessel model and onboards 3 commercial-whaling
targets as a sibling campaign. Demonstrates that the underlying pipeline
(zones, AIS gaps, severity, events) is generic and not krill-specific.

Backend
- src/vessel_db.py: new VesselMission literal ("krill" | "whaling" | "iuu_fishing"),
  added to Vessel model with default "krill" so existing entries are unchanged
- server/app.py: vessel summary now exposes the mission field
- config/krill_fleet.json: adds Kangei Maru (Japanese factory whaler, replaces
  Nisshin Maru, launched 2024), Hvalur 8 and Hvalur 9 (Icelandic fin-whaling
  catchers). All three have IMO/MMSI marked PENDING — operator must populate
  from MarineTraffic / Equasis before going live
- config/ccamlr_areas.geojson: adds three whale-sanctuary polygons (IWC
  Schedule 7) — Southern Ocean Sanctuary split E/W to avoid the antimeridian,
  plus the Indian Ocean Sanctuary. Tagged category="whale_sanctuary" so the
  existing severity rules (ais_gap_in_mpa) automatically fire CRITICAL when
  a whaler enters one and goes dark

Web
- web/data/vessels.ts: adds the three whaling vessels with bilingual notes
- web/app/[locale]/vessels/page.tsx: groups the catalogue by mission with a
  coloured heading per mission (krill = accent, whaling = danger). Whaling
  cards get a danger-tinted border + an explicit mission badge so the user
  sees at a glance these are not krill-fishery vessels
- web/lib/i18n.ts: adds mission labels in EN and FR; renames the page title
  from "Antarctic krill fleet" to "Tracked vessels" since the catalogue now
  spans multiple campaigns

Tests
- tests/test_api.py: locks in that /api/vessels exposes the mission field
  and includes the whaling mission with Kangei Maru in it (110 passed)

Verification
- python -m pytest tests/ -q → 110 passed
- python -m ruff check → All checks passed
- cd web && npx tsc --noEmit → exit 0
- cd web && npx next build → 6 new SSG pages generated for whaling vessels

Why it's small on purpose
- We deliberately did NOT abstract a "campaign" type yet. With one new mission
  in production, the duplication cost is zero and the abstraction shape is
  unclear. Wait until the second non-krill mission (Galápagos / West Africa
  Chinese fleet) before refactoring config/loaders into per-campaign units

Co-authored-by: breaching <devsca@protonmail.com>
breaching added a commit that referenced this pull request May 11, 2026
…-trawlers (#17)

Adds two new IUU-fishing campaigns alongside krill (mission=krill) and
whaling (mission=whaling). The detector pipeline (zones, AIS gaps,
severity, events) is fully reused — only data is added.

Galápagos campaign (mission=iuu_fishing)
- 3 representative Chinese squid jiggers / reefers in fleet config:
  Fu Yuan Yu Leng 999, Long Da 001, Hai Feng 718
- 2 zone polygons added to ccamlr_areas.geojson:
    GMR-GALAPAGOS — Galápagos Marine Reserve incl. Hermandad (2022 expansion)
    EEZ-GALAPAGOS-BUFFER — Ecuador 200-nm EEZ where the squid-jigger fleet
    historically congregates each Aug-Nov
- Headline signal: incursion into GMR (foreign-flag commercial fishing
  prohibited there) or AIS gap inside the buffer

West Africa campaign (mission=iuu_fishing)
- 3 representative DWF vessels in fleet config:
  Lurong Yuanyu 956 (CN), Lurong Yuanyu 988 (CN), Saltic Atlas (RU)
- 3 zone polygons added (simplified bounding boxes):
    EEZ-SENEGAL, EEZ-MAURITANIA, EEZ-GUINEA
- Headline signal: AIS gap inside any of these EEZs (vessel went dark
  while presumably trawling)

Web (`web/data/vessels.ts`)
- All 6 new vessels added to the static catalogue with bilingual notes
  describing the campaign context (Pingtan reefer 2017 Galápagos arrest;
  300+ vessel 2020 stand-off; EJF/Greenpeace West Africa documentation)
- They surface automatically in the existing /vessels mission-grouped UI
  (PR #15) — krill / whaling / IUU fishing sections

All vessels have IMO and MMSI marked PENDING — operator must populate
from GFW Carrier Vessel Portal / Equasis before deployment. The polygon
boundaries are simplified bounding boxes that err conservatively (over-
include slightly); operator should refine with official EEZ data
(e.g. https://www.marineregions.org/) before deployment.

Verification
- python -m pytest tests/ -q → 110 passed
- python -m ruff check → All checks passed
- cd web && npx tsc --noEmit → exit 0

What's deliberately NOT done
- No "campaign" abstraction yet. With 4 missions still hanging off
  krill_fleet.json + ccamlr_areas.geojson, the duplication cost remains
  near zero. Right time for the refactor is when the 5th mission lands.

Co-authored-by: breaching <devsca@protonmail.com>
breaching added a commit that referenced this pull request May 11, 2026
…19)

INVESTIGATOR_PLAYBOOK.md
- New §4 "Multi-mission monitoring" — table of 4 missions, headline signals,
  tracked zones, per-mission webhook routing, smoke-test recipe via the
  /api/_dev/test-alert endpoint
- New §5 "Weekly operational rhythm" — describes the CI cron + production
  cron + Monday-morning Discord ping flow
- "What this tool is NOT" updated to clarify the live-alerting vs offline-
  encounter latency distinction, and to flag IUU vessel IMOs as PENDING
  alongside the existing OFAC/CM-10-09 stub callouts

README.md
- Subtitle pivoted from "krill fishing fleet" to "CPWF-relevant fleets —
  Antarctic krill, commercial whaling, and IUU fishing"
- New top bullet describing the 4 missions; added "Live alerting" + cron
  bullets so the README accurately advertises what shipped in PRs #15-18

.env.example
- Generic + per-mission webhook URLs documented
- WEB_BASE_URL / WEB_DEFAULT_LOCALE for deep links
- Production weekly-report runner vars (KRILL_DB_PATH,
  KRILL_REPORT_OUT_DIR, KRILL_REPORT_PUBLIC_URL,
  DISCORD_WEBHOOK_URL_REPORTS)
- KRILL_ALLOW_DEV_ENDPOINTS for the dev test-alert endpoint

Co-authored-by: breaching <devsca@protonmail.com>
breaching pushed a commit that referenced this pull request May 11, 2026
This commit lands the work documented in PROD_READINESS.md and
docs/AUDIT_NOTES.md as deferred at the end of the 2026-04-29 audit
batch, plus the audit-P19 'Pavillon' i18n leak.

Audit bug #14 — FR translations for curated incidents
  * config/incidents.json: 4 entries gained title_fr/summary_fr
  * src/incidents.py + web/lib/krill-watch.ts: optional locale fields
  * web/components/vessels/IncidentList.tsx: prefers locale variant,
    drops the 'Source · EN' fallback badge when a translation exists

PROD #5 — backup status surfaced on /api/health
  * scripts/backup_duckdb.py writes data/.backup_status.json after a
    successful upload (timestamp, bucket, key, size, retention counts)
  * server/app.py /api/health surfaces a `backup` block with a derived
    `age_seconds` so the frontend can render 'backup ran X hours ago'
    without re-parsing the timestamp
  * tests/test_api.py: 2 new cases lock the contract (none + present)

PROD #10 — k6 load-test scaffold
  * scripts/loadtest.k6.js covers /api/health, /api/vessels,
    /api/positions/live, /api/zones, /api/incidents with three
    profiles (smoke / press-launch / sustained), p(95) thresholds

PROD #15 — Lighthouse CI scaffold
  * web/lighthouserc.js: soft-asserted budgets (perf 0.6 warn, a11y
    0.85 error, SEO 0.9 error) against the production-built bundle
  * .github/workflows/ci.yml: new lighthouse job, continue-on-error

Audit bug #19 — Pavillon i18n leak
  * vesselDetail.flagLabel added to FR + EN dicts
  * vessel detail header now reads 'Pavillon: NOR' on /fr instead of
    leaking the EN literal

Verification: 210 pytest pass (+1 health/backup case); next tsc clean;
12 Playwright e2e specs pass; live API serves the new fields end-to-end.
breaching pushed a commit that referenced this pull request May 11, 2026
Adds the 6 specs called out in PROD_READINESS #3 + #15:

- web/e2e/live-loads.spec.ts: open /en/live, assert >=1 vessel dot,
  banner shows fleet count.
- web/e2e/vessels-loads.spec.ts: /en/vessels mini-map renders with
  dots, click first dot -> popup with severity + dossier link.
- web/e2e/vessel-detail.spec.ts: /en/vessels/antarctic_endurance,
  history map height >100px (regression for the inset-0 collapse),
  trail SVG present, scrubber controls visible.
- web/e2e/firms-toggle.spec.ts: toggle FIRMS layer, assert
  /api/firms/recent fires + label updates.
- web/e2e/operator-background.spec.ts: vessel detail page, assert
  "Aker BioMarine" + "Norway" appear (Wikidata cascade integration).
- web/e2e/a11y.spec.ts: @axe-core/playwright across 7 routes
  (home, about, vessels list, campaigns hub, privacy, terms, legal),
  WCAG 2.1 AA tags. Fails on serious/critical only — minor/moderate
  tolerated so a single tag-soup edge case doesn't block deploys.

- web/playwright.config.ts: chromium project, fullyParallel,
  CI-aware retries/workers/reporter. We deliberately don't use
  Playwright's webServer option — orchestration is owned by the root
  `npm run dev`.

- .gitignore: drop web/test-results/, web/playwright-report/,
  web/playwright/.cache/.

Closes prod-P0 #3 (5 specs) and the axe-core half of P2 #15.
breaching pushed a commit that referenced this pull request May 11, 2026
Vessel-detail and live-tracker UX improvements that were partly shipped
on disk but never committed:

- web/components/map/LiveTrackerMap.tsx (+541 lines):
  * home-port markers (subdued grey diamond, distinct from live AIS dots)
    so silent whaling/IUU vessels appear on the map with a clear
    "no AIS — last known port" marker. Uses position_source flag.
  * NASA FIRMS thermal hot-spots layer (VIIRS NOAA-20) — pulsing red
    markers for fishing-vessel "lights at sea" detections.
  * NASA GIBS true-colour imagery overlay (VIIRS daily mosaic, ~3 h
    latency) with opacity slider so the journalist can see clouds /
    sea-ice edge while keeping dot/zone overlays readable. Date defaults
    to UTC-yesterday since today's tile is often missing for first 6 h.
  * clickAction="popup" mode for /vessels (no side drawer there).
  * onAmbientCount + onFirmsCount callbacks so parent UIs can show
    "(0)" next to a toggle when the fetch found nothing.
  * Extended ALPHA3->ALPHA2 flag map (ISL, FRO, CAN, AUS, etc.).
- web/components/map/VesselsMapClient.tsx: rewire to popup mode +
  ambient count.
- web/components/vessels/{Sanctions,OperatorBackground}Section.tsx:
  optional `prefetched` prop so the parent vessel page can launch the
  three vessel-detail fetches via Promise.all and pass results in,
  skipping serial round-trips. Backwards-compatible (still self-fetches
  when the prop is undefined). OperatorBackground also migrated to
  next/image with explicit dims (no CLS) for Wikimedia thumbnails.
- web/app/[locale]/vessels/[slug]/VesselHistoryMap.tsx: small fix.
- web/app/[locale]/vessels/page.tsx (+34): MISSION_ORDER renders 4
  sections (krill / whaling / Galápagos / West Africa) so the Galápagos
  + West Africa missions stop being fused under "IUU FISHING".
- web/app/[locale]/live/{LivePageClient,page.tsx}: layer toggles
  reflect the new FIRMS/NASA opacity URL state (?firms=1&nasa=1&op=…).
- web/app/[locale]/campagnes/page.tsx: copy refresh for disaffiliation.
- web/components/live/{FleetSidebar,LiveMobileLayout,MobileFiltersSheet}:
  expose the FIRMS / NASA toggles in mobile + desktop sheets.
- web/lib/live-url-state.ts: new firms/nasa/op state knobs.
- web/components/{Acronym,Footer,SupportCTA}: small copy/style tweaks.
- web/app/globals.css: link-cascade fix wrapped in @layer base so
  Tailwind utilities still win the cascade (closes a contrast bug
  caught by axe-core on the support-CTA button).

Closes prod-P2 #16 (image opt), #17 (parallel SSR), audit #9
(Galápagos/West-Africa fused), #15 (LIVE: 0 banner backed by source
data), and most of the live-tracker polish from sprint-3.
breaching pushed a commit that referenced this pull request May 11, 2026
The desktop /live route renders LiveStatsOverlay over the map, which
swaps the green LIVE pill for a yellow "AIS FEED QUIET" warning when
the freshest position is older than 6 h (audit bug #15 resolution).
The mobile layout was missing the same overlay — so a journalist on a
phone hit "Live: 0" with no explanation while desktop visitors got the
quiet-feed context.

Importing LiveStatsOverlay into LiveMobileLayout and dropping it
inside the absolute map container (under the top bar). Same component,
same threshold, same i18n strings. 12/12 Playwright + a11y still green.
breaching pushed a commit that referenced this pull request May 11, 2026
The home page card showed a green pulsing 'LIVE' pill plus the relative
'Last position' age, but never warned that the freshest AIS ping was
older than 6 h. /live already does this via LiveStatsOverlay; aligning
the home banner closes the parallel of audit #15.

Same 6 h threshold as LiveStatsOverlay so the home and the tracker can't
disagree by more than 30 s of clock skew. Yellow 'AIS FEED QUIET' /
'FLUX AIS CALME' pill, yellow border and gradient. Verified live with
the current ~10 d stale ping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant