diff --git a/app/main.py b/app/main.py
index 9d5f64d..d41f877 100644
--- a/app/main.py
+++ b/app/main.py
@@ -189,6 +189,21 @@ async def update_source(source_id: UUID, payload: dict):
@app.get("/api/events", response_model=list[EventOut])
async def list_events(
source_type: SourceType | None = Query(None),
+ bbox: str | None = Query(
+ None,
+ description="Comma-separated 'minlon,minlat,maxlon,maxlat' to bound the "
+ "result set by event coordinates. Omit for all stored events.",
+ ),
+ since: datetime | None = Query(
+ None,
+ description="Only events ingested at/after this UTC instant "
+ "(ISO 8601, e.g. '2026-08-24T12:00:00Z').",
+ ),
+ has_coords: bool = Query(
+ False,
+ description="Only events that carry a location (location_lat/lon set). "
+ "Used by the map's event-blips layer.",
+ ),
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
):
@@ -197,6 +212,28 @@ async def list_events(
stmt = select(events).order_by(events.c.ingested_at.desc())
if source_type:
stmt = stmt.where(events.c.source_type == source_type.value)
+ if since:
+ stmt = stmt.where(events.c.ingested_at >= since)
+ if has_coords:
+ stmt = stmt.where(events.c.location_lat.isnot(None))
+ if bbox:
+ parts = [p.strip() for p in bbox.split(",")]
+ if len(parts) != 4:
+ raise HTTPException(
+ 422, "bbox must be 'minlon,minlat,maxlon,maxlat' (4 comma-separated values)"
+ )
+ try:
+ minlon, minlat, maxlon, maxlat = (float(p) for p in parts)
+ except ValueError:
+ raise HTTPException(
+ 422, "bbox values must be floats: 'minlon,minlat,maxlon,maxlat'"
+ )
+ stmt = stmt.where(
+ and_(
+ events.c.location_lon >= minlon, events.c.location_lon <= maxlon,
+ events.c.location_lat >= minlat, events.c.location_lat <= maxlat,
+ )
+ )
stmt = stmt.limit(limit).offset(offset)
rows = (await session.execute(stmt)).mappings().all()
return [event_to_out(r) for r in rows]
diff --git a/app/models.py b/app/models.py
index 4b332a0..706b5a2 100644
--- a/app/models.py
+++ b/app/models.py
@@ -39,7 +39,7 @@ events = Table(
Column("id", UUID(as_uuid=True), primary_key=True, default=uuid4),
Column("source_type", Enum(
"rss", "gdel-t2", "social", "earthquake", "disaster",
- "weather", "fire", "satellite", name="event_source_type"
+ "weather", "fire", "satellite", "camera", name="event_source_type"
), nullable=False, index=True),
Column("source_id", UUID(as_uuid=True)),
Column("title", Text),
diff --git a/app/schemas.py b/app/schemas.py
index 54d35c3..90d0f67 100644
--- a/app/schemas.py
+++ b/app/schemas.py
@@ -21,6 +21,7 @@ class SourceType(str, Enum):
weather = "weather"
fire = "fire"
satellite = "satellite"
+ camera = "camera"
class EntityKind(str, Enum):
diff --git a/app/static/index.html b/app/static/index.html
index 21f2088..986b34c 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -3,324 +3,612 @@
- OSINT Dashboard
+ OSINT//DASHBOARD — Global Situational Awareness Terminal
+
+
+
-
- OSINT Dashboard
-
- Status:
checking...
- | Last update:
-
+
+
+
+
+
◢◤
+
OSINT// DASHBOARD
+
SECURE TERMINAL INITIALIZING
+
+
+
+
+
+
-
-
-
-
-
Events (24h) -
last 24 hours
-
-
-
-
-
+
+
-
-
-
Search Events
-
-
- Search
-
-
-
-
-
- Recent Events
- Alerts
- Entities
- Ingest
- Keys
- Map
- News
-
-
-
-
-
Recent Events
-
- Time Source Title Sentiment Location
-
-
-
-
-
-
-
Open Alerts
-
- Time Severity Type Title
-
-
-
-
-
-
-
Tracked Entities
-
- Name Type Events Last Seen
-
-
-
-
-
-
-
Data Ingestion
-
-
-
RSS Feed
-
-
- Fetch
-
-
-
-
GDELT Articles
-
Global news monitoring
-
Fetch Latest
-
-
-
Earthquakes (USGS)
-
Last hour of seismic data
-
Fetch Latest
-
-
-
NATS Consumer
-
Process pending messages
-
Process
-
-
-
-
-
-
-
-
API Keys
-
Keys used by ingest services (FIRMS, Gemini, Telegram, ...). Values are stored in Postgres and never shown again — only set/missing status and the last 4 chars are displayed.
-
-
-
-
-
-
Global Satellite Map
-
NASA GIBS satellite basemap (the same tiles worldview.earthdata.nasa.gov renders). Pick a layer and date, then zoom/pan — fires and open cameras can be overlaid. Use the Layers panel to toggle feeds, adjust opacity, and filter fires by time.
+
+
-
+
+
+
-
◈ Layers ▼
+
+ ◈ Layers ▼
+
+
+
+ Event Blips
+ 0
+
+
+ Since
+
+ Last 6h
+ Last 24h
+ Last 48h
+ Last 7d
+ All time
+
+
+
+
Geolocated events from the ingest pipeline. Color = source type.
+
+
+
+
+ Weather Warnings FEED PENDING
+
+
+
+
+ Flights · ADS-B FEED PENDING
+
+
+
+
+ Vessels · AIS FEED PENDING
+
+
+
Fire heat intensity
@@ -369,50 +692,259 @@
-
-
-
-
-
-
-
Global News Feed
-
Scraped hourly from ~257 RSS sources · latest executive summary pinned above
-
-
-
—
-
Refresh
+
Initializing…
+
+
FIRMS —
+
CAMS —
+
BLIPS —
+
-
-
-
Executive Summary LATEST
-
—
-
-
No executive summary yet — the Gemini summarizer is idle until GEMINI_API_KEY is set on the Pi.
+
+
+
+
◉ Global News Brief
+
Scraped on the 15-minute cycle · latest LLM executive summary pinned above
+
+
+
+
Executive Summary WAITING
+
—
+
+
No executive summary yet — the Gemini summarizer is idle until GEMINI_API_KEY is set on the Pi.
+
+
+
+
+ Recent Headlines
+ 0 articles
+
+
+
-
-
- Recent Headlines
- 0 articles
-
-
-
+
+
+
+
≣ Event Stream
+
Ingested intel events across all sources
-
-
-
Search Results (0 )
-
- Time Source Title Sentiment Location
-
-
+
+
+
Events (24h) -
last 24 hours
+
+
+
+
+
+
+
+
Search Events
+
+
+ Search
+
+
+
+
+
Search Results (0 )
+
+ Time Source Title Sentiment Location
+
+
+
+
+
+
Recent Events
+
+ Time Source Title Sentiment Location
+
+
+
+
+
+
+
+
+
+
⚠ Open Alerts
+
Rule-triggered alerts from entity mentions, sentiment shifts, geo proximity & thresholds
+
+
+ Time Severity Type Title
+
+
+
+
+
+
+
+
+
+
◎ Tracked Entities
+
People, organizations, locations, topics & assets of interest
+
+
+ Name Type Events Last Seen
+
+
+
+
+
+
+
+
+
+
⇪ Data Ingestion
+
Manually trigger feed pulls & NATS processing
+
+
+
RSS Feed
+
+
+ Fetch
+
+
+
+
GDELT Articles
+
Global news monitoring
+
Fetch Latest
+
+
+
Earthquakes (USGS)
+
Last hour of seismic data
+
Fetch Latest
+
+
+
NATS Consumer
+
Process pending messages
+
Process
+
+
+
+
+
+
+
+
+
+
⚿ API Keys
+
Keys used by ingest services (FIRMS, Gemini, Telegram, …). Stored in Postgres, never shown again — only status + last 4 chars.
+
+
+
+
+
+
+
+
⚙ Settings
+
Terminal preferences — stored locally in this browser
+
+
+
Market Data Stream
+
Space reserved for the stock / futures ticker. The backend feed is not wired yet — when GET /api/market returns {"symbols": [{"symbol","price","change","change_pct","updated_at"}]}, the bottom ticker activates automatically.
+
+ Tracked symbols (comma separated)
+
+
+
+ Save symbols
+ Reset
+
+
+
+
+
Map Defaults
+
Applied to the live map on load.
+
+ Fires · since
+
+ All time
+ Last 6h
+ Last 24h
+ Last 48h
+ Last 7d
+ Last 30d
+
+
+
+ Event blips · since
+
+ Last 6h
+ Last 24h
+ Last 48h
+ Last 7d
+ All time
+
+
+
+
+ Save defaults
+ Reset
+
+
+
+
+
System
+
+
API status —
+
Total events —
+
Active sources —
+
Open alerts —
+
Tracked entities —
+
News cycle 15 min
+
Market feed STANDBY
+
+
+ Refresh
+
+
+
+
+
+
+
+
+
+
+
● NEWS
+
+
15-MIN CYCLE
+
+
@@ -420,10 +952,321 @@