All checks were successful
build-and-deploy / build (push) Successful in 2m32s
The Vessels layer now retunes the server-side AISStream subscription to the client viewport instead of a static AISSTREAM_BBOX. The frontend POSTs its quantized viewport box to /api/vessels/subscribe on moveend; the ais_stream worker coalesces and applies it at the service's 1 subscription/s cap, then last-known positions for the new area arrive within a couple of seconds (the frontend does one follow-up fetch after retuning). Bounds the in-memory vessel store across regions. Key stays server-side.
312 lines
9 KiB
Python
312 lines
9 KiB
Python
"""OSINT Dashboard — Pydantic schemas."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# ─── Enums ───────────────────────────────────────────────────────────────
|
|
|
|
class SourceType(str, Enum):
|
|
rss = "rss"
|
|
gdel_t2 = "gdel-t2"
|
|
social = "social"
|
|
earthquake = "earthquake"
|
|
disaster = "disaster"
|
|
weather = "weather"
|
|
fire = "fire"
|
|
satellite = "satellite"
|
|
camera = "camera"
|
|
|
|
|
|
class EntityKind(str, Enum):
|
|
person = "person"
|
|
organization = "organization"
|
|
location = "location"
|
|
topic = "topic"
|
|
asset = "asset"
|
|
|
|
|
|
class Sentiment(str, Enum):
|
|
positive = "positive"
|
|
neutral = "neutral"
|
|
negative = "negative"
|
|
|
|
|
|
class AlertType(str, Enum):
|
|
entity_mention = "entity_mention"
|
|
sentiment_shift = "sentiment_shift"
|
|
geo_proximity = "geo_proximity"
|
|
keyword_match = "keyword_match"
|
|
threshold = "threshold"
|
|
anomaly = "anomaly"
|
|
|
|
|
|
class AlertSeverity(str, Enum):
|
|
low = "low"
|
|
medium = "medium"
|
|
high = "high"
|
|
critical = "critical"
|
|
|
|
|
|
# ─── Feed Sources ───────────────────────────────────────────────────────
|
|
|
|
class FeedSourceCreate(BaseModel):
|
|
name: str
|
|
source_type: SourceType
|
|
url: Optional[str] = None
|
|
config: Optional[dict] = None
|
|
|
|
|
|
class FeedSourceOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
source_type: SourceType
|
|
url: Optional[str]
|
|
config: Optional[dict]
|
|
enabled: bool
|
|
created_at: datetime
|
|
|
|
|
|
# ─── Events ─────────────────────────────────────────────────────────────
|
|
|
|
class EventCreate(BaseModel):
|
|
source_type: SourceType
|
|
source_id: Optional[UUID] = None
|
|
title: Optional[str] = None
|
|
body: Optional[str] = None
|
|
url: Optional[str] = None
|
|
sentiment_score: Optional[float] = None
|
|
sentiment_label: Optional[Sentiment] = None
|
|
location_lat: Optional[float] = None
|
|
location_lon: Optional[float] = None
|
|
location_name: Optional[str] = None
|
|
entities: Optional[list[dict]] = None
|
|
tags: Optional[list[str]] = None
|
|
raw: Optional[dict] = None
|
|
source_timestamp: Optional[datetime] = None
|
|
|
|
|
|
class EventOut(BaseModel):
|
|
id: UUID
|
|
source_type: SourceType
|
|
source_id: Optional[UUID]
|
|
title: Optional[str]
|
|
body: Optional[str]
|
|
url: Optional[str]
|
|
sentiment_score: Optional[float]
|
|
sentiment_label: Optional[Sentiment]
|
|
location_lat: Optional[float]
|
|
location_lon: Optional[float]
|
|
location_name: Optional[str]
|
|
entities: Optional[list[dict]]
|
|
tags: Optional[list[str]]
|
|
ingested_at: datetime
|
|
source_timestamp: datetime
|
|
|
|
|
|
# ─── Search ──────────────────────────────────────────────────────────────
|
|
|
|
class SearchQuery(BaseModel):
|
|
q: str = Field(..., min_length=1, max_length=500)
|
|
source_type: Optional[SourceType] = None
|
|
entity_id: Optional[UUID] = None
|
|
sentiment: Optional[Sentiment] = None
|
|
min_date: Optional[datetime] = None
|
|
max_date: Optional[datetime] = None
|
|
min_lat: Optional[float] = None
|
|
max_lat: Optional[float] = None
|
|
min_lon: Optional[float] = None
|
|
max_lon: Optional[float] = None
|
|
limit: int = Field(50, ge=1, le=500)
|
|
offset: int = Field(0, ge=0)
|
|
|
|
|
|
class SearchResult(BaseModel):
|
|
events: list[EventOut]
|
|
total: int
|
|
has_more: bool
|
|
|
|
|
|
# ─── Entities ────────────────────────────────────────────────────────────
|
|
|
|
class EntityCreate(BaseModel):
|
|
name: str
|
|
entity_type: EntityKind
|
|
aliases: Optional[list[str]] = None
|
|
description: Optional[str] = None
|
|
metadata: Optional[dict] = None
|
|
location_lat: Optional[float] = None
|
|
location_lon: Optional[float] = None
|
|
|
|
|
|
class EntityOut(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
entity_type: EntityKind
|
|
aliases: Optional[list[str]]
|
|
description: Optional[str]
|
|
metadata: Optional[dict]
|
|
location_lat: Optional[float]
|
|
location_lon: Optional[float]
|
|
event_count: int
|
|
first_seen: datetime
|
|
last_seen: datetime
|
|
|
|
|
|
# ─── Alerts ──────────────────────────────────────────────────────────────
|
|
|
|
class AlertCreate(BaseModel):
|
|
alert_type: AlertType
|
|
entity_id: Optional[UUID] = None
|
|
event_id: Optional[UUID] = None
|
|
severity: AlertSeverity
|
|
title: str
|
|
message: Optional[str] = None
|
|
context: Optional[dict] = None
|
|
|
|
|
|
class AlertUpdate(BaseModel):
|
|
acknowledged: Optional[bool] = None
|
|
acknowledged_by: Optional[str] = None
|
|
resolved_at: Optional[datetime] = None
|
|
|
|
|
|
class AlertOut(BaseModel):
|
|
id: UUID
|
|
alert_type: AlertType
|
|
entity_id: Optional[UUID]
|
|
event_id: Optional[UUID]
|
|
severity: AlertSeverity
|
|
title: str
|
|
message: Optional[str]
|
|
context: Optional[dict]
|
|
acknowledged: bool
|
|
acknowledged_by: Optional[str]
|
|
created_at: datetime
|
|
resolved_at: Optional[datetime]
|
|
|
|
|
|
# ─── Documents ───────────────────────────────────────────────────────────
|
|
|
|
class DocumentCreate(BaseModel):
|
|
bucket: str
|
|
object_key: str
|
|
content_type: Optional[str] = None
|
|
size_bytes: Optional[int] = None
|
|
description: Optional[str] = None
|
|
tags: Optional[list[str]] = None
|
|
event_id: Optional[UUID] = None
|
|
|
|
|
|
class DocumentOut(BaseModel):
|
|
id: UUID
|
|
bucket: str
|
|
object_key: str
|
|
content_type: Optional[str]
|
|
size_bytes: Optional[int]
|
|
description: Optional[str]
|
|
tags: Optional[list[str]]
|
|
event_id: Optional[UUID]
|
|
uploaded_at: datetime
|
|
|
|
|
|
# ─── API Keys ────────────────────────────────────────────────────────────
|
|
|
|
class KeyValueIn(BaseModel):
|
|
"""Body for POST /api/keys/{name} — the value to store.
|
|
|
|
The raw value is accepted only on write; it is never read back out.
|
|
"""
|
|
value: str = Field(..., min_length=1, max_length=4096)
|
|
|
|
|
|
class KeyOut(BaseModel):
|
|
"""One key's status as exposed by GET /api/keys.
|
|
|
|
``masked`` is the "****last4" hint; the full value is NEVER included.
|
|
"""
|
|
name: str
|
|
set: bool
|
|
masked: Optional[str] = None
|
|
description: Optional[str] = None
|
|
example: Optional[str] = None
|
|
validated: bool = True
|
|
|
|
|
|
# ─── Active fires / hotspots (NASA FIRMS) ────────────────────────────────
|
|
|
|
class FireOut(BaseModel):
|
|
latitude: float
|
|
longitude: float
|
|
brightness: float # bright_ti4 brightness temperature, Kelvin
|
|
confidence: str # VIIRS: n (nominal) / l (low) / h (high)
|
|
acq_time: datetime
|
|
satellite: str
|
|
instrument: Optional[str] = None
|
|
bright_ti5: Optional[float] = None # 12µm brightness temperature, Kelvin
|
|
frp: Optional[float] = None # fire radiative power, MW
|
|
daynight: Optional[str] = None # D / N
|
|
|
|
|
|
# ─── News pipeline (scraper + summarizer) ────────────────────────────────
|
|
|
|
class NewsArticleOut(BaseModel):
|
|
"""One scraped article as exposed by GET /api/news."""
|
|
|
|
id: int
|
|
title: Optional[str] = None
|
|
url: Optional[str] = None
|
|
content: Optional[str] = None
|
|
domain: Optional[str] = None
|
|
timestamp: Optional[datetime] = None
|
|
|
|
|
|
class NewsSummaryOut(BaseModel):
|
|
"""One master LLM summary as exposed by GET /api/news/summaries."""
|
|
|
|
id: int
|
|
summary_text: str
|
|
batch_timestamp: datetime
|
|
|
|
|
|
# ─── Aggregations ────────────────────────────────────────────────────────
|
|
|
|
class SentimentSummary(BaseModel):
|
|
period: str
|
|
positive_count: int
|
|
neutral_count: int
|
|
negative_count: int
|
|
avg_score: float
|
|
|
|
|
|
class TimelinePoint(BaseModel):
|
|
timestamp: datetime
|
|
event_count: int
|
|
avg_sentiment: float
|
|
|
|
|
|
class DashboardSummary(BaseModel):
|
|
total_events: int
|
|
events_last_24h: int
|
|
active_sources: int
|
|
open_alerts: int
|
|
tracked_entities: int
|
|
sentiment: SentimentSummary
|
|
top_entities: list[EntityOut]
|
|
|
|
|
|
class VesselBboxUpdate(BaseModel):
|
|
"""Retune the server-side AISStream subscription to a client viewport box.
|
|
|
|
``bbox`` is "minlon,minlat,maxlon,maxlat" (Leaflet order). ``None``/empty
|
|
resets to the env AISSTREAM_BBOX default.
|
|
"""
|
|
|
|
bbox: str | None = None
|
|
|