Files
AR-VMS-Seaman/docs/mockups/mobile_overview.html
T
alro65 deb04c9315 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>
2026-05-17 07:26:06 -04:00

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>