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:
+52
-8
@@ -227,6 +227,26 @@ body {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Small select controls in the toolbar (trail window, vector mode/time) */
|
||||
.tb-select {
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 2px;
|
||||
font-size: 0.62rem;
|
||||
font-family: var(--sans);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 2px 4px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.tb-select:hover { border-color: var(--border-light); color: var(--text-primary); }
|
||||
.tb-select:focus { border-color: var(--accent); }
|
||||
.tb-select option { background: var(--bg-panel2); color: var(--text-primary); }
|
||||
|
||||
#map { flex: 1; }
|
||||
|
||||
#map-coords {
|
||||
@@ -413,7 +433,7 @@ body {
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 0.54rem;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
@@ -421,7 +441,7 @@ body {
|
||||
}
|
||||
|
||||
.field-value {
|
||||
font-size: 0.76rem;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
@@ -430,19 +450,19 @@ body {
|
||||
background: var(--bg-base);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
padding: 6px 10px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.88rem;
|
||||
color: var(--cyan);
|
||||
line-height: 1.35;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.coords-block .label {
|
||||
font-size: 0.54rem;
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1px;
|
||||
margin-bottom: 2px;
|
||||
font-family: var(--sans);
|
||||
}
|
||||
|
||||
@@ -1460,3 +1480,27 @@ html.night .ol-zoom button {
|
||||
}
|
||||
.aton-ok { color: var(--green); }
|
||||
.aton-warn { color: var(--yellow); font-weight: 600; }
|
||||
|
||||
/* ── Battery history chart ───────────────────────────────────────────────── */
|
||||
.batt-chart-hdr {
|
||||
display: flex; align-items: center;
|
||||
justify-content: space-between; margin-bottom: 6px;
|
||||
}
|
||||
.batt-range-btns { display: flex; gap: 4px; }
|
||||
.batt-rb {
|
||||
background: transparent; border: 1px solid var(--border);
|
||||
color: var(--text-secondary); border-radius: 2px;
|
||||
font-size: 0.6rem; font-family: var(--mono);
|
||||
padding: 2px 6px; cursor: pointer; transition: all .15s;
|
||||
}
|
||||
.batt-rb:hover { border-color: var(--border-light); color: var(--text-primary); }
|
||||
.batt-rb.active { background: var(--accent-dim); border-color: var(--accent); color: #fff; }
|
||||
#batt-chart-wrap { margin-bottom: 8px; }
|
||||
#batt-chart-svg { width: 100%; }
|
||||
.batt-stats {
|
||||
display: flex; flex-wrap: wrap; gap: 6px 12px;
|
||||
font-size: 0.68rem; color: var(--text-muted);
|
||||
margin-top: 4px; font-family: var(--mono);
|
||||
}
|
||||
.batt-stat { color: var(--text-secondary); }
|
||||
.batt-stat-eta { color: var(--text-secondary); }
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AidsMonitoring — Maritime Traffic System</title>
|
||||
<script>
|
||||
// Auto-scale UI to physical monitor resolution — same breakpoints as AR ECDIS.
|
||||
(function () {
|
||||
const w = window.screen.width;
|
||||
const z = w < 1366 ? 0.80 : w < 1600 ? 0.90 : w < 1920 ? 1.00 : 1.10;
|
||||
if (z !== 1.00) document.documentElement.style.zoom = z;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v9.2.4/ol.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
</head>
|
||||
@@ -144,6 +152,29 @@
|
||||
<button class="tb-btn active" id="toggle-lang">EN/ES</button>
|
||||
<div class="toolbar-sep"></div>
|
||||
<button class="tb-btn" id="btn-sdr" title="Launch AIS-catcher (RTL-SDR receiver)">SDR</button>
|
||||
<div class="toolbar-sep"></div>
|
||||
<span class="toolbar-label">AIS</span>
|
||||
<button class="tb-btn active" id="btn-trails" title="Show vessel past tracks (breadcrumb trail)">TRAILS</button>
|
||||
<select class="tb-select" id="trail-window" title="Trail history window">
|
||||
<option value="60000">1 min</option>
|
||||
<option value="120000">2 min</option>
|
||||
<option value="360000" selected>6 min</option>
|
||||
<option value="720000">12 min</option>
|
||||
<option value="1800000">30 min</option>
|
||||
<option value="0">ALL</option>
|
||||
</select>
|
||||
<div class="toolbar-sep"></div>
|
||||
<button class="tb-btn" id="btn-vectors" title="Show COG/SOG vectors">VECT</button>
|
||||
<select class="tb-select" id="vector-mode" title="Vector mode: True (COG absolute) or Relative (minus own ship)">
|
||||
<option value="true">TRUE</option>
|
||||
<option value="relative">RELAT</option>
|
||||
</select>
|
||||
<select class="tb-select" id="vector-time" title="Vector time ahead (minutes)">
|
||||
<option value="3">3 min</option>
|
||||
<option value="6" selected>6 min</option>
|
||||
<option value="12">12 min</option>
|
||||
<option value="20">20 min</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
<div id="map-coords" class="mono">LAT -- LON --</div>
|
||||
@@ -272,6 +303,11 @@
|
||||
<div id="tab-catalog" class="ctab-panel">
|
||||
<div style="font-size:0.7rem;color:var(--text-muted);margin-bottom:10px">
|
||||
Click DOWNLOAD to fetch directly from NOAA servers and install. No manual download needed.
|
||||
<a href="https://charts.coast.noaa.gov/ENCs/AllENCs.zip" target="_blank"
|
||||
style="color:var(--accent);text-decoration:none;margin-left:8px"
|
||||
title="Browse all NOAA ENCs on the NOAA Chart Portal">
|
||||
↗ NOAA ENC Portal
|
||||
</a>
|
||||
</div>
|
||||
<table class="chart-table" id="noaa-catalog-table">
|
||||
<thead><tr><th>Cell</th><th>Description</th><th>Status</th><th></th></tr></thead>
|
||||
|
||||
+81
-2
@@ -171,14 +171,31 @@ const Modal = {
|
||||
btn.textContent = 'SAVING...';
|
||||
status.textContent = '';
|
||||
|
||||
// MMSI: empty string → unassign (sends "" so backend nulls the column).
|
||||
// Non-empty → must be digits-only; backend enforces uniqueness across aids.
|
||||
const mmsiVal = v('ef-mmsi');
|
||||
if (mmsiVal && !/^\d{6,9}$/.test(mmsiVal)) {
|
||||
status.textContent = 'MMSI must be 6–9 digits, or blank to unassign.';
|
||||
status.className = 'save-status err';
|
||||
btn.disabled = false; btn.textContent = 'SAVE CHANGES';
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
puerto_responsable: v('ef-puerto') || null,
|
||||
empresa_responsable: v('ef-empresa') || null,
|
||||
caracteristica_luz: v('ef-luz') || null,
|
||||
alcance_nm: flt('ef-alcance'),
|
||||
radio_borneo_m: flt('ef-borneo'),
|
||||
observaciones: v('ef-obs') || null,
|
||||
lamp_id: v('ef-lamp') || null,
|
||||
displacement_warn_m: flt('ef-drift-warn'),
|
||||
displacement_alarm_m: flt('ef-drift-alarm'),
|
||||
signal_loss_min: flt('ef-signal-loss') ? parseInt(v('ef-signal-loss')) : null,
|
||||
din3_function: v('ef-din3') || null,
|
||||
din4_function: v('ef-din4') || null,
|
||||
observaciones: v('ef-obs') || null,
|
||||
lamp_id: v('ef-lamp') || null,
|
||||
mmsi: mmsiVal, // "" unassigns; backend handles
|
||||
tipo_ais: mmsiVal ? 'ATON_21' : 'SIN_AIS',
|
||||
modificado_por: Auth.session.nombre,
|
||||
motivo_cambio: motivo,
|
||||
};
|
||||
@@ -273,6 +290,24 @@ function buildEditForm(p) {
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
// AIS link — when set, AIS Type 21 with this MMSI updates the buoy's
|
||||
// lat_actual (ghost marker) and triggers drift/battery alerts using this
|
||||
// aid's per-buoy thresholds.
|
||||
const aisBlock = `
|
||||
<div class="modal-section-label">AIS LINK</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-muted);margin-bottom:6px">
|
||||
MMSI of the AtoN transponder on this buoy.
|
||||
Once set, the system listens for AIS Type 21 with this MMSI and shows
|
||||
the AIS-reported position as a transparent ghost over the nominal marker.
|
||||
Leave blank if the buoy has no AIS.
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">MMSI</label>
|
||||
<input class="form-input" id="ef-mmsi" type="text" inputmode="numeric"
|
||||
pattern="\\d{6,9}" maxlength="9"
|
||||
value="${p.mmsi || ''}" placeholder="e.g. 993001002">
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<div class="modal-section-label">GENERAL</div>
|
||||
|
||||
@@ -327,12 +362,56 @@ function buildEditForm(p) {
|
||||
value="${p.radio_borneo_m ?? 10}">
|
||||
</div>
|
||||
|
||||
<div class="modal-section-label">ALERT THRESHOLDS</div>
|
||||
<div style="font-size:0.68rem;color:var(--text-muted);margin-bottom:6px">
|
||||
Leave blank to use global settings. Override per buoy based on anchor chain length and operating area.
|
||||
</div>
|
||||
<div class="field-row-modal">
|
||||
<div class="form-field">
|
||||
<label class="form-label">Drift warn (m)</label>
|
||||
<input class="form-input" id="ef-drift-warn" type="number" step="1" min="1"
|
||||
value="${p.displacement_warn_m ?? ''}" placeholder="global">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Drift alarm (m)</label>
|
||||
<input class="form-input" id="ef-drift-alarm" type="number" step="1" min="1"
|
||||
value="${p.displacement_alarm_m ?? ''}" placeholder="global">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Signal loss (min)</label>
|
||||
<input class="form-input" id="ef-signal-loss" type="number" step="1" min="1"
|
||||
value="${p.signal_loss_min ?? ''}" placeholder="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row-modal">
|
||||
<div class="form-field">
|
||||
<label class="form-label">Digital IN3 function</label>
|
||||
<select class="form-input-select" id="ef-din3">
|
||||
<option value="">— Not connected —</option>
|
||||
<option value="WATER_INGRESS_WARN" ${p.din3_function==='WATER_INGRESS_WARN' ?'selected':''}>Water ingress (warning)</option>
|
||||
<option value="WATER_INGRESS_CRITICAL" ${p.din3_function==='WATER_INGRESS_CRITICAL' ?'selected':''}>Water ingress (critical)</option>
|
||||
<option value="LISTING" ${p.din3_function==='LISTING' ?'selected':''}>Listing / tilt sensor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Digital IN4 function</label>
|
||||
<select class="form-input-select" id="ef-din4">
|
||||
<option value="">— Not connected —</option>
|
||||
<option value="WATER_INGRESS_WARN" ${p.din4_function==='WATER_INGRESS_WARN' ?'selected':''}>Water ingress (warning)</option>
|
||||
<option value="WATER_INGRESS_CRITICAL" ${p.din4_function==='WATER_INGRESS_CRITICAL' ?'selected':''}>Water ingress (critical)</option>
|
||||
<option value="LISTING" ${p.din4_function==='LISTING' ?'selected':''}>Listing / tilt sensor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label">Observations</label>
|
||||
<textarea class="form-textarea" id="ef-obs"
|
||||
style="height:64px">${p.observaciones || ''}</textarea>
|
||||
</div>
|
||||
|
||||
${aisBlock}
|
||||
|
||||
${nominalBlock}
|
||||
|
||||
<div class="modal-section-label">AUDIT</div>
|
||||
|
||||
+810
-62
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -793,9 +793,11 @@ document.getElementById('btn-rec-search')?.addEventListener('click', async () =>
|
||||
// ── MODAL LAMP CATALOG ────────────────────────────────────────────────────
|
||||
window._lampCache = []; // exposed so the right panel can populate its dropdown
|
||||
|
||||
function _lampThresholds(vmin, vmax) {
|
||||
function _lampThresholds(vmin, vmax, warn_pct, alarm_pct) {
|
||||
const rng = vmax - vmin;
|
||||
return { warn: +(vmin + rng * 0.20).toFixed(3), alarm: +(vmin + rng * 0.10).toFixed(3) };
|
||||
const wp = ((warn_pct ?? 20) / 100);
|
||||
const ap = ((alarm_pct ?? 10) / 100);
|
||||
return { warn: +(vmin + rng * wp).toFixed(3), alarm: +(vmin + rng * ap).toFixed(3) };
|
||||
}
|
||||
|
||||
async function loadLamps() {
|
||||
@@ -844,20 +846,24 @@ window.editLamp = function(id) {
|
||||
<td><input class="form-input" id="lp-edit-count" type="number" value="${l.lamp_count}" style="width:60px"></td>
|
||||
<td><input class="form-input" id="lp-edit-vmin" type="number" step="0.1" value="${l.voltage_min}" style="width:70px"></td>
|
||||
<td><input class="form-input" id="lp-edit-vmax" type="number" step="0.1" value="${l.voltage_max}" style="width:70px"></td>
|
||||
<td colspan="2" style="font-size:0.7rem;color:var(--text-muted)" id="lp-edit-preview">${l.warn_v} V / ${l.alarm_v} V</td>
|
||||
<td><input class="form-input" id="lp-edit-wpct" type="number" step="1" min="1" max="50" value="${l.warn_pct ?? 20}" style="width:55px" title="Warn %"></td>
|
||||
<td><input class="form-input" id="lp-edit-apct" type="number" step="1" min="1" max="50" value="${l.alarm_pct ?? 10}" style="width:55px" title="Alarm %"></td>
|
||||
<td style="font-size:0.7rem" id="lp-edit-preview">${l.warn_v} V / ${l.alarm_v} V</td>
|
||||
<td><input class="form-input" id="lp-edit-notes" value="${l.notes || ''}"></td>
|
||||
<td style="display:flex;gap:4px">
|
||||
<button class="chart-row-btn" onclick="saveLamp('${id}')">SAVE</button>
|
||||
<button class="chart-row-btn danger" onclick="renderLampsTable()">CANCEL</button>
|
||||
</td>`;
|
||||
// Live preview while editing vmin/vmax
|
||||
['lp-edit-vmin','lp-edit-vmax'].forEach(i =>
|
||||
['lp-edit-vmin','lp-edit-vmax','lp-edit-wpct','lp-edit-apct'].forEach(i =>
|
||||
document.getElementById(i).addEventListener('input', () => {
|
||||
const vmin = parseFloat(document.getElementById('lp-edit-vmin').value);
|
||||
const vmax = parseFloat(document.getElementById('lp-edit-vmax').value);
|
||||
const wpct = parseFloat(document.getElementById('lp-edit-wpct').value);
|
||||
const apct = parseFloat(document.getElementById('lp-edit-apct').value);
|
||||
const cell = document.getElementById('lp-edit-preview');
|
||||
if (isNaN(vmin) || isNaN(vmax) || vmax <= vmin) { cell.textContent = '—'; return; }
|
||||
const t = _lampThresholds(vmin, vmax);
|
||||
const t = _lampThresholds(vmin, vmax, wpct, apct);
|
||||
cell.innerHTML = `<span style="color:var(--yellow)">${t.warn} V</span> / <span style="color:var(--red)">${t.alarm} V</span>`;
|
||||
}));
|
||||
};
|
||||
@@ -870,6 +876,8 @@ window.saveLamp = async function(id) {
|
||||
lamp_count: parseInt(document.getElementById('lp-edit-count').value) || 1,
|
||||
voltage_min: parseFloat(document.getElementById('lp-edit-vmin').value),
|
||||
voltage_max: parseFloat(document.getElementById('lp-edit-vmax').value),
|
||||
warn_pct: parseFloat(document.getElementById('lp-edit-wpct').value) || 20.0,
|
||||
alarm_pct: parseFloat(document.getElementById('lp-edit-apct').value) || 10.0,
|
||||
notes: document.getElementById('lp-edit-notes').value.trim() || null,
|
||||
};
|
||||
if (!payload.manufacturer || !payload.model || isNaN(payload.voltage_min) || isNaN(payload.voltage_max)) {
|
||||
|
||||
Reference in New Issue
Block a user