deb04c9315
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>
583 lines
19 KiB
HTML
583 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>VMS-Sailor Mobile · Overview</title>
|
|
<link rel="icon" type="image/svg+xml" href="../brand/favicon.svg">
|
|
<link rel="stylesheet" href="_tokens.css">
|
|
<style>
|
|
body {
|
|
background:
|
|
radial-gradient(ellipse at top, rgba(0,217,255,0.10), transparent 60%),
|
|
radial-gradient(ellipse at bottom, rgba(91,192,235,0.06), transparent 60%),
|
|
var(--g-deep-sea);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--s-7) var(--s-5);
|
|
gap: var(--s-7);
|
|
}
|
|
|
|
.phone {
|
|
width: 390px;
|
|
height: 844px;
|
|
background: #000;
|
|
border-radius: 56px;
|
|
padding: 12px;
|
|
box-shadow:
|
|
var(--e-5),
|
|
0 0 0 2px rgba(255,255,255,0.05),
|
|
0 0 0 14px #1a1a1a;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
}
|
|
.phone::before {
|
|
/* Dynamic Island */
|
|
content: ""; position: absolute;
|
|
top: 16px; left: 50%; transform: translateX(-50%);
|
|
width: 124px; height: 36px;
|
|
background: #000; border-radius: 18px;
|
|
z-index: 10;
|
|
}
|
|
.screen {
|
|
width: 100%; height: 100%;
|
|
background: var(--c-abyss);
|
|
border-radius: 44px;
|
|
overflow: hidden;
|
|
display: flex; flex-direction: column;
|
|
position: relative;
|
|
}
|
|
.ios-status {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 16px 32px 6px;
|
|
font-family: var(--f-ui);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--c-foam);
|
|
height: 56px;
|
|
}
|
|
.ios-status .indicators {
|
|
display: flex; gap: 6px; align-items: center;
|
|
}
|
|
.ios-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 11px; }
|
|
.ios-bars i { width: 3px; background: var(--c-foam); border-radius: 1px; }
|
|
.ios-bars i:nth-child(1) { height: 4px; }
|
|
.ios-bars i:nth-child(2) { height: 6px; }
|
|
.ios-bars i:nth-child(3) { height: 8px; }
|
|
.ios-bars i:nth-child(4) { height: 11px; }
|
|
.ios-battery { width: 25px; height: 12px; border: 1px solid var(--c-foam); border-radius: 3px; padding: 1px; position: relative; }
|
|
.ios-battery::after { content: ""; position: absolute; top: 3px; right: -3px; width: 2px; height: 6px; background: var(--c-foam); border-radius: 1px; }
|
|
.ios-battery div { width: 88%; height: 100%; background: var(--c-foam); border-radius: 1px; }
|
|
|
|
/* App header */
|
|
.m-header {
|
|
padding: var(--s-3) var(--s-5);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
border-bottom: 1px solid var(--c-steel);
|
|
}
|
|
.m-header .left {
|
|
display: flex; align-items: center; gap: var(--s-2);
|
|
}
|
|
.m-header img { height: 24px; }
|
|
.m-header .vname {
|
|
font-family: var(--f-display);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: var(--c-foam);
|
|
line-height: 1;
|
|
}
|
|
.m-header .vsub {
|
|
font-size: 10px;
|
|
color: var(--c-fog);
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
}
|
|
.m-header .alarm-icon {
|
|
position: relative;
|
|
width: 36px; height: 36px;
|
|
border-radius: 50%;
|
|
background: rgba(255,176,32,0.14);
|
|
border: 1px solid rgba(255,176,32,0.4);
|
|
display: flex; align-items: center; justify-content: center;
|
|
color: var(--c-warn);
|
|
}
|
|
.m-header .alarm-icon::after {
|
|
content: "2"; position: absolute;
|
|
top: -2px; right: -2px;
|
|
width: 18px; height: 18px;
|
|
background: var(--c-emergency);
|
|
color: white;
|
|
border-radius: 50%;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
display: flex; align-items: center; justify-content: center;
|
|
border: 2px solid var(--c-abyss);
|
|
}
|
|
|
|
.m-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--s-4) var(--s-4) var(--s-9);
|
|
display: flex; flex-direction: column; gap: var(--s-4);
|
|
}
|
|
|
|
.hero {
|
|
padding: var(--s-5);
|
|
background: linear-gradient(135deg, rgba(0,217,255,0.18), rgba(0,217,255,0.02));
|
|
border: 1px solid rgba(0,217,255,0.3);
|
|
border-radius: var(--r-4);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.hero::before {
|
|
content: ""; position: absolute;
|
|
top: -30px; right: -30px;
|
|
width: 160px; height: 160px;
|
|
background: radial-gradient(circle, rgba(0,217,255,0.35), transparent 70%);
|
|
filter: blur(12px);
|
|
}
|
|
.hero .label {
|
|
font-size: 10px;
|
|
color: var(--c-cyan);
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
font-weight: 700;
|
|
}
|
|
.hero .value {
|
|
font-family: var(--f-display);
|
|
font-size: 34px;
|
|
font-weight: 700;
|
|
color: var(--c-foam);
|
|
margin-top: 4px;
|
|
letter-spacing: -1px;
|
|
}
|
|
.hero .extra {
|
|
display: flex; gap: var(--s-4); margin-top: var(--s-3);
|
|
font-family: var(--f-mono); font-size: 12px;
|
|
}
|
|
.hero .extra div span { color: var(--c-fog); display: block; font-size: 10px; letter-spacing: 1px; text-transform: uppercase; }
|
|
.hero .extra div strong { color: var(--c-foam); font-size: 14px; }
|
|
|
|
.row-2 {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--s-3);
|
|
}
|
|
.stat-mini {
|
|
padding: var(--s-3);
|
|
background: var(--c-midnight);
|
|
border: 1px solid var(--c-steel);
|
|
border-radius: var(--r-3);
|
|
}
|
|
.stat-mini .lbl {
|
|
font-size: 9px;
|
|
color: var(--c-fog);
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
font-weight: 700;
|
|
}
|
|
.stat-mini .val {
|
|
font-family: var(--f-mono);
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
color: var(--c-foam);
|
|
margin-top: 4px;
|
|
line-height: 1;
|
|
}
|
|
.stat-mini .val .u {
|
|
font-size: 11px;
|
|
color: var(--c-fog);
|
|
margin-left: 4px;
|
|
}
|
|
.stat-mini .footer {
|
|
display: flex; gap: 6px; align-items: center;
|
|
font-size: 10px; color: var(--c-fog);
|
|
margin-top: var(--s-2);
|
|
font-family: var(--f-mono);
|
|
}
|
|
.stat-mini.warn { border-color: rgba(255,176,32,0.5); background: rgba(255,176,32,0.04); }
|
|
.stat-mini.warn .val { color: var(--c-warn); }
|
|
|
|
.panel {
|
|
padding: var(--s-4);
|
|
background: var(--c-midnight);
|
|
border: 1px solid var(--c-steel);
|
|
border-radius: var(--r-3);
|
|
}
|
|
.panel h4 {
|
|
margin: 0 0 var(--s-3);
|
|
font-size: 13px;
|
|
letter-spacing: 1.5px;
|
|
text-transform: uppercase;
|
|
color: var(--c-fog);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.engine-mini {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: var(--s-2);
|
|
padding: var(--s-2) 0;
|
|
border-bottom: 1px solid var(--c-steel);
|
|
}
|
|
.engine-mini:last-child { border-bottom: none; }
|
|
.engine-mini .n {
|
|
font-family: var(--f-mono);
|
|
font-size: 12px;
|
|
color: var(--c-foam);
|
|
font-weight: 600;
|
|
}
|
|
.engine-mini .meta {
|
|
display: flex; gap: var(--s-3);
|
|
font-family: var(--f-mono); font-size: 10px; color: var(--c-fog);
|
|
margin-top: 2px;
|
|
}
|
|
.engine-mini .rpm {
|
|
font-family: var(--f-mono);
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--c-foam);
|
|
text-align: right;
|
|
}
|
|
.engine-mini .rpm small { color: var(--c-fog); font-size: 10px; display: block; font-weight: 400; }
|
|
|
|
.alarm-mini {
|
|
display: grid;
|
|
grid-template-columns: 8px 1fr auto;
|
|
gap: var(--s-2);
|
|
align-items: center;
|
|
padding: var(--s-2) 0;
|
|
border-bottom: 1px solid var(--c-steel);
|
|
}
|
|
.alarm-mini:last-child { border-bottom: none; }
|
|
.alarm-mini .ttl {
|
|
font-size: 12px; color: var(--c-foam); font-weight: 600;
|
|
}
|
|
.alarm-mini .ttl .src {
|
|
color: var(--c-cyan); font-family: var(--f-mono); font-size: 10px; display: block; margin-top: 2px;
|
|
}
|
|
.alarm-mini .when {
|
|
font-family: var(--f-mono); font-size: 10px; color: var(--c-fog);
|
|
}
|
|
.alarm-mini .bar {
|
|
width: 4px; height: 100%; min-height: 32px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Horizon mini */
|
|
.horizon-mini { display: flex; justify-content: center; padding: var(--s-3); }
|
|
.horizon-mini svg { width: 100%; max-width: 200px; aspect-ratio: 1; }
|
|
.att-readout-row {
|
|
display: grid; grid-template-columns: 1fr 1fr;
|
|
gap: var(--s-3); margin-top: var(--s-2);
|
|
}
|
|
.att-mini {
|
|
padding: var(--s-2);
|
|
background: var(--c-abyss);
|
|
border-radius: var(--r-2);
|
|
text-align: center;
|
|
font-family: var(--f-mono);
|
|
}
|
|
.att-mini .lbl { font-size: 9px; color: var(--c-fog); letter-spacing: 1.5px; text-transform: uppercase; }
|
|
.att-mini .val { font-size: 20px; color: var(--c-foam); font-weight: 600; margin-top: 4px; }
|
|
|
|
/* Bottom tab bar */
|
|
.tab-bar {
|
|
position: absolute;
|
|
bottom: 0; left: 0; right: 0;
|
|
background: rgba(10,26,46,0.92);
|
|
border-top: 1px solid var(--c-steel);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
display: flex;
|
|
padding: var(--s-2) var(--s-3) calc(var(--s-3) + 18px);
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
display: flex; flex-direction: column; align-items: center;
|
|
gap: 4px;
|
|
padding: 6px 4px;
|
|
color: var(--c-fog);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.3px;
|
|
cursor: pointer;
|
|
}
|
|
.tab.active { color: var(--c-cyan); }
|
|
.tab .ic {
|
|
width: 22px; height: 22px;
|
|
stroke: currentColor; fill: none; stroke-width: 2;
|
|
}
|
|
.tab .badge {
|
|
position: absolute;
|
|
transform: translate(14px, -2px);
|
|
background: var(--c-emergency);
|
|
color: #FFFFFF;
|
|
font-size: 8px;
|
|
font-weight: 700;
|
|
padding: 1px 5px;
|
|
border-radius: var(--r-pill);
|
|
}
|
|
|
|
/* Side panel description */
|
|
.desc-panel {
|
|
max-width: 320px;
|
|
color: var(--c-sand);
|
|
}
|
|
.desc-panel h2 {
|
|
font-family: var(--f-display);
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
margin: 0 0 var(--s-3);
|
|
color: var(--c-foam);
|
|
}
|
|
.desc-panel p {
|
|
font-size: 14px;
|
|
line-height: 1.7;
|
|
color: var(--c-sand);
|
|
}
|
|
.desc-panel ul { list-style: none; padding: 0; }
|
|
.desc-panel li {
|
|
padding: var(--s-3) 0;
|
|
border-bottom: 1px solid var(--c-steel);
|
|
display: flex; gap: var(--s-3);
|
|
font-size: 13px;
|
|
}
|
|
.desc-panel li::before {
|
|
content: "✓";
|
|
color: var(--c-cyan);
|
|
font-weight: 700;
|
|
width: 24px;
|
|
flex-shrink: 0;
|
|
}
|
|
.ic { stroke: currentColor; fill: none; stroke-width: 2; width: 18px; height: 18px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="phone">
|
|
<div class="screen">
|
|
<!-- iOS status bar -->
|
|
<div class="ios-status">
|
|
<span>03:42</span>
|
|
<div class="indicators">
|
|
<span class="ios-bars"><i></i><i></i><i></i><i></i></span>
|
|
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" style="color: var(--c-foam);"><path d="M8 4a5 5 0 0 1 5 5M8 1a8 8 0 0 1 8 8M8 7a2 2 0 0 1 2 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
|
<span class="ios-battery"><div></div></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- App header -->
|
|
<div class="m-header">
|
|
<div class="left">
|
|
<img src="../brand/logo-mark.svg" alt="">
|
|
<div>
|
|
<div class="vname">M/Y Aurora</div>
|
|
<div class="vsub">Sunseeker 76</div>
|
|
</div>
|
|
</div>
|
|
<div class="alarm-icon">
|
|
<svg class="ic" viewBox="0 0 24 24" stroke-width="2.2"><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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="m-body">
|
|
|
|
<div class="hero">
|
|
<div class="label">⚡ Estado general</div>
|
|
<div class="value">Normal · todo en rango</div>
|
|
<div class="extra">
|
|
<div><span>Velocidad</span><strong>12.4 kn</strong></div>
|
|
<div><span>Rumbo</span><strong>148°</strong></div>
|
|
<div><span>Profundidad</span><strong>34 m</strong></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row-2">
|
|
<div class="stat-mini">
|
|
<div class="lbl">Combustible</div>
|
|
<div class="val">2,840<span class="u">L</span></div>
|
|
<div class="footer">▼ 18 L/h · 158 h</div>
|
|
</div>
|
|
<div class="stat-mini">
|
|
<div class="lbl">Baterías</div>
|
|
<div class="val">27.8<span class="u">V</span></div>
|
|
<div class="footer">▲ Cargando 12 A</div>
|
|
</div>
|
|
<div class="stat-mini">
|
|
<div class="lbl">Gen</div>
|
|
<div class="val">28.4<span class="u">kW</span></div>
|
|
<div class="footer">GEN_1 · 55%</div>
|
|
</div>
|
|
<div class="stat-mini warn">
|
|
<div class="lbl">Sentinas</div>
|
|
<div class="val">12<span class="u">%</span></div>
|
|
<div class="footer">⚠ BILGE_MID watch</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h4>Máquinas</h4>
|
|
<div class="engine-mini">
|
|
<div>
|
|
<div class="n">ME_PORT</div>
|
|
<div class="meta">
|
|
<span>Aceite 4.8 bar</span>
|
|
<span>82°C</span>
|
|
</div>
|
|
</div>
|
|
<div class="rpm">1,520 <small>rpm</small></div>
|
|
</div>
|
|
<div class="engine-mini">
|
|
<div>
|
|
<div class="n">ME_STBD</div>
|
|
<div class="meta">
|
|
<span>Aceite 4.9 bar</span>
|
|
<span>81°C</span>
|
|
</div>
|
|
</div>
|
|
<div class="rpm">1,498 <small>rpm</small></div>
|
|
</div>
|
|
<div class="engine-mini">
|
|
<div>
|
|
<div class="n" style="color: var(--c-warn);">GEN_1</div>
|
|
<div class="meta">
|
|
<span>L1 231 V</span>
|
|
<span style="color: var(--c-warn);">89°C ▲</span>
|
|
<span>88%</span>
|
|
</div>
|
|
</div>
|
|
<div class="rpm">1,800 <small>rpm</small></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h4>Actitud</h4>
|
|
<div class="horizon-mini">
|
|
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="msky" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stop-color="#3A6BA8"/>
|
|
<stop offset="100%" stop-color="#1B3E6E"/>
|
|
</linearGradient>
|
|
<linearGradient id="msea" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stop-color="#0A1A2E"/>
|
|
<stop offset="100%" stop-color="#04111F"/>
|
|
</linearGradient>
|
|
<clipPath id="mclip"><circle cx="100" cy="100" r="85"/></clipPath>
|
|
</defs>
|
|
<circle cx="100" cy="100" r="90" fill="none" stroke="#1A2B42" stroke-width="2"/>
|
|
<g clip-path="url(#mclip)">
|
|
<g transform="rotate(-4 100 100)">
|
|
<rect x="0" y="0" width="200" height="100" fill="url(#msky)"/>
|
|
<rect x="0" y="100" width="200" height="100" fill="url(#msea)"/>
|
|
<line x1="0" y1="100" x2="200" y2="100" stroke="#00D9FF" stroke-width="2" opacity="0.9"/>
|
|
<line x1="80" y1="80" x2="120" y2="80" stroke="#E6EAF0" stroke-width="1" opacity="0.6"/>
|
|
<line x1="80" y1="120" x2="120" y2="120" stroke="#E6EAF0" stroke-width="1" opacity="0.6"/>
|
|
</g>
|
|
</g>
|
|
<g stroke="#00D9FF" stroke-width="2.5" fill="none">
|
|
<line x1="78" y1="100" x2="92" y2="100"/>
|
|
<line x1="108" y1="100" x2="122" y2="100"/>
|
|
</g>
|
|
<circle cx="100" cy="100" r="3" fill="#00D9FF"/>
|
|
<!-- Safe top marker -->
|
|
<path d="M 100 12 L 96 4 L 104 4 Z" fill="#00E08A"/>
|
|
</svg>
|
|
</div>
|
|
<div class="att-readout-row">
|
|
<div class="att-mini">
|
|
<div class="lbl">Roll</div>
|
|
<div class="val">-4.1°</div>
|
|
</div>
|
|
<div class="att-mini">
|
|
<div class="lbl">Pitch</div>
|
|
<div class="val">+1.8°</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<h4>Alarmas recientes</h4>
|
|
<div class="alarm-mini">
|
|
<div class="bar" style="background: var(--c-warn);"></div>
|
|
<div>
|
|
<div class="ttl">
|
|
Coolant 89°C ▲
|
|
<span class="src">GEN_1.COOLANT_TEMP</span>
|
|
</div>
|
|
</div>
|
|
<div class="when">5m</div>
|
|
</div>
|
|
<div class="alarm-mini">
|
|
<div class="bar" style="background: var(--c-info);"></div>
|
|
<div>
|
|
<div class="ttl">
|
|
Bilge 12%
|
|
<span class="src">BILGE_MID.LEVEL</span>
|
|
</div>
|
|
</div>
|
|
<div class="when">12m</div>
|
|
</div>
|
|
<div class="alarm-mini" style="opacity:0.5;">
|
|
<div class="bar" style="background: var(--c-iron);"></div>
|
|
<div>
|
|
<div class="ttl">
|
|
Resuelto · oil temp 88°C
|
|
<span class="src">ME_PORT.OIL_TEMP</span>
|
|
</div>
|
|
</div>
|
|
<div class="when">31m</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab bar -->
|
|
<nav class="tab-bar">
|
|
<div class="tab 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="tab">
|
|
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3v18"/></svg>
|
|
Mímicos
|
|
</div>
|
|
<div class="tab">
|
|
<span class="badge">2</span>
|
|
<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"/></svg>
|
|
Alarmas
|
|
</div>
|
|
<div class="tab">
|
|
<svg class="ic" viewBox="0 0 24 24"><path d="M6 3v18M18 3v18"/><circle cx="12" cy="12" r="3"/></svg>
|
|
Trim
|
|
</div>
|
|
<div class="tab">
|
|
<svg class="ic" viewBox="0 0 24 24"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
|
|
Más
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="desc-panel">
|
|
<span class="overline">Mockup móvil</span>
|
|
<h2>VMS-Sailor Mobile</h2>
|
|
<p>App nativa Flutter para iOS y Android. <strong>Solo WiFi local del buque</strong> — nunca expuesta a internet. Autenticación TOTP + biométrico del dispositivo.</p>
|
|
<ul>
|
|
<li>Enrollment por QR desde estación principal</li>
|
|
<li>Permissives evaluados en el servidor — el móvil es solo UI</li>
|
|
<li>Notificaciones push <em>locales</em> (sin Firebase ni APNs externos)</li>
|
|
<li>Modo offline degradado con últimos valores cached</li>
|
|
<li>Acciones de propulsión <strong>solo desde estación fija</strong></li>
|
|
</ul>
|
|
<p style="font-family: var(--f-mono); font-size: 12px; color: var(--c-fog); margin-top: var(--s-5);">
|
|
Sprint 11 · Build iOS via TestFlight · Android APK firmado distribuido directo.
|
|
</p>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|