security: CORS hardening, path traversal fix, WebSocket auth + cleanup

- Restrict CORS to localhost origins (was allow_origins=[*])
- Require valid JWT on WebSocket /ws (anonymous no longer gets admin view)
- Fix path traversal in delete_cell(): resolve() + parent check
- Validate cell_id format in /charts/download-noaa/{cell_id}
- Exclude charts/ and Cartas/ from git (keep US1GC09M world overview)
- Add NOAA ENC Portal external link in charts catalog tab
- Untrack __pycache__/, .db, .claude/ session files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 12:45:43 -04:00
parent 3e04c4113f
commit cfd94f905a
47 changed files with 1847 additions and 427 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22 -3
View File
@@ -1,4 +1,4 @@
from sqlalchemy import Column, String, Float, Boolean, DateTime, Enum, Text
from sqlalchemy import Column, String, Float, Boolean, DateTime, Enum, Text, Integer
from sqlalchemy.sql import func
from database import Base
import enum
@@ -36,13 +36,32 @@ class Aid(Base):
# 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
fuente_posicion = Column(String, default="MANUAL") # S57 | MANUAL | AIS
# Link estable al feature S-57 (cuando el Aid fue creado desde una carta).
# cell_id + chart_feature_id permite reencontrar el feature aunque el ENC
# se reedite. Si no hay LNAM en el feature original, chart_feature_id es
# un fingerprint: tipo_obj + round(lat,6) + round(lon,6).
source_chart = Column(String, nullable=True) # 'S57' | 'MANUAL' | 'AIS'
cell_id = Column(String, nullable=True, index=True)
chart_feature_id = Column(String, nullable=True, index=True)
# Solo flotantes
radio_borneo_m = Column(Float, default=10.0)
radio_borneo_m = Column(Float, default=10.0)
# Per-aid displacement alert thresholds (override global config when set).
# Different buoys have different anchor chain lengths / acceptable drift.
displacement_warn_m = Column(Float, nullable=True) # None → use global setting
displacement_alarm_m = Column(Float, nullable=True) # None → use global setting
# Minutes without AIS signal before PERDIDA_SENAL alert fires.
# None = no signal-loss monitoring for this aid.
signal_loss_min = Column(Integer, nullable=True)
# Si tiene AIS
mmsi = Column(String, unique=True, nullable=True)
# Digital input function mapping (what each IN means for THIS buoy)
# Values: NULL | 'WATER_INGRESS_WARN' | 'WATER_INGRESS_CRITICAL' | 'LISTING'
din3_function = Column(String, nullable=True)
din4_function = Column(String, nullable=True)
# Posición actual (actualizada por AIS)
lat_actual = Column(Float, nullable=True)
+6
View File
@@ -18,6 +18,12 @@ class Lamp(Base):
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)
# Battery alert thresholds as % of usable voltage range.
# warn_pct=20 means: alert when remaining capacity ≤ 20% of (maxmin).
# Defaults match the original hardcoded values (20% / 10%).
# Override per lamp model — e.g. Sabik recommends 30%/15%.
warn_pct = Column(Float, default=20.0) # % of range → warning
alarm_pct = Column(Float, default=10.0) # % of range → alarm
notes = Column(Text, nullable=True)
creado_en = Column(DateTime, server_default=func.now())
modificado_en = Column(DateTime, onupdate=func.now())