| 1 | """All ORM models for Sightline. Kept in one module to keep relationships coherent |
| 2 | and avoid circular imports in a scaffold of this size.""" |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import uuid |
| 6 | from datetime import datetime |
| 7 | |
| 8 | from sqlalchemy import ( |
| 9 | BigInteger, Boolean, CheckConstraint, Date, DateTime, Float, ForeignKey, |
| 10 | Index, Integer, JSON, LargeBinary, Numeric, String, Text, UniqueConstraint, func, |
| 11 | ) |
| 12 | from sqlalchemy.orm import Mapped, mapped_column, relationship |
| 13 | |
| 14 | from app.db.base import Base, TimestampMixin |
| 15 | |
| 16 | |
| 17 | def _uuid() -> str: |
| 18 | return str(uuid.uuid4()) |
| 19 | |
| 20 | |
| 21 | class Role(Base, TimestampMixin): |
| 22 | __tablename__ = "roles" |
| 23 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 24 | name: Mapped[str] = mapped_column(String, unique=True, nullable=False) |
| 25 | permissions: Mapped[dict] = mapped_column(JSON, default=dict) |
| 26 | |
| 27 | |
| 28 | class User(Base, TimestampMixin): |
| 29 | __tablename__ = "users" |
| 30 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 31 | email: Mapped[str] = mapped_column(String, unique=True, nullable=False, index=True) |
| 32 | hashed_password: Mapped[str] = mapped_column(String, nullable=False) |
| 33 | full_name: Mapped[str | None] = mapped_column(String) |
| 34 | role: Mapped[str] = mapped_column(String, default="analyst", nullable=False) |
| 35 | is_active: Mapped[bool] = mapped_column(Boolean, default=True) |
| 36 | |
| 37 | |
| 38 | class ApiKey(Base, TimestampMixin): |
| 39 | __tablename__ = "api_keys" |
| 40 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 41 | user_id: Mapped[str] = mapped_column(ForeignKey("users.id")) |
| 42 | name: Mapped[str] = mapped_column(String) |
| 43 | key_hash: Mapped[str] = mapped_column(String, index=True) |
| 44 | scopes: Mapped[dict] = mapped_column(JSON, default=dict) |
| 45 | last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 46 | revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 47 | |
| 48 | |
| 49 | class AuditLog(Base): |
| 50 | __tablename__ = "audit_logs" |
| 51 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 52 | actor_user_id: Mapped[str | None] = mapped_column(String, index=True) |
| 53 | action: Mapped[str] = mapped_column(String, nullable=False) |
| 54 | entity_type: Mapped[str | None] = mapped_column(String) |
| 55 | entity_id: Mapped[str | None] = mapped_column(String) |
| 56 | ip: Mapped[str | None] = mapped_column(String) |
| 57 | meta: Mapped[dict] = mapped_column("metadata", JSON, default=dict) |
| 58 | created_at: Mapped[datetime] = mapped_column( |
| 59 | DateTime(timezone=True), server_default=func.now(), index=True |
| 60 | ) |
| 61 | __table_args__ = (Index("ix_audit_entity", "entity_type", "entity_id"),) |
| 62 | |
| 63 | |
| 64 | class Source(Base, TimestampMixin): |
| 65 | __tablename__ = "sources" |
| 66 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 67 | name: Mapped[str] = mapped_column(String, nullable=False) |
| 68 | system_type: Mapped[str | None] = mapped_column(String) |
| 69 | organization: Mapped[str | None] = mapped_column(String) |
| 70 | jurisdiction: Mapped[str | None] = mapped_column(String) |
| 71 | trust_level: Mapped[float] = mapped_column(Float, default=0.8) |
| 72 | |
| 73 | |
| 74 | class Import(Base, TimestampMixin): |
| 75 | __tablename__ = "imports" |
| 76 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 77 | source_id: Mapped[str | None] = mapped_column(ForeignKey("sources.id")) |
| 78 | filename: Mapped[str] = mapped_column(String) |
| 79 | content_type: Mapped[str | None] = mapped_column(String) |
| 80 | sha256: Mapped[str] = mapped_column(String, unique=True, index=True) |
| 81 | raw_blob_ref: Mapped[str | None] = mapped_column(String) |
| 82 | row_count: Mapped[int] = mapped_column(Integer, default=0) |
| 83 | status: Mapped[str] = mapped_column(String, default="received") |
| 84 | error: Mapped[str | None] = mapped_column(Text) |
| 85 | created_by: Mapped[str | None] = mapped_column(String) |
| 86 | |
| 87 | |
| 88 | class Location(Base, TimestampMixin): |
| 89 | __tablename__ = "locations" |
| 90 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 91 | label: Mapped[str | None] = mapped_column(String) |
| 92 | address: Mapped[str | None] = mapped_column(String) |
| 93 | lat: Mapped[float | None] = mapped_column(Float) |
| 94 | lon: Mapped[float | None] = mapped_column(Float) |
| 95 | jurisdiction: Mapped[str | None] = mapped_column(String) |
| 96 | __table_args__ = (Index("ix_locations_latlon", "lat", "lon"),) |
| 97 | |
| 98 | |
| 99 | class CameraGroup(Base, TimestampMixin): |
| 100 | __tablename__ = "camera_groups" |
| 101 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 102 | name: Mapped[str] = mapped_column(String, nullable=False) |
| 103 | organization: Mapped[str | None] = mapped_column(String) |
| 104 | description: Mapped[str | None] = mapped_column(Text) |
| 105 | |
| 106 | |
| 107 | class Camera(Base, TimestampMixin): |
| 108 | __tablename__ = "cameras" |
| 109 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 110 | source_id: Mapped[str | None] = mapped_column(ForeignKey("sources.id")) |
| 111 | external_camera_id: Mapped[str] = mapped_column(String, index=True) |
| 112 | name: Mapped[str | None] = mapped_column(String) |
| 113 | direction_facing: Mapped[str | None] = mapped_column(String) |
| 114 | operator: Mapped[str | None] = mapped_column(String, index=True) |
| 115 | manufacturer: Mapped[str | None] = mapped_column(String) |
| 116 | zone: Mapped[str | None] = mapped_column(String) |
| 117 | state: Mapped[str | None] = mapped_column(String, index=True) |
| 118 | osm_id: Mapped[str | None] = mapped_column(String) |
| 119 | image_url: Mapped[str | None] = mapped_column(String) |
| 120 | has_live_feed: Mapped[bool] = mapped_column(Boolean, default=False, index=True) |
| 121 | live_source: Mapped[str | None] = mapped_column(String) |
| 122 | area: Mapped[str | None] = mapped_column(String) |
| 123 | location_id: Mapped[str | None] = mapped_column(ForeignKey("locations.id")) |
| 124 | camera_group_id: Mapped[str | None] = mapped_column(ForeignKey("camera_groups.id")) |
| 125 | meta: Mapped[dict] = mapped_column("metadata", JSON, default=dict) |
| 126 | location: Mapped[Location | None] = relationship() |
| 127 | __table_args__ = (UniqueConstraint("source_id", "external_camera_id", name="uq_camera_source_extid"),) |
| 128 | |
| 129 | |
| 130 | class LicensePlate(Base, TimestampMixin): |
| 131 | __tablename__ = "license_plates" |
| 132 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 133 | plate_raw: Mapped[str] = mapped_column(String) |
| 134 | plate_norm: Mapped[str] = mapped_column(String, index=True, nullable=False) |
| 135 | state: Mapped[str | None] = mapped_column(String) |
| 136 | plate_type: Mapped[str | None] = mapped_column(String) |
| 137 | __table_args__ = (UniqueConstraint("plate_norm", "state", name="uq_plate_norm_state"),) |
| 138 | |
| 139 | |
| 140 | class Vehicle(Base, TimestampMixin): |
| 141 | __tablename__ = "vehicles" |
| 142 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 143 | primary_plate_id: Mapped[str | None] = mapped_column(ForeignKey("license_plates.id")) |
| 144 | make: Mapped[str | None] = mapped_column(String) |
| 145 | model: Mapped[str | None] = mapped_column(String) |
| 146 | color: Mapped[str | None] = mapped_column(String) |
| 147 | body_type: Mapped[str | None] = mapped_column(String) |
| 148 | year: Mapped[int | None] = mapped_column(Integer) |
| 149 | notes: Mapped[str | None] = mapped_column(Text) |
| 150 | possible_duplicate_of: Mapped[str | None] = mapped_column(ForeignKey("vehicles.id")) |
| 151 | |
| 152 | |
| 153 | class PlateRead(Base): |
| 154 | __tablename__ = "plate_reads" |
| 155 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 156 | import_id: Mapped[str | None] = mapped_column(ForeignKey("imports.id")) |
| 157 | source_id: Mapped[str | None] = mapped_column(ForeignKey("sources.id")) |
| 158 | camera_id: Mapped[str | None] = mapped_column(ForeignKey("cameras.id"), index=True) |
| 159 | plate_id: Mapped[str | None] = mapped_column(ForeignKey("license_plates.id")) |
| 160 | plate_norm: Mapped[str] = mapped_column(String, index=True, nullable=False) |
| 161 | read_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True, nullable=False) |
| 162 | direction: Mapped[str | None] = mapped_column(String) |
| 163 | confidence: Mapped[float | None] = mapped_column(Numeric(4, 3)) |
| 164 | lane: Mapped[str | None] = mapped_column(String) |
| 165 | lat: Mapped[float | None] = mapped_column(Float) |
| 166 | lon: Mapped[float | None] = mapped_column(Float) |
| 167 | hotlist_hit: Mapped[bool] = mapped_column(Boolean, default=False) |
| 168 | incident_ref: Mapped[str | None] = mapped_column(String) |
| 169 | burst_count: Mapped[int] = mapped_column(Integer, default=1) |
| 170 | raw_read: Mapped[dict] = mapped_column(JSON, default=dict) |
| 171 | dedup_hash: Mapped[bytes] = mapped_column(LargeBinary, unique=True) |
| 172 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 173 | __table_args__ = (Index("ix_reads_ts_camera", "read_ts", "camera_id"),) |
| 174 | |
| 175 | |
| 176 | class Sighting(Base): |
| 177 | __tablename__ = "sightings" |
| 178 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 179 | plate_read_id: Mapped[int | None] = mapped_column(ForeignKey("plate_reads.id")) |
| 180 | vehicle_id: Mapped[str | None] = mapped_column(ForeignKey("vehicles.id"), index=True) |
| 181 | camera_id: Mapped[str | None] = mapped_column(ForeignKey("cameras.id")) |
| 182 | location_id: Mapped[str | None] = mapped_column(ForeignKey("locations.id")) |
| 183 | investigation_id: Mapped[str | None] = mapped_column(ForeignKey("investigations.id"), index=True) |
| 184 | read_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) |
| 185 | confidence: Mapped[float | None] = mapped_column(Numeric(4, 3)) |
| 186 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 187 | __table_args__ = (Index("ix_sightings_vehicle_ts", "vehicle_id", "read_ts"),) |
| 188 | |
| 189 | |
| 190 | class Person(Base, TimestampMixin): |
| 191 | __tablename__ = "persons" |
| 192 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 193 | full_name: Mapped[str] = mapped_column(String, index=True, nullable=False) |
| 194 | aliases: Mapped[list | None] = mapped_column(JSON, default=list) |
| 195 | dob: Mapped[str | None] = mapped_column(String) |
| 196 | sex: Mapped[str | None] = mapped_column(String) |
| 197 | height_cm: Mapped[int | None] = mapped_column(Integer) |
| 198 | build: Mapped[str | None] = mapped_column(String) |
| 199 | hair: Mapped[str | None] = mapped_column(String) |
| 200 | eyes: Mapped[str | None] = mapped_column(String) |
| 201 | distinguishing_marks: Mapped[str | None] = mapped_column(Text) |
| 202 | associated_plate: Mapped[str | None] = mapped_column(String, index=True) |
| 203 | associated_plates: Mapped[list | None] = mapped_column(JSON, default=list) |
| 204 | threat_level: Mapped[str] = mapped_column(String, default="none") |
| 205 | wanted: Mapped[bool] = mapped_column(Boolean, default=False) |
| 206 | consented: Mapped[bool] = mapped_column(Boolean, default=False, index=True) |
| 207 | notes: Mapped[str | None] = mapped_column(Text) |
| 208 | |
| 209 | |
| 210 | class AppSetting(Base): |
| 211 | """Mutable runtime settings (singleton-ish key/value). Used for the profile-engine scope |
| 212 | switch so an operator can lock the dossier builder to consented subjects only.""" |
| 213 | __tablename__ = "app_settings" |
| 214 | key: Mapped[str] = mapped_column(String, primary_key=True) |
| 215 | value: Mapped[dict] = mapped_column(JSON, default=dict) |
| 216 | |
| 217 | |
| 218 | class Detection(Base): |
| 219 | """A person/occupant observation at a camera, carrying soft-biometric and clothing |
| 220 | descriptors - supports search by name, outfit, or article of clothing. (In a real deployment |
| 221 | these come from video analytics; in the POC they are synthetic but realistically structured.)""" |
| 222 | __tablename__ = "detections" |
| 223 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 224 | person_id: Mapped[str | None] = mapped_column(ForeignKey("persons.id"), index=True) |
| 225 | camera_id: Mapped[str | None] = mapped_column(ForeignKey("cameras.id"), index=True) |
| 226 | sighting_id: Mapped[int | None] = mapped_column(ForeignKey("sightings.id")) |
| 227 | read_ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) |
| 228 | plate_norm: Mapped[str | None] = mapped_column(String, index=True) |
| 229 | occupant_role: Mapped[str | None] = mapped_column(String) |
| 230 | clothing_top: Mapped[str | None] = mapped_column(String, index=True) |
| 231 | clothing_bottom: Mapped[str | None] = mapped_column(String) |
| 232 | articles: Mapped[list | None] = mapped_column(JSON, default=list) |
| 233 | description: Mapped[str | None] = mapped_column(Text) |
| 234 | confidence: Mapped[float | None] = mapped_column(Numeric(4, 3)) |
| 235 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 236 | |
| 237 | |
| 238 | class Investigation(Base, TimestampMixin): |
| 239 | __tablename__ = "investigations" |
| 240 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 241 | title: Mapped[str] = mapped_column(String, nullable=False) |
| 242 | status: Mapped[str] = mapped_column(String, default="open") |
| 243 | priority: Mapped[str] = mapped_column(String, default="normal") |
| 244 | summary: Mapped[str | None] = mapped_column(Text) |
| 245 | lead_user_id: Mapped[str | None] = mapped_column(String) |
| 246 | organization: Mapped[str | None] = mapped_column(String) |
| 247 | |
| 248 | |
| 249 | class Target(Base, TimestampMixin): |
| 250 | __tablename__ = "targets" |
| 251 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 252 | investigation_id: Mapped[str] = mapped_column(ForeignKey("investigations.id")) |
| 253 | kind: Mapped[str] = mapped_column(String) |
| 254 | label: Mapped[str] = mapped_column(String) |
| 255 | plate_id: Mapped[str | None] = mapped_column(ForeignKey("license_plates.id")) |
| 256 | vehicle_id: Mapped[str | None] = mapped_column(ForeignKey("vehicles.id")) |
| 257 | osint_entity_id: Mapped[str | None] = mapped_column(ForeignKey("osint_entities.id")) |
| 258 | notes: Mapped[str | None] = mapped_column(Text) |
| 259 | |
| 260 | |
| 261 | class Incident(Base, TimestampMixin): |
| 262 | __tablename__ = "incidents" |
| 263 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 264 | external_ref: Mapped[str | None] = mapped_column(String) |
| 265 | title: Mapped[str] = mapped_column(String) |
| 266 | kind: Mapped[str | None] = mapped_column(String) |
| 267 | occurred_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 268 | location_id: Mapped[str | None] = mapped_column(ForeignKey("locations.id")) |
| 269 | description: Mapped[str | None] = mapped_column(Text) |
| 270 | |
| 271 | |
| 272 | class Note(Base, TimestampMixin): |
| 273 | __tablename__ = "notes" |
| 274 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 275 | investigation_id: Mapped[str] = mapped_column(ForeignKey("investigations.id")) |
| 276 | author_id: Mapped[str | None] = mapped_column(String) |
| 277 | body: Mapped[str] = mapped_column(Text) |
| 278 | pinned: Mapped[bool] = mapped_column(Boolean, default=False) |
| 279 | |
| 280 | |
| 281 | class Tag(Base, TimestampMixin): |
| 282 | __tablename__ = "tags" |
| 283 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 284 | name: Mapped[str] = mapped_column(String, unique=True) |
| 285 | color: Mapped[str | None] = mapped_column(String) |
| 286 | |
| 287 | |
| 288 | class Tagging(Base): |
| 289 | __tablename__ = "taggings" |
| 290 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 291 | tag_id: Mapped[str] = mapped_column(ForeignKey("tags.id")) |
| 292 | entity_type: Mapped[str] = mapped_column(String) |
| 293 | entity_id: Mapped[str] = mapped_column(String) |
| 294 | __table_args__ = (Index("ix_taggings_entity", "entity_type", "entity_id"),) |
| 295 | |
| 296 | |
| 297 | class EvidenceFile(Base, TimestampMixin): |
| 298 | __tablename__ = "evidence_files" |
| 299 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 300 | investigation_id: Mapped[str] = mapped_column(ForeignKey("investigations.id")) |
| 301 | filename: Mapped[str] = mapped_column(String) |
| 302 | content_type: Mapped[str | None] = mapped_column(String) |
| 303 | size_bytes: Mapped[int] = mapped_column(Integer, default=0) |
| 304 | sha256: Mapped[str] = mapped_column(String, index=True) |
| 305 | blob_ref: Mapped[str | None] = mapped_column(String) |
| 306 | source_id: Mapped[str | None] = mapped_column(ForeignKey("sources.id")) |
| 307 | description: Mapped[str | None] = mapped_column(Text) |
| 308 | uploaded_by: Mapped[str | None] = mapped_column(String) |
| 309 | |
| 310 | |
| 311 | class Watchlist(Base, TimestampMixin): |
| 312 | __tablename__ = "watchlists" |
| 313 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 314 | name: Mapped[str] = mapped_column(String, nullable=False) |
| 315 | organization: Mapped[str | None] = mapped_column(String) |
| 316 | kind: Mapped[str] = mapped_column(String, default="plate") |
| 317 | active: Mapped[bool] = mapped_column(Boolean, default=True) |
| 318 | created_by: Mapped[str | None] = mapped_column(String) |
| 319 | |
| 320 | |
| 321 | class WatchlistEntry(Base, TimestampMixin): |
| 322 | __tablename__ = "watchlist_entries" |
| 323 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 324 | watchlist_id: Mapped[str] = mapped_column(ForeignKey("watchlists.id")) |
| 325 | plate_norm: Mapped[str | None] = mapped_column(String, index=True) |
| 326 | partial_plate: Mapped[str | None] = mapped_column(String) |
| 327 | vehicle_desc: Mapped[dict | None] = mapped_column(JSON) |
| 328 | reason: Mapped[str | None] = mapped_column(Text) |
| 329 | expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 330 | |
| 331 | |
| 332 | class WatchlistHit(Base): |
| 333 | __tablename__ = "watchlist_hits" |
| 334 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 335 | watchlist_entry_id: Mapped[str] = mapped_column(ForeignKey("watchlist_entries.id")) |
| 336 | plate_read_id: Mapped[int | None] = mapped_column(ForeignKey("plate_reads.id")) |
| 337 | sighting_id: Mapped[int | None] = mapped_column(ForeignKey("sightings.id")) |
| 338 | matched_on: Mapped[str] = mapped_column(String) |
| 339 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) |
| 340 | |
| 341 | |
| 342 | class AlertRule(Base, TimestampMixin): |
| 343 | __tablename__ = "alert_rules" |
| 344 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 345 | watchlist_id: Mapped[str | None] = mapped_column(ForeignKey("watchlists.id")) |
| 346 | condition: Mapped[dict] = mapped_column(JSON, default=dict) |
| 347 | channel: Mapped[str] = mapped_column(String, default="in_app") |
| 348 | throttle_seconds: Mapped[int] = mapped_column(Integer, default=0) |
| 349 | active: Mapped[bool] = mapped_column(Boolean, default=True) |
| 350 | |
| 351 | |
| 352 | class Alert(Base): |
| 353 | __tablename__ = "alerts" |
| 354 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 355 | alert_rule_id: Mapped[str | None] = mapped_column(ForeignKey("alert_rules.id")) |
| 356 | watchlist_hit_id: Mapped[int | None] = mapped_column(ForeignKey("watchlist_hits.id")) |
| 357 | status: Mapped[str] = mapped_column(String, default="new") |
| 358 | payload: Mapped[dict] = mapped_column(JSON, default=dict) |
| 359 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True) |
| 360 | |
| 361 | |
| 362 | class OsintEntity(Base, TimestampMixin): |
| 363 | __tablename__ = "osint_entities" |
| 364 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 365 | kind: Mapped[str] = mapped_column(String) |
| 366 | value: Mapped[str] = mapped_column(String) |
| 367 | attributes: Mapped[dict] = mapped_column(JSON, default=dict) |
| 368 | first_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 369 | last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 370 | __table_args__ = (UniqueConstraint("kind", "value", name="uq_osint_kind_value"),) |
| 371 | |
| 372 | |
| 373 | class EntityRelationship(Base): |
| 374 | __tablename__ = "entity_relationships" |
| 375 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 376 | src_type: Mapped[str] = mapped_column(String) |
| 377 | src_id: Mapped[str] = mapped_column(String) |
| 378 | dst_type: Mapped[str] = mapped_column(String) |
| 379 | dst_id: Mapped[str] = mapped_column(String) |
| 380 | relation: Mapped[str] = mapped_column(String) |
| 381 | weight: Mapped[float] = mapped_column(Float, default=1.0) |
| 382 | confidence: Mapped[float | None] = mapped_column(Float) |
| 383 | evidence_ref: Mapped[str | None] = mapped_column(String) |
| 384 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 385 | __table_args__ = ( |
| 386 | Index("ix_rel_src", "src_type", "src_id"), |
| 387 | Index("ix_rel_dst", "dst_type", "dst_id"), |
| 388 | ) |
| 389 | |
| 390 | |
| 391 | class EnrichmentJob(Base): |
| 392 | __tablename__ = "enrichment_jobs" |
| 393 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 394 | investigation_id: Mapped[str | None] = mapped_column(ForeignKey("investigations.id")) |
| 395 | enricher: Mapped[str] = mapped_column(String) |
| 396 | target: Mapped[str] = mapped_column(String) |
| 397 | status: Mapped[str] = mapped_column(String, default="pending") |
| 398 | result: Mapped[dict | None] = mapped_column(JSON) |
| 399 | confidence: Mapped[float | None] = mapped_column(Float) |
| 400 | raw_response_ref: Mapped[str | None] = mapped_column(String) |
| 401 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 402 | finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 403 | |
| 404 | |
| 405 | class SensorAsset(Base): |
| 406 | """A real, publicly-sourced fixed asset: ALPR/traffic/surveillance camera, weather station, |
| 407 | radar site, seismic station, stream gauge, air-quality monitor, etc. Sourced from public |
| 408 | APIs (OSM/DeFlock, NWS, USGS, ...) - every row is real and carries its provenance.""" |
| 409 | __tablename__ = "sensor_assets" |
| 410 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 411 | kind: Mapped[str] = mapped_column(String, index=True) |
| 412 | category: Mapped[str] = mapped_column(String, index=True) |
| 413 | source: Mapped[str] = mapped_column(String, index=True) |
| 414 | external_id: Mapped[str | None] = mapped_column(String, index=True) |
| 415 | name: Mapped[str | None] = mapped_column(String) |
| 416 | operator: Mapped[str | None] = mapped_column(String, index=True) |
| 417 | lat: Mapped[float | None] = mapped_column(Float) |
| 418 | lon: Mapped[float | None] = mapped_column(Float) |
| 419 | state: Mapped[str | None] = mapped_column(String, index=True) |
| 420 | attributes: Mapped[dict] = mapped_column(JSON, default=dict) |
| 421 | last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) |
| 422 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 423 | __table_args__ = (Index("ix_assets_kind_state", "kind", "state"), |
| 424 | Index("ix_assets_latlon", "lat", "lon")) |
| 425 | |
| 426 | |
| 427 | class LiveEntity(Base): |
| 428 | """Latest snapshot of a real moving public entity (aircraft via ADS-B, satellite via TLE, |
| 429 | vessel via AIS). Upserted by (source, identifier) as the live feed polls.""" |
| 430 | __tablename__ = "live_entities" |
| 431 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 432 | kind: Mapped[str] = mapped_column(String, index=True) |
| 433 | source: Mapped[str] = mapped_column(String, index=True) |
| 434 | identifier: Mapped[str] = mapped_column(String, index=True) |
| 435 | label: Mapped[str | None] = mapped_column(String) |
| 436 | lat: Mapped[float | None] = mapped_column(Float) |
| 437 | lon: Mapped[float | None] = mapped_column(Float) |
| 438 | altitude_m: Mapped[float | None] = mapped_column(Float) |
| 439 | heading: Mapped[float | None] = mapped_column(Float) |
| 440 | velocity: Mapped[float | None] = mapped_column(Float) |
| 441 | origin_country: Mapped[str | None] = mapped_column(String, index=True) |
| 442 | attributes: Mapped[dict] = mapped_column(JSON, default=dict) |
| 443 | observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) |
| 444 | __table_args__ = (UniqueConstraint("source", "identifier", name="uq_entity_source_ident"),) |
| 445 | |
| 446 | |
| 447 | class EntityPosition(Base): |
| 448 | """Append-only position history for moving entities - builds the track/trail for any entity.""" |
| 449 | __tablename__ = "entity_positions" |
| 450 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| 451 | identifier: Mapped[str] = mapped_column(String, index=True) |
| 452 | kind: Mapped[str] = mapped_column(String) |
| 453 | lat: Mapped[float] = mapped_column(Float) |
| 454 | lon: Mapped[float] = mapped_column(Float) |
| 455 | altitude_m: Mapped[float | None] = mapped_column(Float) |
| 456 | observed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) |
| 457 | __table_args__ = (Index("ix_pos_ident_ts", "identifier", "observed_at"),) |
| 458 | |
| 459 | |
| 460 | class Report(Base): |
| 461 | __tablename__ = "reports" |
| 462 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 463 | investigation_id: Mapped[str] = mapped_column(ForeignKey("investigations.id")) |
| 464 | title: Mapped[str] = mapped_column(String) |
| 465 | format: Mapped[str] = mapped_column(String, default="markdown") |
| 466 | sections: Mapped[dict] = mapped_column(JSON, default=dict) |
| 467 | rendered_ref: Mapped[str | None] = mapped_column(String) |
| 468 | created_by: Mapped[str | None] = mapped_column(String) |
| 469 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |
| 470 | |
| 471 | |
| 472 | class Export(Base): |
| 473 | __tablename__ = "exports" |
| 474 | id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid) |
| 475 | investigation_id: Mapped[str | None] = mapped_column(ForeignKey("investigations.id")) |
| 476 | kind: Mapped[str] = mapped_column(String) |
| 477 | format: Mapped[str] = mapped_column(String) |
| 478 | filters: Mapped[dict] = mapped_column(JSON, default=dict) |
| 479 | file_ref: Mapped[str | None] = mapped_column(String) |
| 480 | created_by: Mapped[str | None] = mapped_column(String) |
| 481 | created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) |