v1-base: estado funcional al 2026-04-30 — buoys 3D, beacons 3D, enfilaciones, land-clip, buoy light merge

This commit is contained in:
2026-04-30 13:50:01 -04:00
commit 8edb425c24
7228 changed files with 112813 additions and 0 deletions
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+72
View File
@@ -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)
+48
View File
@@ -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())
+23
View File
@@ -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())
+23
View File
@@ -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='{}')
+48
View File
@@ -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)