v1-base: estado funcional al 2026-04-30 — buoys 3D, beacons 3D, enfilaciones, land-clip, buoy light merge
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
from sqlalchemy import Column, String, Float, Boolean, DateTime, Enum, Text
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
import enum
|
||||
|
||||
class AidCategory(str, enum.Enum):
|
||||
FLOTANTE = "FLOTANTE"
|
||||
FIJA = "FIJA"
|
||||
|
||||
class AidType(str, enum.Enum):
|
||||
BOYA_LATERAL = "BOYA_LATERAL"
|
||||
BOYA_CARDINAL = "BOYA_CARDINAL"
|
||||
BOYA_PELIGRO = "BOYA_PELIGRO"
|
||||
BOYA_AGUAS_SEGURAS = "BOYA_AGUAS_SEGURAS"
|
||||
BOYA_ESPECIAL = "BOYA_ESPECIAL"
|
||||
FARO = "FARO"
|
||||
FAROL = "FAROL"
|
||||
BALIZA = "BALIZA"
|
||||
ENFILACION = "ENFILACION"
|
||||
|
||||
class AisType(str, enum.Enum):
|
||||
ATON_21 = "ATON_21"
|
||||
AIS_NORMAL = "AIS_NORMAL"
|
||||
SIN_AIS = "SIN_AIS"
|
||||
|
||||
class Aid(Base):
|
||||
__tablename__ = "aids"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
nombre = Column(String, nullable=False)
|
||||
numero_interno = Column(String)
|
||||
categoria = Column(String, nullable=False) # FLOTANTE | FIJA
|
||||
tipo = Column(String, nullable=False)
|
||||
tipo_ais = Column(String, default="SIN_AIS")
|
||||
|
||||
# Posición oficial (fuente de verdad)
|
||||
lat_nominal = Column(Float, nullable=False)
|
||||
lon_nominal = Column(Float, nullable=False)
|
||||
fuente_posicion = Column(String, default="MANUAL") # S57 | MANUAL
|
||||
|
||||
# Solo flotantes
|
||||
radio_borneo_m = Column(Float, default=10.0)
|
||||
|
||||
# Si tiene AIS
|
||||
mmsi = Column(String, unique=True, nullable=True)
|
||||
|
||||
# Posición actual (actualizada por AIS)
|
||||
lat_actual = Column(Float, nullable=True)
|
||||
lon_actual = Column(Float, nullable=True)
|
||||
desplazamiento_m = Column(Float, default=0.0)
|
||||
ultima_senal = Column(DateTime, nullable=True)
|
||||
|
||||
# Estado
|
||||
en_posicion = Column(Boolean, default=True)
|
||||
en_movimiento = Column(Boolean, default=False)
|
||||
activa = Column(Boolean, default=True)
|
||||
|
||||
# Características luminosas
|
||||
caracteristica_luz = Column(String, nullable=True)
|
||||
alcance_nm = Column(Float, nullable=True)
|
||||
lamp_id = Column(String, nullable=True) # FK → lamps.id (no enforced constraint to allow nullable)
|
||||
|
||||
# Responsabilidad
|
||||
puerto_responsable = Column(String, nullable=True) # ej: "Barranquilla", "Cartagena"
|
||||
empresa_responsable = Column(String, nullable=True) # empresa de señalización
|
||||
observaciones = Column(Text, nullable=True)
|
||||
|
||||
# Auditoría
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
modificado_en = Column(DateTime, onupdate=func.now())
|
||||
modificado_por = Column(String, nullable=True)
|
||||
motivo_cambio = Column(Text, nullable=True)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Contact catalog: people/offices that get notified when an alert is reported.
|
||||
|
||||
A Contact has a role (PORT_AUTHORITY or OWNER). When the user clicks REPORT
|
||||
on an alert, the backend resolves recipients by matching:
|
||||
- port_name == aid.puerto_responsable → port authorities
|
||||
- company_name == aid.empresa_responsable → owners
|
||||
"""
|
||||
from sqlalchemy import Column, String, DateTime, Text
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
|
||||
|
||||
class Contact(Base):
|
||||
__tablename__ = "contacts"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
name = Column(String, nullable=False)
|
||||
role = Column(String, nullable=False) # PORT_AUTHORITY | OWNER
|
||||
email = Column(String, nullable=True)
|
||||
phone = Column(String, nullable=True)
|
||||
whatsapp = Column(String, nullable=True) # E.164 e.g. +573001234567
|
||||
port_name = Column(String, nullable=True) # for PORT_AUTHORITY
|
||||
company_name = Column(String, nullable=True) # for OWNER
|
||||
preferred_channel = Column(String, default="EMAIL") # EMAIL | SMS | WHATSAPP
|
||||
notes = Column(Text, nullable=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
modificado_en = Column(DateTime, onupdate=func.now())
|
||||
|
||||
|
||||
class AlertReport(Base):
|
||||
"""Audit log: each REPORT click creates one row per channel/recipient."""
|
||||
__tablename__ = "alert_reports"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
alert_id = Column(String, nullable=True, index=True) # client-generated UUID
|
||||
alert_tipo = Column(String, nullable=True)
|
||||
alert_subtipo = Column(String, nullable=True)
|
||||
aid_id = Column(String, nullable=True)
|
||||
aid_nombre = Column(String, nullable=True)
|
||||
mmsi = Column(String, nullable=True)
|
||||
contact_id = Column(String, nullable=True)
|
||||
contact_name = Column(String, nullable=True)
|
||||
channel = Column(String, nullable=False) # EMAIL | SMS | WHATSAPP
|
||||
recipient = Column(String, nullable=False) # the actual address used
|
||||
message = Column(Text, nullable=True)
|
||||
reported_by = Column(String, nullable=False) # username
|
||||
reported_at = Column(DateTime, server_default=func.now())
|
||||
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Column, String, Float, Integer, Text, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
|
||||
|
||||
class Lamp(Base):
|
||||
"""Marine navigation lamp catalog. Aids reference a Lamp by id; the
|
||||
battery warning/alarm thresholds for each Aid are computed from the
|
||||
lamp's voltage_min / voltage_max:
|
||||
warn = voltage_min + (voltage_max - voltage_min) * 0.20
|
||||
alarm = voltage_min + (voltage_max - voltage_min) * 0.10
|
||||
"""
|
||||
__tablename__ = "lamps"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
manufacturer = Column(String, nullable=False)
|
||||
model = Column(String, nullable=False)
|
||||
lamp_count = Column(Integer, default=1)
|
||||
voltage_min = Column(Float, nullable=False) # discharged threshold (V)
|
||||
voltage_max = Column(Float, nullable=False) # fully-charged nominal (V)
|
||||
notes = Column(Text, nullable=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
modificado_en = Column(DateTime, onupdate=func.now())
|
||||
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, Enum, Text
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
import enum
|
||||
|
||||
class Role(str, enum.Enum):
|
||||
SUPERADMIN = "SUPERADMIN"
|
||||
ADMIN = "ADMIN"
|
||||
USER = "USER"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
username = Column(String, unique=True, nullable=False)
|
||||
nombre = Column(String, nullable=False)
|
||||
email = Column(String, unique=True, nullable=True)
|
||||
hashed_pw = Column(String, nullable=False)
|
||||
role = Column(String, default="USER")
|
||||
activo = Column(Boolean, default=True)
|
||||
creado_en = Column(DateTime, server_default=func.now())
|
||||
ultimo_login = Column(DateTime, nullable=True)
|
||||
prefs_json = Column(Text, nullable=True, default='{}')
|
||||
@@ -0,0 +1,48 @@
|
||||
from sqlalchemy import Column, String, Float, DateTime, Boolean, Integer
|
||||
from sqlalchemy.sql import func
|
||||
from database import Base
|
||||
|
||||
class Vessel(Base):
|
||||
__tablename__ = "vessels"
|
||||
|
||||
mmsi = Column(String, primary_key=True)
|
||||
nombre = Column(String, nullable=True)
|
||||
tipo = Column(Integer, nullable=True) # AIS ship type code
|
||||
bandera = Column(String, nullable=True)
|
||||
destino = Column(String, nullable=True)
|
||||
|
||||
# Posición actual
|
||||
lat = Column(Float, nullable=True)
|
||||
lon = Column(Float, nullable=True)
|
||||
sog = Column(Float, nullable=True) # speed over ground (knots)
|
||||
cog = Column(Float, nullable=True) # course over ground (degrees)
|
||||
heading = Column(Float, nullable=True)
|
||||
status = Column(Integer, nullable=True) # AIS nav status
|
||||
|
||||
ultima_senal = Column(DateTime, nullable=True)
|
||||
activo = Column(Boolean, default=True)
|
||||
|
||||
class VesselTrack(Base):
|
||||
__tablename__ = "vessel_tracks"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
mmsi = Column(String, nullable=False)
|
||||
timestamp = Column(DateTime, server_default=func.now())
|
||||
lat = Column(Float, nullable=False)
|
||||
lon = Column(Float, nullable=False)
|
||||
sog = Column(Float, nullable=True)
|
||||
cog = Column(Float, nullable=True)
|
||||
heading = Column(Float, nullable=True)
|
||||
evento_id = Column(String, nullable=True) # grabación asociada
|
||||
|
||||
class RecordingEvent(Base):
|
||||
__tablename__ = "recording_events"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
mmsi = Column(String, nullable=False)
|
||||
aid_id = Column(String, nullable=False)
|
||||
inicio = Column(DateTime, nullable=False)
|
||||
fin = Column(DateTime, nullable=True)
|
||||
distancia_min_m = Column(Float, nullable=True)
|
||||
trigger = Column(String) # PROXIMIDAD | PROYECCION
|
||||
cerrado = Column(Boolean, default=False)
|
||||
Reference in New Issue
Block a user