sprint-0: fundaciones VMS-Sailor

Sprint 0 completo del producto VMS-Sailor (Vessel Management System
integrado para buques 30-40m). Brief de referencia en
VMS_Sailor_v2_Parte_*.md (intacto).

Core (vmssailor.core, 95.17% coverage, 99 tests verde):
- ShipCoord: sistema naval x_pp/y_cl/z_bl frozen
- Vessel, Deck, Bulkhead
- Equipment, EquipmentModel, Sensor, EquipmentSpec
- Tag, AlarmConfig, TagBinding, Scaling
- CardInstance, Bus, Topology con validacion 21 puntos I/O AR-NMEA-IO-v1.0
- Alarm, PermissiveRule, Condition
- Project agregado raiz con validacion cross-entity
- Persistencia portable .vmsproj (SQLite) con roundtrip verificable

Biblioteca curada seed (vmssailor.library):
- systems_catalog.json completo (catalogo maestro Parte 1 sec 7)
- 2 vessels: Sunseeker 76, Ferretti 850
- 2 motores: MTU 12V 2000 M96, Volvo D13-900
- 1 genset: Northern Lights M65C13
- yacht_motor_planeo.yaml (reglas heuristicas)
- TODO marcado data_source=seed_estimate - requiere validacion datasheets

Tools:
- vms-validate-library: CLI valida biblioteca completa
- vms-generate-test-project: CLI demo + verificacion roundtrip persistencia

Design System + 8 mockups HTML estaticos:
- docs/design_system.md (paleta Deep Ocean, gradientes, typography, motion)
- docs/brand/ (logo + variantes SVG)
- docs/mockups/splash, studio_main, runtime_overview,
  runtime_mimic_fuel (P&ID animado), runtime_alarms, runtime_trim (panel
  estrella con horizonte artificial), mobile_overview, mobile_trim
- docs/mockups/index.html (galeria)

Firmware (Sprint 12+ implementacion):
- firmware/ar_nmea_io_v1/src/config/pinout.h con macros GPIO

Decisiones autonomas documentadas en docs/decisions_sprint0.md.

Stack: Python 3.11 + uv + Pydantic v2 + SQLite stdlib + hatchling +
pytest 9 + ruff + mypy. Sin PySide6, FastAPI, Flutter ni firmware
funcional (entran en sprints siguientes).

