"""Vessel AIS models for live maritime tracking.""" from sqlalchemy import BigInteger, Column, DateTime, Float, Index, Integer, JSON, SmallInteger, String from sqlalchemy.sql import func from app.core.time import to_iso8601_utc from app.db.session import Base class VesselStatic(Base): """Slow-changing vessel identity and dimensions.""" __tablename__ = "vessel_static" mmsi = Column(BigInteger, primary_key=True) name = Column(String(128), nullable=True) callsign = Column(String(16), nullable=True) vessel_type = Column(SmallInteger, nullable=True, index=True) vessel_type_name = Column(String(64), nullable=True, index=True) flag = Column(String(4), nullable=True, index=True) length = Column(Float, nullable=True) width = Column(Float, nullable=True) draught = Column(Float, nullable=True) imo = Column(BigInteger, nullable=True) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) def to_dict(self) -> dict: return { "mmsi": self.mmsi, "name": self.name, "callsign": self.callsign, "vessel_type": self.vessel_type, "vessel_type_name": self.vessel_type_name, "flag": self.flag, "length": self.length, "width": self.width, "draught": self.draught, "imo": self.imo, "updated_at": to_iso8601_utc(self.updated_at), } class VesselPosition(Base): """Append-only AIS positions retained for short history windows.""" __tablename__ = "vessel_position" id = Column(Integer, primary_key=True, autoincrement=True) mmsi = Column(BigInteger, nullable=False, index=True) lat = Column(Float, nullable=False) lon = Column(Float, nullable=False) sog = Column(Float, nullable=True) cog = Column(Float, nullable=True) heading = Column(SmallInteger, nullable=True) nav_status = Column(SmallInteger, nullable=True, index=True) received_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) __table_args__ = ( Index("idx_vessel_pos_mmsi_time", "mmsi", "received_at"), Index("idx_vessel_pos_time", "received_at"), Index("idx_vessel_pos_lat_lon", "lat", "lon"), ) def to_dict(self) -> dict: return { "id": self.id, "mmsi": self.mmsi, "lat": self.lat, "lon": self.lon, "sog": self.sog, "cog": self.cog, "heading": self.heading, "nav_status": self.nav_status, "received_at": to_iso8601_utc(self.received_at), } class AISRawObservation(Base): """Source-level AIS fact before aggregation and conflict resolution.""" __tablename__ = "ais_raw_observations" id = Column(Integer, primary_key=True, autoincrement=True) target_schema = Column(String(64), nullable=False, default="vessel_ais", index=True) source = Column(String(100), nullable=False, index=True) entity_key = Column(String(64), nullable=False, index=True) delivery_mode = Column(String(32), nullable=False, index=True) transport = Column(String(32), nullable=False, index=True) message_type = Column(String(64), nullable=True, index=True) source_message_id = Column(String(128), nullable=True, index=True) observation_hash = Column(String(64), nullable=False, unique=True, index=True) observed_at = Column(DateTime(timezone=True), nullable=False, index=True) collected_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) normalized_payload = Column(JSON, default=dict) raw_payload = Column(JSON, default=dict) quality_flags = Column(JSON, default=list) __table_args__ = ( Index("idx_ais_raw_entity_observed", "target_schema", "entity_key", "observed_at"), Index("idx_ais_raw_schema_observed_entity", "target_schema", "observed_at", "entity_key"), Index("idx_ais_raw_source_entity", "source", "entity_key"), ) def to_dict(self) -> dict: return { "id": self.id, "target_schema": self.target_schema, "source": self.source, "entity_key": self.entity_key, "delivery_mode": self.delivery_mode, "transport": self.transport, "message_type": self.message_type, "source_message_id": self.source_message_id, "observation_hash": self.observation_hash, "observed_at": to_iso8601_utc(self.observed_at), "collected_at": to_iso8601_utc(self.collected_at), "normalized_payload": self.normalized_payload or {}, "raw_payload": self.raw_payload or {}, "quality_flags": self.quality_flags or [], } class AISConflictRecord(Base): """Recorded field-level disagreement between AIS sources.""" __tablename__ = "ais_conflict_records" id = Column(Integer, primary_key=True, autoincrement=True) target_schema = Column(String(64), nullable=False, default="vessel_ais", index=True) entity_key = Column(String(64), nullable=False, index=True) field = Column(String(64), nullable=False, index=True) candidates = Column(JSON, default=dict) selected_source = Column(String(100), nullable=True, index=True) selected_value = Column(JSON, nullable=True) selected_reason = Column(String(64), nullable=True, index=True) resolved_by = Column(String(32), nullable=False, default="system", index=True) status = Column(String(32), nullable=False, default="open", index=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now()) __table_args__ = ( Index("idx_ais_conflict_entity_field", "target_schema", "entity_key", "field"), ) def to_dict(self) -> dict: return { "id": self.id, "target_schema": self.target_schema, "entity_key": self.entity_key, "field": self.field, "candidates": self.candidates or {}, "selected_source": self.selected_source, "selected_value": self.selected_value, "selected_reason": self.selected_reason, "resolved_by": self.resolved_by, "status": self.status, "created_at": to_iso8601_utc(self.created_at), "updated_at": to_iso8601_utc(self.updated_at), } class AISSourceHealth(Base): """Runtime health signal for an AIS collector source.""" __tablename__ = "ais_source_health" source = Column(String(100), primary_key=True) connection_state = Column(String(32), nullable=False, default="disconnected", index=True) last_seen_at = Column(DateTime(timezone=True), nullable=True, index=True) last_success_at = Column(DateTime(timezone=True), nullable=True, index=True) last_error = Column(String(500), nullable=True) message_rate = Column(Float, nullable=True) lag_seconds = Column(Float, nullable=True) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), index=True) def to_dict(self) -> dict: return { "source": self.source, "connection_state": self.connection_state, "last_seen_at": to_iso8601_utc(self.last_seen_at), "last_success_at": to_iso8601_utc(self.last_success_at), "last_error": self.last_error, "message_rate": self.message_rate, "lag_seconds": self.lag_seconds, "updated_at": to_iso8601_utc(self.updated_at), }