Commit graph

70 commits

Author SHA1 Message Date
Sirius DevOps
734c310d2c perf: abort stale overlay fetches, skip same-cell rebuilds, update markers in place
moveend aborts in-flight overlay JSON and skips work when the 0.01° cell
is unchanged. Live aircraft/trains/vessels diff-update by id. Camera popup
HTML is built on open. Alert/perimeter polygons paint on canvas.
2026-08-27 21:23:25 -04:00
Sirius DevOps
435f63e473 perf: shared httpx client, gzip, overlay cache-control
Reuse one TLS pool for overlay upstreams (8s/3s timeouts). Gzip JSON
over 1 KB. Aircraft/vessels Cache-Control max-age=5, alerts/perimeters 30.
Lifespan replaces deprecated on_event startup.
2026-08-27 21:21:19 -04:00
Sirius DevOps
8f89994201 perf: slim fires/cameras/events/news map payloads
Heatmap uses {lat,lon,i,c}. Camera list drops URLs (detail via GET
/api/cameras/{id}). Event blips skip body. News omits content unless
include_content=true.
2026-08-27 21:19:42 -04:00
Sirius DevOps
9c14b899ee perf: simplify WFIGS perimeter geometry at query time
Ask ArcGIS for 500 features, 5-decimal precision, and ~250m offset so
fire rings are outlines instead of tens of thousands of vertices.
2026-08-27 21:16:53 -04:00
Sirius DevOps
fdd59052c5 perf: cache IEM SBW globally, clip alerts to viewport, slim properties
National storm-based warnings are fetched once (45s TTL) and clipped to
the quantized bbox. Popup fields only — NWS descriptions stay off the wire.
2026-08-27 21:16:16 -04:00
Sirius DevOps
84532d505a perf: per-key overlay cache locks and quantized bbox keys
Nearby pans share a 0.25° cache cell. Slow NWS/WFIGS factories no longer
hold a process-wide lock that stalls trains/aircraft/radar fills.
2026-08-27 21:14:49 -04:00
Sirius DevOps
07638288a9 perf: faster map boot — zoom-gate heavy layers, static basemap, defer HUD, lazy HLS
World view no longer fetches cameras/NWS/WFIGS. Default Blue Marble so init
does not wait on GIBS times. News/summary/events load with their views.
hls.min.js loads only on the first HLS camera popup.
2026-08-27 21:12:58 -04:00
dd7a09aadd Merge pull request 'Distinct heading-aware marker symbols for live feeds (aircraft/trains/vessels)' (#3) from feat/distinct-live-markers into master
All checks were successful
build-and-deploy / build (push) Successful in 2m55s
Reviewed-on: #3
2026-08-27 20:50:05 -04:00
Sirius DevOps
ce5207e060 feat: distinct heading-aware marker symbols for live feeds (aircraft/trains/vessels)
Replace the shared colored-dot renderPoints for live feeds with per-feed SVG
glyphs on heading-aware divIcons:
- Aircraft: plane silhouette rotated by heading, color-coded by alt
- Trains: railcar glyph (respecting per-train iconColor), rotated when heading present
- Vessels: sailboat glyph rotated by heading, color by speed (teal/gray)

Icons are cached per (feed, color, heading) so thousands of markers reuse a
bounded set of L.divIcon instances. Rotation is applied to the inner glyph
span only (never the marker container), keeping clustering, spiderfy, and
iconAnchor intact. Missing/NaN heading falls back to an upright glyph.
Color values are sanitized against style-attribute breakout.
2026-08-27 20:50:35 -04:00
cc03778a87 Merge pull request 'feat: toggleable live map feeds (ADS-B, trains, AIS, radar, WFIGS, NWS)' (#2) from feat/live-map-feeds into master
All checks were successful
build-and-deploy / build (push) Successful in 2m54s
Reviewed-on: #2
2026-08-27 19:08:21 -04:00
Sirius DevOps
f779b1f225 feat: toggleable live map feeds (ADS-B, trains, AIS, radar, WFIGS, NWS)
Wire the free data streams from docs/free-data-streams.md into the
dashboard as layer-panel toggles. Third-party APIs are proxied/cached
in FastAPI; raster tiles (IEM, RainViewer, GIBS) stay in the browser.

- Aircraft via ADSB.lol viewport poll (bbox required, radius ≤ 150 nm)
- Amtraker trains, NHC storms, WFIGS incidents/perimeters
- NWS + IEM SBW as /api/weather-alerts (does not collide with /api/alerts)
- AISStream worker is server-side only and idles without AISSTREAM_API_KEY
- Caltrans CWWP2 D1–D12 camera parser; FIRMS dual-write NOAA-20/21
2026-08-27 19:08:30 -04:00
Sirius DevOps
47628cc5f2 ci: pin db image instead of rebuilding in CI
All checks were successful
build-and-deploy / build (push) Successful in 2m14s
Dockerfile.pg installs TimescaleDB from packagecloud.io, which the Pi's
network cannot reach (ISP abuse-mitigation blackholes AWS ranges after the
masscan incident). Every CI deploy rebuilt the DB image and failed on the
packagecloud fetch, blocking all pipelines.

The DB image is now built once manually (docker compose build db) and
pinned as localhost/osint-dashboard-pg:latest; compose up uses it directly.
The app build only needs deb.debian.org + pypi + the docker daemon's
registry mirror, all reachable.
2026-08-27 18:26:57 -04:00
Sirius DevOps
50d3dbb1dc chore: drop merged frontend-overhaul worktree gitlink
Some checks failed
build-and-deploy / build (push) Failing after 35s
The Ghost-in-the-Shell overhaul was merged via PR #1; the .worktrees
gitlink was only ever a local staging worktree and must not live in the
repo — it breaks CI checkout (git submodule foreach: 'No url found for
submodule path .worktrees/frontend-overhaul').
2026-08-27 17:55:08 -04:00
fd43a6165a Merge pull request 'frontend: Ghost-in-the-Shell overhaul — map-first landing, event blips, tickers' (#1) from frontend-overhaul into master
Some checks failed
build-and-deploy / build (push) Failing after 1m5s
Reviewed-on: #1
2026-08-27 17:48:58 -04:00
Sirius DevOps
2a433a41e9 map: port master popup-guard fix — real popup state via events, close on zoom/drag
Master's b2369bc fixed cameras vanishing after opening a popup and zooming:
Leaflet 1.9.4's Map.closePopup() never nulls map._popup, so the old
'if (map._popup) return' guard skipped every overlay reload forever after
the first popup. Port the fix into the redesigned frontend: track popup
state via popupopen/popupclose, close on user zoom/drag so moveend reloads
always run, and keep the autopan skip for the popup's own pan.
2026-08-27 17:48:39 -04:00
Sirius DevOps
8560884bbd Merge remote-tracking branch 'forgejo/master' into frontend-overhaul
# Conflicts:
#	app/static/index.html
2026-08-27 17:47:31 -04:00
Sirius DevOps
2ce0e9ba4e ci: retrigger deploy (previous run raced the force-push)
Some checks failed
build-and-deploy / build (push) Failing after 39s
2026-08-27 17:42:03 -04:00
Sirius DevOps
b2369bcc20 map: fix cameras vanishing after opening a camera popup and zooming
Some checks failed
build-and-deploy / build (push) Failing after 52s
Root cause: Leaflet 1.9.4's Map.closePopup() never nulls map._popup (only
unbindPopup does), so the moveend guard 'if (map._popup) return' skipped
every overlay reload forever after the first popup was ever opened. Opening
one camera popup froze the marker layer: zoom out and the map kept showing
the stale zoomed-in subset (or nothing) until a full page refresh.

Track real popup state via popupopen/popupclose events instead, and close
the popup on user zoom/drag so the moveend reload always runs after
navigation; the autopan skip still protects the popup when it pans itself
into view.
2026-08-27 17:38:48 -04:00
Sirius DevOps
51e906c41f Merge remote-tracking branch 'forgejo/master' into frontend-overhaul
# Conflicts:
#	app/static/index.html
2026-08-27 17:33:12 -04:00
Sirius DevOps
12f01a17a9 frontend: Ghost-in-the-Shell overhaul — map-first landing, HUD chrome, event blips, tickers
- Map is now the landing view: full-viewport NASA GIBS globe, existing
  fires/cameras/HLS map machinery preserved verbatim
- New 'Event Blips' layer: geolocated ingest events, color-coded by source,
  bbox/since/has_coords filters added to GET /api/events
- Weather / Flights(ADS-B) / Vessels(AIS) layer slots reserved (feed pending)
- Market ticker strip with configurable symbols — auto-promotes to LIVE when
  GET /api/market returns {symbols:[...]} (contract documented in Settings)
- Breaking-news ticker: LLM exec-summary flash + headline marquee, 15-min cycle
- Dropdown nav (Map/News/Events/Alerts/Entities/Ingest/API Keys/Settings),
  Settings view (localStorage: symbols, map layer defaults), System panel
- Section-9 theme: near-black navy, cyan/magenta accents, Orbitron/Rajdhani/
  Share Tech Mono, chamfered HUD panels, scanlines, boot splash, UTC clock
- Fix: events.camera enum value missing from models.py/schemas.py caused 500s
  on every events query once camera events flowed in (migration 004 added it
  to the DB enum only)
2026-08-27 17:22:14 -04:00
Sirius DevOps
5cea0a5940 map: cluster camera markers so cameras stay visible when zoomed in
All checks were successful
build-and-deploy / build (push) Successful in 4m33s
Zooming in replaced the marker group with a bbox subset, so dense world
views (5k dots) thinned to a handful of pins — or zero on a wrapped world
copy, where currentBBox collapsed to a sliver and the API returned ~nothing.
Cameras only seemed to exist when zoomed out.

- vendor leaflet.markercluster 1.5.3; camera markers now render as numbered
  neon-green clusters at low zoom and split into individual dots as you zoom,
  so camera coverage is visible at every zoom level (spiderfy at max zoom)
- guard against null lat/lon rows (no phantom markers at 0,0)
- stale-response guard for loadCams/loadFires: rapid wheel zoom can resolve
  fetches out of order and a stale bbox response blanked the map
- serve index.html with Cache-Control: no-cache so the browser never keeps
  serving a pre-deploy copy after fixes land
2026-08-27 17:00:09 -04:00
Sirius DevOps
6f0fc8dbe3 cameras: unwrap Leaflet world copies so California bbox hits ALERTWest
All checks were successful
build-and-deploy / build (push) Successful in 2m54s
Viewing CA on a wrapped map copy clamped both edges to 180, so the API
returned the newest 2000 feeds (mostly VDOT HLS in Virginia) and the CA
viewport was empty. Normalize longitudes into [-180,180] and load 5000.
2026-08-27 16:03:50 -04:00
Sirius DevOps
1145ce8ec1 cameras: fix world-zoom bbox collapsing to ~48 pins
All checks were successful
build-and-deploy / build (push) Successful in 2m23s
Leaflet worldCopyJump at zoom 2 reports longitudes outside ±180; clamping
them independently produced west>=east or a thin sliver. Low zoom now
queries the full world, and the API ignores inverted bboxes.
2026-08-27 15:59:22 -04:00
Sirius DevOps
0c7f80655e cameras: Live-Environment-Streams + HLS.js player
All checks were successful
build-and-deploy / build (push) Successful in 3m8s
Ingest ~4.2k direct HLS/YouTube feeds (VDOT, MDSHA, DelDOT, Iowa DOT,
OpenCCTV, etc.) from the public GeoJSON catalog. Popup plays HLS via a
CORS-safe playlist proxy + vendored hls.js, YouTube via embed, with
ffmpeg-MJPEG fallback. ALERTWest rows now use a stable camera id so
hourly scrapes do not duplicate.
2026-08-27 15:54:54 -04:00
Sirius DevOps
f3c35c2230 cameras: ingest ALERTCalifornia/ALERTWest official public JPEGs
All checks were successful
build-and-deploy / build (push) Successful in 3m36s
Adds the documented getCameraDataByLoc API (~10k public wildfire, DOT,
and FAA stills with lat/lon). Masscan will not produce viewable feeds;
this will. Map bbox fetch raised to 2000 markers.
2026-08-27 15:46:49 -04:00
Sirius DevOps
7ceba736dd cameras: map only working HTTP/MJPEG feeds
All checks were successful
build-and-deploy / build (push) Successful in 2m53s
Masscan open-554 hosts almost never have a public picture. The map now
defaults to cameras with an http(s) snapshot_url (~132 directory cams).

Ingest probes unauthenticated still/MJPEG paths first and skips the rest,
so dead RTSP-only hosts never land on the map. Pass working=false to see
unverified port-554 hits.
2026-08-25 22:05:15 -04:00
Sirius DevOps
12262d1562 cameras: RTSP preview via ffmpeg, stop opening VLC, drop masscan to 200pps
All checks were successful
build-and-deploy / build (push) Successful in 2m23s
Masscan finds are rtsp:// with no snapshot_url, so the popup skipped the
<img> and the leftover source link handed the OS an rtsp:// URL (VLC).

- Popup always hits /api/cameras/{id}/snapshot (HTTP stills, then one
  ffmpeg frame grab). No credentials. 10s hard timeout.
- rtsp:// is rendered as text, never as an href.
- ffmpeg added to the app image for the RTSP still/MJPEG path.
- Default MASSCAN_RATE 200 (1k/10k saturated the home uplink).
2026-08-24 23:36:34 -04:00
Sirius DevOps
3af9511a3a masscan: drop default rate to 1000 pps (home uplink)
All checks were successful
build-and-deploy / build (push) Successful in 1m39s
10k pps saturated the residential link. New default is 1000 pps
(~0.6 Mbps of SYNs). Live override is /etc/osint-dashboard/masscan.env.
2026-08-24 23:22:03 -04:00
Sirius DevOps
085061492e masscan: active RTSP (554) camera discovery service
All checks were successful
build-and-deploy / build (push) Successful in 2m0s
Continuous whole-IPv4 rolling sweep for open TCP 554, feeding the same
cameras table as the passive scraper (discovery_source=masscan).

- masscan_config.py: env-driven knobs (range, ports, rate, retries, excludes)
- masscan_scanner.py: JSON-lines parser, rtsp://IP/ URL + url_hash dedupe,
  ip-api geolocation, insert/refresh, NATS publish for new finds
- run_masscan_service.py: long-lived rolling-sweep runner (streams results
  in, restarts on pass completion); fails closed without an excludefile
- deploy/: systemd unit + README + excludes file for the Pi host
- .env.example: masscan section

Verified end-to-end against a local Postgres: parse, insert, and dedupe
(0 new on re-ingest) all pass.
2026-08-24 22:23:36 -04:00
Sirius DevOps
11085f8901 ci: retrigger clean deploy (was racing a manual compose up)
All checks were successful
build-and-deploy / build (push) Successful in 2m7s
2026-08-24 22:12:07 -04:00
Sirius DevOps
16436a8c08 fires: default FIRMS_DAYS to 2 (NRT latency leaves day=1 empty)
Some checks failed
build-and-deploy / build (push) Failing after 1m31s
At some hours FIRMS NRT returns zero detections for day range 1;
day range 2 returns thousands. Bump default to 2.
2026-08-24 22:07:51 -04:00
Sirius DevOps
250dbbb5df Camera popups: live MJPEG preview via /api/cameras/{id}/stream passthrough, snapshot fallback, taller thumb
All checks were successful
build-and-deploy / build (push) Successful in 1m52s
2026-08-24 21:38:46 -04:00
Sirius DevOps
1f0b831835 fires: append FIRMS day-range param; API 400s without it
All checks were successful
build-and-deploy / build (push) Successful in 1m54s
FIRMS area-CSV requires an explicit [1..5] day range — requests without
it return 'Invalid day range. Expects [1..5].' as a 400. New
FIRMS_DAYS env (default 1).
2026-08-24 21:32:55 -04:00
Sirius DevOps
1ce3da4403 Migration 004 fix: pg_type catalog table, not pg_typname
All checks were successful
build-and-deploy / build (push) Successful in 1m37s
2026-08-24 21:29:25 -04:00
Sirius DevOps
c1a19ecf06 Migration 004: add 'camera' to event_source_type enum so ingester can store camera events
All checks were successful
build-and-deploy / build (push) Successful in 1m57s
2026-08-24 21:25:21 -04:00
Sirius DevOps
e6648ec1ed fires: resolve FIRMS_MAP_KEY from Postgres keystore, not just env
All checks were successful
build-and-deploy / build (push) Successful in 1m40s
Key saved via the dashboard Keys page sat unused in api_keys while
ingest_fires only checked the env var. Now env first, keystore fallback,
picked up on next poll without a restart.
2026-08-24 21:21:15 -04:00
Sirius DevOps
b1c611d4c4 Ingestor: update JetStream stream subjects on startup so events.camera gets covered (add_stream silently no-ops when stream exists)
All checks were successful
build-and-deploy / build (push) Successful in 1m52s
2026-08-24 21:19:20 -04:00
Sirius DevOps
456712fc32 Ingestor: include events.camera in JetStream stream subjects (camera publishes were hitting NoResponders)
All checks were successful
build-and-deploy / build (push) Successful in 1m51s
2026-08-24 21:11:34 -04:00
Sirius DevOps
347ee3a642 Camera scraper: live public source (fury999io/public-ip-cams), markdown-list parser, batch IP geolocation via ip-api; fix empty-env fallback so default source applies
All checks were successful
build-and-deploy / build (push) Successful in 1m38s
2026-08-24 21:03:33 -04:00
Sirius DevOps
28146c1adc Map: clamp GIBS daily-layer latest to yesterday UTC — today's mosaic isn't ingested yet at 00:xx UTC so tiles 404'd and the map went black
All checks were successful
build-and-deploy / build (push) Successful in 1m39s
2026-08-24 20:47:50 -04:00
f122740d56 CI: verify end-to-end deploy after compose plugin + project-name fixes
All checks were successful
build-and-deploy / build (push) Successful in 1m36s
2026-08-24 18:07:49 -04:00
Sirius DevOps
acbf5cc56d Add news feed panel to dashboard: exec summary pinned + scrolling headlines + 15min auto-refresh
Some checks failed
build-and-deploy / build (push) Has been cancelled
- New News tab consuming GET /api/news (headlines: source domain badge,
  relative timestamp, link-out target=_blank) and GET /api/news/summaries
  (latest executive summary pinned in a glowing briefing card).
- Four states handled: loading, empty (summarizer idle w/o GEMINI key),
  error, success; HTML-escaped titles/domains (newsEsc).
- Lightweight markdown renderer for summary bodies (bold/headers/lists).
- Auto-refresh every 15min (NEWS_REFRESH_MS) + lazy load on tab open + manual
  refresh button.
- Stretch hook: if articles ever carry lat/lon, headline pins plot on the
  GIBS map (renderNewsPins) + geotag indicator in the feed.
- Dark neon styling matching existing dashboard theme.
2026-08-24 18:06:43 -04:00
Sirius DevOps
aec3ecae9a Map: FIRMS heatmap + camera thumbnail popups + unified layer panel
All checks were successful
build-and-deploy / build (push) Successful in 1m37s
- Vendored leaflet.heat 0.2.0; fires overlay is now a real heatmap
  (L.heatLayer) with intensity driven by brightness or VIIRS confidence
  (color mode selector) and a since= time filter (6h/24h/48h/7d/30d/all).
- Camera markers now open popups with the live snapshot thumbnail proxied
  through GET /api/cameras/{id}/snapshot (backend TTL cache) + full
  metadata (vendor, device, discovery source, first/last seen, coords,
  source link); missing-snapshot cams show a placeholder instead of 404.
- New collapsible Layers panel (dark neon) over the map: per-layer
  visibility toggles, opacity sliders (basemap/fires/cameras), live
  counts legend, and a fire heat-intensity ramp that swaps for the
  confidence legend in confidence mode.
- Clamp currentBBox() to world bounds so /api/cameras doesn't 422 at low
  zoom (worldCopyJump spans multiple copies).
- Skip overlay reloads on moveend while a popup is open — popup autopan
  no longer rebuilds the marker group and closes the popup.
- Zoom control moved to top-right so it doesn't collide with the panel.
2026-08-24 18:03:21 -04:00
347f781cf6 CI: fix deploy failures — pin compose project name in docker-compose.yml, drop COMPOSE_PROJECT_NAME=osint override (would orphan osint-dashboard_osint-pgdata); runner now mounts cli-plugins so 'docker compose' works in job containers
Some checks failed
build-and-deploy / build (push) Failing after 3s
2026-08-24 17:58:50 -04:00
Sirius DevOps
5849d72299 Fix news scraper feed discovery + summarizer startup race
Some checks failed
build-and-deploy / build (push) Failing after 4s
The vendored spider only started on urls.txt lines containing '/rss' or
'/feed' — but the curated urls.txt holds 200 homepages, so the crawler
matched ZERO feeds and silently did nothing (2ms, 0 items). Rewrite the
spider with feed autodiscovery: fetch each homepage, find its
<link rel="alternate" type="application/rss+xml"> (or /feed|/rss link),
parse the feed, then follow each item to extract the article. Bump
DEPTH_LIMIT 1->3 (homepage -> feed -> article).

Also make the summarizer resilient to booting before the app container has
run alembic (docker-compose only guarantees `db` is up): add idempotent
ensure_tables() mirroring the scraper's CREATE TABLE IF NOT EXISTS.
2026-08-24 17:42:06 -04:00
Sirius DevOps
3aa6f265bb Add NASA GIBS satellite basemap world map tab (Leaflet)
Some checks failed
build-and-deploy / build (push) Failing after 5s
Interactive world map panel for the dashboard:
- Vendored Leaflet 1.9.4 under app/static/vendor/leaflet/ (served via
  new /static mount on the FastAPI app)
- New Map tab: GIBS WMTS raster basemap (BlueMarble_ShadedRelief_Bathymetry,
  VIIRS/MODIS/Aqua CorrectedReflectance_TrueColor, VIIRS_DayNightBand),
  layer picker + UTC date selector (Latest/-7d/-30d quick picks)
- /api/map/layers: curated GIBS catalog (tms, format, has_time, max_zoom)
- /api/map/times: per-layer date windows from GIBS Domains XML (6h cache,
  graceful 502 on GIBS hiccup; static layers 422)
- Fire hotspots (existing /api/fires) + open cameras (/api/cameras) as
  toggleable bbox-scoped overlays; dark UI matching dashboard theme
2026-08-24 17:35:44 -04:00
Sirius DevOps
91f436390b Add news pipeline: hourly scraper + Gemini summarizer in compose
Some checks failed
build-and-deploy / build (push) Failing after 4s
Vendor the newsPipeline scraper + summarizer into the repo and wire them into
docker-compose against the EXISTING osint-db (no second Postgres), replacing
the upstream k8s CronJobs with in-compose wall-clock loops (:00 scrape, :05
summarize).

- news/scraper: vendored Scrapy project (257 RSS feeds) + hourly loop
  scheduler (run_news_scraper.py)
- news/summerizer: vendored Gemini map-reduce summarizer, cleaned:
  * fix broken google-genai response handling (_extract_text, defensive)
  * fix malformed INSERT/GRANT query in save_summary_to_db
  * OSINT-neutral default MAP_PROMPT; futures/markets language gated behind
    INCLUDE_FUTURES=0 (yfinance lazy-imported)
  * env-configurable model, batch size, lookback window
  + hourly loop scheduler (run_news_summarizer.py, :05)
- alembic 003_news: idempotent articles + article_summaries tables
- API: GET /api/news and GET /api/news/summaries (+ models, schemas)
- tests/test_api_news.py: 5 DB-backed contract tests (all pass vs real PG)
- docs/news.md + .env.example updates

Both services run under the `ingest` compose profile (matching the
ingester/camera-scraper pattern) and build arm64 on the Pi via the existing
Forgejo CI workflow. telebot left out of scope (reserved env only).
2026-08-24 17:28:46 -04:00
Sirius DevOps
172273bed6 Fix alembic migration fork: chain 002_cameras after 002_fires
Some checks failed
build-and-deploy / build (push) Failing after 4s
002_fires and 002_cameras both had down_revision=001_initial, producing two
heads so 'alembic upgrade head' fails at container startup and the dashboard
never boots. Tables are independent; linearize 001 -> 002_fires -> 002_cameras.
2026-08-24 15:44:05 -04:00
Sirius DevOps
cf98af8101 Merge remote-tracking branch 'forgejo/master'
Some checks failed
build-and-deploy / build (push) Failing after 4s
# Conflicts:
#	.env.example
2026-08-24 15:40:31 -04:00
Sirius DevOps
627990efde Add NASA FIRMS active-fire ingest + /api/fires; API keys management page
Coherent merge of two coordinated features on the shared working tree:

FIRMS fire heatmap (backend, t_6e404c14):
- app/fire_sources.py: fetch FIRMS VIIRS area CSV (free MAP_KEY) -> NATS events.fire
- fires hypertable (TimescaleDB, 1-day chunks) with natural-key PK
  (latitude, longitude, acq_time, satellite); idempotent ON CONFLICT DO NOTHING
- alembic/versions/002_fires.py; GET /api/fires?bbox=&since= (JSON only)
- POST /api/ingest/fires; ~15 min poll loop (FIRMS_INTERVAL=900) in ingester
- env-driven config (FIRMS_MAP_KEY/DATASET/BBOX/INTERVAL); docs/firms.md covers
  the zero-cost GIBS VIIRS_SNPP_Thermal_Anomalies_375m_All tile alternative
- 18 tests (parser, mapping, idempotency, API contract) verified vs real
  TimescaleDB+PostGIS (localhost/osint-dashboard-pg image)

API keys page (frontend, t_4433cff2):
- app/keystore.py: api_keys table (self-creating), FIRMS/GEMINI/TELEGRAM
  registry with format validation, ****last4 masking, get_api_key()
- GET/POST/DELETE /api/keys (never returns full values); Keys tab in index.html

DB_NULL_POOL env switch in app/database.py enables a NullPool for tests /
short-lived processes that open a fresh event loop per unit.
2026-08-24 15:37:42 -04:00