Criterio de aceptacion Sprint 0: cumplido.
- uv sync: OK
- pytest: 99/99 verde
- cov vmssailor.core: 95.17% (objetivo >=80%)
- ruff: clean
- vms-validate-library: OK
- vms-generate-test-project: INTEGRIDAD OK

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 07:26:06 -04:00
commit deb04c9315
96 changed files with 15335 additions and 0 deletions
+774
View File
@@ -0,0 +1,774 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VMS-Sailor Runtime · M/Y Aurora · Overview</title>
<link rel="icon" type="image/svg+xml" href="../brand/favicon.svg">
<link rel="stylesheet" href="_tokens.css">
<style>
body { overflow-x: hidden; }
.rt {
display: grid;
grid-template-rows: 64px 1fr 36px;
grid-template-columns: 240px 1fr;
grid-template-areas:
"topbar topbar"
"sidebar main"
"ticker ticker";
height: 100vh;
}
/* Topbar */
.topbar {
grid-area: topbar;
display: flex;
align-items: center;
gap: var(--s-5);
padding: 0 var(--s-5);
background: var(--c-midnight);
border-bottom: 1px solid var(--c-steel);
}
.vessel-id {
display: flex; align-items: center; gap: var(--s-3);
}
.vessel-id img { height: 32px; }
.vessel-id h1 {
margin: 0;
font-family: var(--f-display);
font-size: 18px;
font-weight: 600;
color: var(--c-foam);
line-height: 1;
}
.vessel-id .sub {
font-size: 11px;
color: var(--c-fog);
letter-spacing: 1.5px;
text-transform: uppercase;
}
.status-pill {
display: flex; align-items: center; gap: var(--s-2);
padding: 6px 14px;
background: rgba(0,224,138,0.12);
border: 1px solid rgba(0,224,138,0.4);
border-radius: var(--r-pill);
font-size: 12px;
font-weight: 600;
color: var(--c-ok);
letter-spacing: 0.5px;
}
.top-spacer { flex: 1; }
.top-meta {
display: flex; gap: var(--s-5); align-items: center;
font-family: var(--f-mono);
font-size: 12px;
color: var(--c-fog);
}
.top-meta strong { color: var(--c-sand); }
.alarm-chip {
display: flex; align-items: center; gap: var(--s-2);
padding: 6px 14px;
background: rgba(255,176,32,0.10);
border: 1px solid rgba(255,176,32,0.4);
border-radius: var(--r-pill);
font-size: 12px;
color: var(--c-warn);
font-weight: 700;
}
.user-chip {
display: flex; align-items: center; gap: var(--s-2);
padding: 6px 12px 6px 6px;
background: var(--c-steel);
border-radius: var(--r-pill);
font-size: 12px;
}
.avatar {
width: 26px; height: 26px;
border-radius: 50%;
background: var(--g-cyan);
color: #04111F;
display: flex; align-items: center; justify-content: center;
font-weight: 700;
font-size: 12px;
}
/* Sidebar */
.sidebar {
grid-area: sidebar;
background: var(--c-midnight);
border-right: 1px solid var(--c-steel);
overflow-y: auto;
padding: var(--s-4) 0;
}
.sb-section { padding: var(--s-4) var(--s-3); }
.sb-title {
font-size: 10px; font-weight: 700; letter-spacing: 2px;
color: var(--c-fog); text-transform: uppercase;
margin-bottom: var(--s-2); padding-left: var(--s-2);
}
.nav-item {
display: flex; align-items: center; gap: var(--s-3);
padding: 10px 12px;
border-radius: var(--r-2);
color: var(--c-sand); font-size: 13px;
cursor: pointer;
transition: background 120ms;
}
.nav-item:hover { background: var(--c-steel); }
.nav-item.active {
background: linear-gradient(90deg, rgba(0,217,255,0.12), transparent);
color: var(--c-foam);
box-shadow: inset 2px 0 0 var(--c-cyan);
}
.nav-item .ic { color: var(--c-fog); }
.nav-item.active .ic { color: var(--c-cyan); }
.nav-item .count {
margin-left: auto;
font-family: var(--f-mono);
font-size: 11px;
color: var(--c-fog);
}
.nav-item .alarm-badge {
margin-left: auto;
padding: 2px 8px;
background: var(--c-emergency);
color: white;
border-radius: var(--r-pill);
font-size: 10px;
font-weight: 700;
}
/* Main */
.main {
grid-area: main;
padding: var(--s-5);
overflow-y: auto;
background: var(--g-deep-sea);
}
.page-title {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: var(--s-5);
}
.page-title h2 {
margin: 0;
font-family: var(--f-display);
font-size: 28px;
font-weight: 600;
color: var(--c-foam);
}
.page-title .sub {
color: var(--c-fog); font-size: 13px; margin-top: 4px;
}
.clock {
font-family: var(--f-mono);
font-size: 32px;
color: var(--c-foam);
letter-spacing: 1px;
text-align: right;
}
.clock .date {
font-size: 12px; color: var(--c-fog);
letter-spacing: 2px;
}
/* Grid de cards */
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--s-5);
}
.col-3 { grid-column: span 3; }
.col-4 { grid-column: span 4; }
.col-6 { grid-column: span 6; }
.col-8 { grid-column: span 8; }
.col-12 { grid-column: span 12; }
.stat {
background: var(--c-midnight);
border: 1px solid var(--c-steel);
border-radius: var(--r-3);
padding: var(--s-5);
box-shadow: var(--e-2);
position: relative;
overflow: hidden;
}
.stat::before {
content: ""; position: absolute;
top: 0; left: 0; right: 0; height: 2px;
background: var(--c-iron);
}
.stat.accent-cyan::before { background: var(--g-cyan); }
.stat.accent-ok::before { background: var(--g-ok); }
.stat.accent-warn::before { background: var(--g-warn); }
.stat-header {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: var(--s-3);
}
.stat-label {
font-size: 11px; font-weight: 600; letter-spacing: 1.5px;
color: var(--c-fog); text-transform: uppercase;
}
.stat-value {
font-family: var(--f-mono);
font-size: 44px;
font-weight: 600;
color: var(--c-foam);
line-height: 1;
letter-spacing: -1px;
}
.stat-unit {
font-size: 16px;
color: var(--c-fog);
font-weight: 400;
margin-left: 4px;
}
.stat-trend {
display: flex; align-items: center; gap: 4px;
font-size: 12px;
font-family: var(--f-mono);
color: var(--c-fog);
margin-top: var(--s-2);
}
.stat-trend.up { color: var(--c-ok); }
.stat-trend.down { color: var(--c-warn); }
/* Engines card */
.engine-row {
display: grid;
grid-template-columns: 60px 1fr 110px;
gap: var(--s-4);
align-items: center;
padding: var(--s-3) 0;
border-bottom: 1px solid var(--c-steel);
}
.engine-row:last-child { border-bottom: none; }
.engine-label {
font-family: var(--f-mono);
font-size: 13px;
font-weight: 600;
color: var(--c-foam);
}
.engine-state {
display: inline-block;
padding: 2px 8px;
background: rgba(0,224,138,0.12);
color: var(--c-ok);
border-radius: var(--r-pill);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
margin-top: 2px;
}
.gauge-bar {
position: relative;
height: 8px;
background: var(--c-steel);
border-radius: var(--r-pill);
overflow: hidden;
}
.gauge-fill {
position: absolute;
inset: 0 auto 0 0;
background: var(--g-cyan);
border-radius: var(--r-pill);
box-shadow: 0 0 12px rgba(0,217,255,0.4);
}
.gauge-fill.warn { background: var(--g-warn); box-shadow: 0 0 12px rgba(255,176,32,0.4); }
.engine-meta {
display: flex; gap: var(--s-3);
font-size: 11px;
color: var(--c-fog);
margin-top: 4px;
font-family: var(--f-mono);
}
.engine-meta strong { color: var(--c-sand); }
.engine-rpm {
font-family: var(--f-mono);
font-size: 22px;
font-weight: 600;
color: var(--c-foam);
text-align: right;
}
.engine-rpm small { color: var(--c-fog); font-size: 11px; display: block; font-weight: 400; }
/* Roll/pitch */
.horizon {
width: 100%;
aspect-ratio: 1;
max-width: 280px;
margin: 0 auto;
position: relative;
}
.horizon-readout {
display: flex; justify-content: space-around;
margin-top: var(--s-4);
font-family: var(--f-mono);
}
.ro-block { text-align: center; }
.ro-block .lbl { color: var(--c-fog); font-size: 10px; letter-spacing: 2px; text-transform: uppercase; }
.ro-block .val { font-size: 26px; color: var(--c-foam); font-weight: 600; }
/* Recent alarms list */
.alarm-row {
display: grid;
grid-template-columns: 90px 1fr auto;
gap: var(--s-3);
padding: var(--s-3);
border-radius: var(--r-2);
align-items: center;
transition: background 120ms;
cursor: pointer;
}
.alarm-row:hover { background: var(--c-steel); }
.alarm-time {
font-family: var(--f-mono);
font-size: 11px;
color: var(--c-fog);
}
.alarm-msg {
font-size: 13px;
color: var(--c-sand);
}
.alarm-msg strong { color: var(--c-foam); }
.alarm-msg .src {
font-family: var(--f-mono);
font-size: 11px;
color: var(--c-cyan);
margin-right: 6px;
}
/* Tanks */
.tank {
display: flex; flex-direction: column; align-items: center;
gap: var(--s-2);
}
.tank-shell {
position: relative;
width: 70px;
height: 120px;
border: 2px solid var(--c-iron);
border-radius: 8px 8px 4px 4px;
overflow: hidden;
background: var(--c-abyss);
}
.tank-fill {
position: absolute;
left: 0; right: 0; bottom: 0;
background: linear-gradient(180deg, #1B7FB5 0%, #00D9FF 100%);
}
.tank-fill::before {
content: ""; position: absolute;
top: -3px; left: 0; right: 0; height: 6px;
background: rgba(0,217,255,0.5);
filter: blur(2px);
}
.tank-fill.warn { background: linear-gradient(180deg, #C0760F 0%, #FFB020 100%); }
.tank-label { font-family: var(--f-mono); font-size: 11px; color: var(--c-fog); }
.tank-pct {
font-family: var(--f-mono);
font-size: 16px;
color: var(--c-foam);
font-weight: 600;
}
.tank-row {
display: flex; gap: var(--s-5); justify-content: space-around;
align-items: flex-end;
}
/* Ticker */
.ticker {
grid-area: ticker;
display: flex;
align-items: center;
gap: var(--s-5);
padding: 0 var(--s-5);
background: var(--c-midnight);
border-top: 1px solid var(--c-steel);
font-family: var(--f-mono);
font-size: 11px;
color: var(--c-fog);
}
.ticker .sep { color: var(--c-iron); }
.ticker .pulse {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--c-ok);
box-shadow: 0 0 12px rgba(0,224,138,0.7);
animation: heartbeat 2s ease-in-out infinite;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.6; }
}
.tk-spacer { flex: 1; }
.ic { width: 16px; height: 16px; stroke-width: 2; stroke: currentColor; fill: none; }
</style>
</head>
<body>
<div class="app-root rt">
<header class="topbar">
<div class="vessel-id">
<img src="../brand/logo-mark.svg" alt="">
<div>
<h1>M/Y Aurora</h1>
<div class="sub">Sunseeker 76 · 23.4 m</div>
</div>
</div>
<span class="status-pill">
<span class="dot ok"></span> Normal · todo en rango
</span>
<span class="top-spacer"></span>
<span class="alarm-chip">
<svg class="ic" viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
2 alarmas activas
</span>
<span class="chip">
<span class="dot cyan"></span>
Autoridad: <strong style="color:var(--c-cyan)">PUENTE</strong>
</span>
<span class="user-chip">
<span class="avatar">A</span>
Álvaro · <span style="color:var(--c-fog)">Admin</span>
</span>
</header>
<aside class="sidebar">
<div class="sb-section">
<div class="sb-title">Vistas</div>
<div class="nav-item active">
<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
Overview
</div>
<div class="nav-item">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3v18M3 12h18"/></svg>
Mímicos
</div>
<div class="nav-item">
<svg class="ic" viewBox="0 0 24 24"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>
Alarmas
<span class="alarm-badge">2</span>
</div>
<div class="nav-item">
<svg class="ic" viewBox="0 0 24 24"><polyline points="3 17 9 11 13 15 21 7"/></svg>
Trends
</div>
<div class="nav-item">
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><polyline points="12 6 12 12 15 14"/></svg>
Log Book
</div>
<div class="nav-item">
<svg class="ic" viewBox="0 0 24 24"><path d="M6 3v18M18 3v18"/><circle cx="12" cy="12" r="3"/></svg>
Trim & Maniobra
</div>
</div>
<div class="sb-section">
<div class="sb-title">Sistemas</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M5 12h2M17 12h2M12 5v2M12 17v2"/></svg> Máquina principal</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><path d="M13 2L3 14h7v8l10-12h-7V2z"/></svg> Generación eléctrica</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><path d="M12 2v10l4 4M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0z"/></svg> Combustible</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><path d="M6 8c0-4 6-6 6-6s6 2 6 6v3a6 6 0 0 1-12 0V8z"/></svg> Refrigeración</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><polyline points="12 6 12 12 8 14"/></svg> Sentinas</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><path d="M3 12h6l3-9 3 18 3-9h3"/></svg> HVAC</div>
<div class="nav-item"><svg class="ic" viewBox="0 0 24 24"><path d="M12 2v6M12 16v6M2 12h6M16 12h6M5 5l4 4M15 15l4 4M5 19l4-4M15 9l4-4"/></svg> Iluminación</div>
</div>
<div class="sb-section">
<div class="sb-title">Soporte</div>
<div class="nav-item">
<svg class="ic" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Auditoría VPN
</div>
</div>
</aside>
<main class="main">
<div class="page-title">
<div>
<h2>Estado general del buque</h2>
<div class="sub">Última actualización: hace 1.2 s · Sin desconexiones</div>
</div>
<div class="clock">
<div>03:42:18</div>
<div class="date">2026-05-17 · UTC-04</div>
</div>
</div>
<div class="grid">
<!-- 4 stats -->
<div class="stat col-3 accent-cyan">
<div class="stat-header">
<span class="stat-label">Combustible</span>
<span class="dot ok"></span>
</div>
<div class="stat-value">2,840<span class="stat-unit">L</span></div>
<div class="stat-trend down">▼ 18 L/h consumo medio</div>
</div>
<div class="stat col-3 accent-ok">
<div class="stat-header">
<span class="stat-label">Generación</span>
<span class="dot ok"></span>
</div>
<div class="stat-value">28.4<span class="stat-unit">kW</span></div>
<div class="stat-trend">GEN_1 · 55% carga</div>
</div>
<div class="stat col-3 accent-ok">
<div class="stat-header">
<span class="stat-label">Baterías</span>
<span class="dot ok"></span>
</div>
<div class="stat-value">27.8<span class="stat-unit">V</span></div>
<div class="stat-trend up">▲ Cargando 12 A</div>
</div>
<div class="stat col-3 accent-warn">
<div class="stat-header">
<span class="stat-label">Sentinas</span>
<span class="dot warn"></span>
</div>
<div class="stat-value">12<span class="stat-unit">%</span></div>
<div class="stat-trend down">BILGE_MID en watch</div>
</div>
<!-- Motores -->
<div class="card col-8">
<div class="stat-header">
<span class="stat-label">Máquina principal · 2× MTU 12V 2000 M96</span>
<a href="#" style="font-size:12px">Ver mímico →</a>
</div>
<div style="margin-top: var(--s-4);">
<div class="engine-row">
<div>
<div class="engine-label">ME_PORT</div>
<span class="engine-state">RUNNING</span>
</div>
<div>
<div class="gauge-bar"><div class="gauge-fill" style="width: 62%;"></div></div>
<div class="engine-meta">
<span>Aceite <strong>4.8 bar</strong></span>
<span>Coolant <strong>82°C</strong></span>
<span>Carga <strong>62%</strong></span>
<span>Horas <strong>1,284</strong></span>
</div>
</div>
<div class="engine-rpm">1,520 <small>rpm</small></div>
</div>
<div class="engine-row">
<div>
<div class="engine-label">ME_STBD</div>
<span class="engine-state">RUNNING</span>
</div>
<div>
<div class="gauge-bar"><div class="gauge-fill" style="width: 58%;"></div></div>
<div class="engine-meta">
<span>Aceite <strong>4.9 bar</strong></span>
<span>Coolant <strong>81°C</strong></span>
<span>Carga <strong>58%</strong></span>
<span>Horas <strong>1,287</strong></span>
</div>
</div>
<div class="engine-rpm">1,498 <small>rpm</small></div>
</div>
<div class="engine-row">
<div>
<div class="engine-label">GEN_1</div>
<span class="engine-state">RUNNING</span>
</div>
<div>
<div class="gauge-bar"><div class="gauge-fill warn" style="width: 88%;"></div></div>
<div class="engine-meta">
<span>L1 <strong>231 V</strong></span>
<span>Coolant <strong>89°C</strong></span>
<span>Carga <strong>88%</strong></span>
<span>Horas <strong>3,418</strong></span>
</div>
</div>
<div class="engine-rpm">1,800 <small>rpm</small></div>
</div>
</div>
</div>
<!-- Roll/pitch -->
<div class="card col-4">
<div class="stat-header">
<span class="stat-label">Actitud (NMEA 2000 · PGN 127257)</span>
<span class="badge badge-ok">SAFE</span>
</div>
<div class="horizon">
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3A6BA8"/>
<stop offset="100%" stop-color="#1B3E6E"/>
</linearGradient>
<linearGradient id="sea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0A1A2E"/>
<stop offset="100%" stop-color="#04111F"/>
</linearGradient>
<clipPath id="circleClip">
<circle cx="100" cy="100" r="86"/>
</clipPath>
</defs>
<circle cx="100" cy="100" r="92" fill="none" stroke="#1A2B42" stroke-width="2"/>
<g clip-path="url(#circleClip)">
<g transform="rotate(-4 100 100)">
<rect x="0" y="0" width="200" height="100" fill="url(#sky)"/>
<rect x="0" y="100" width="200" height="100" fill="url(#sea)"/>
<line x1="0" y1="100" x2="200" y2="100" stroke="#00D9FF" stroke-width="1.5" opacity="0.7"/>
<g font-family="JetBrains Mono" font-size="9" fill="#E6EAF0">
<line x1="80" y1="80" x2="120" y2="80" stroke="#E6EAF0" stroke-width="1" opacity="0.6"/>
<text x="125" y="84">10°</text>
<line x1="80" y1="120" x2="120" y2="120" stroke="#E6EAF0" stroke-width="1" opacity="0.6"/>
<text x="125" y="124">10°</text>
</g>
</g>
</g>
<!-- Center cross -->
<g stroke="#00D9FF" stroke-width="2" fill="none">
<line x1="80" y1="100" x2="95" y2="100"/>
<line x1="105" y1="100" x2="120" y2="100"/>
<circle cx="100" cy="100" r="3" fill="#00D9FF"/>
</g>
<!-- Tick scale top -->
<g stroke="#7C8B9F" stroke-width="1">
<line x1="100" y1="8" x2="100" y2="16"/>
<line x1="50" y1="22" x2="56" y2="29" opacity="0.5"/>
<line x1="150" y1="22" x2="144" y2="29" opacity="0.5"/>
</g>
</svg>
</div>
<div class="horizon-readout">
<div class="ro-block">
<div class="lbl">Roll</div>
<div class="val">-4.1°</div>
</div>
<div class="ro-block">
<div class="lbl">Pitch</div>
<div class="val">+1.8°</div>
</div>
</div>
<div style="margin-top: var(--s-4); padding-top: var(--s-3); border-top: 1px solid var(--c-steel); font-size: 11px; color: var(--c-fog); display:flex; justify-content: space-between; font-family: var(--f-mono);">
<span>Envelope ±10°</span>
<span>L3 trigger 18°</span>
</div>
</div>
<!-- Tanques -->
<div class="card col-6">
<div class="stat-header">
<span class="stat-label">Tanques estructurales</span>
<a href="#" style="font-size:12px">Detalle →</a>
</div>
<div class="tank-row" style="margin-top: var(--s-4);">
<div class="tank">
<div class="tank-shell">
<div class="tank-fill" style="height: 78%;"></div>
</div>
<div class="tank-pct">78%</div>
<div class="tank-label">FUEL 1</div>
</div>
<div class="tank">
<div class="tank-shell">
<div class="tank-fill" style="height: 64%;"></div>
</div>
<div class="tank-pct">64%</div>
<div class="tank-label">FUEL 2</div>
</div>
<div class="tank">
<div class="tank-shell">
<div class="tank-fill" style="height: 91%; background: linear-gradient(180deg, #007F4E 0%, #00E08A 100%);"></div>
</div>
<div class="tank-pct">91%</div>
<div class="tank-label">WATER</div>
</div>
<div class="tank">
<div class="tank-shell">
<div class="tank-fill warn" style="height: 12%;"></div>
</div>
<div class="tank-pct" style="color: var(--c-warn);">12%</div>
<div class="tank-label">BILGE</div>
</div>
<div class="tank">
<div class="tank-shell">
<div class="tank-fill" style="height: 28%; background: linear-gradient(180deg, #5A6B7F 0%, #94A3B8 100%);"></div>
</div>
<div class="tank-pct">28%</div>
<div class="tank-label">BLACK</div>
</div>
</div>
</div>
<!-- Alarmas recientes -->
<div class="card col-6">
<div class="stat-header">
<span class="stat-label">Alarmas recientes</span>
<a href="#" style="font-size:12px">Ver todas (2) →</a>
</div>
<div style="margin-top: var(--s-3);">
<div class="alarm-row">
<span class="alarm-time">03:38:42</span>
<div class="alarm-msg">
<span class="src">GEN_1.COOLANT_TEMP</span>
<strong>89°C</strong> alta — aproximando límite 92°C
</div>
<span class="badge badge-low">LOW</span>
</div>
<div class="alarm-row">
<span class="alarm-time">03:31:17</span>
<div class="alarm-msg">
<span class="src">BILGE_MID.LEVEL</span>
Nivel <strong>12%</strong> — verificar bomba
</div>
<span class="badge badge-info">INFO</span>
</div>
<div class="alarm-row" style="opacity: 0.5;">
<span class="alarm-time">03:12:04</span>
<div class="alarm-msg">
<span class="src">ME_PORT.OIL_TEMP</span>
Recuperado a <strong>88°C</strong> · resolved
</div>
<span class="badge badge-muted">CLEARED</span>
</div>
<div class="alarm-row" style="opacity: 0.5;">
<span class="alarm-time">02:58:30</span>
<div class="alarm-msg">
<span class="src">SHORE_POWER.STATUS</span>
Transferencia a gen — desconexión muelle
</div>
<span class="badge badge-muted">CLEARED</span>
</div>
</div>
</div>
</div>
</main>
<footer class="ticker">
<span class="pulse"></span>
<span>WebSocket <strong style="color:var(--c-sand)">LIVE</strong></span>
<span class="sep">|</span>
<span>Latencia <strong style="color:var(--c-sand)">42 ms</strong></span>
<span class="sep">|</span>
<span>Driver Modbus RTU <strong style="color:var(--c-ok)">OK</strong></span>
<span class="sep">|</span>
<span>Driver NMEA 2000 <strong style="color:var(--c-ok)">OK</strong></span>
<span class="sep">|</span>
<span>Tags activos <strong style="color:var(--c-sand)">187</strong></span>
<span class="tk-spacer"></span>
<span><span class="dot cyan"></span> VPN soporte <strong>INACTIVA</strong></span>
<span class="sep">|</span>
<span>Telemetría <strong style="color:var(--c-ok)">activa</strong> (visible)</span>
<span class="sep">|</span>
<span>v0.1.0.dev0</span>
</footer>
</div>
</body>
</html>