diff --git a/DESPLIEGUE.md b/DESPLIEGUE.md
index 6ba6fe5..c54feca 100644
--- a/DESPLIEGUE.md
+++ b/DESPLIEGUE.md
@@ -54,7 +54,7 @@ El backend la deduce sola de `ACTAS_DIR`; si prefieres fijarla, agrega a
Environment=ADJUNTOS_DIR=/srv/projects/drogheda/adjuntos
Environment=ADJUNTO_MAX_MB=8
Environment=ARCHIVO_MAX_MB=20
-Environment=ACTAS_REPETIBLES=diagnostico-campo,instalacion-campo
+Environment=ACTAS_REPETIBLES=diagnostico-campo,diagnostico-banco,instalacion-campo,reflasheo,dano,envio,recepcion
```
### 4 · nginx — subida de fotos e identidad del técnico (una sola vez)
diff --git a/backend/actas-hbox.service b/backend/actas-hbox.service
index c55df89..f88b683 100644
--- a/backend/actas-hbox.service
+++ b/backend/actas-hbox.service
@@ -10,7 +10,7 @@ Environment=ACTAS_DIR=/srv/projects/drogheda/actas
Environment=ADJUNTOS_DIR=/srv/projects/drogheda/adjuntos
Environment=ADJUNTO_MAX_MB=8
Environment=ARCHIVO_MAX_MB=20
-Environment=ACTAS_REPETIBLES=diagnostico-campo,instalacion-campo
+Environment=ACTAS_REPETIBLES=diagnostico-campo,diagnostico-banco,instalacion-campo,reflasheo,dano,envio,recepcion
Environment=ACTAS_ADMINS=leo
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
Environment=PASS_MIN=8
diff --git a/backend/app.py b/backend/app.py
index 9483fa4..6bc0120 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -36,7 +36,7 @@ ADMINS = {u.strip().lower() for u in os.environ.get("ACTAS_ADMINS", "leo").split
TIPOS_UNICOS = {"ensamble"} # tipos en los que un componente solo puede usarse en un acta real
# Tipos REPETIBLES: una misma serie puede tener varias actas (cada visita a campo es un evento nuevo).
TIPOS_REPETIBLES = {t.strip() for t in os.environ.get(
- "ACTAS_REPETIBLES", "diagnostico-campo,diagnostico-banco,instalacion-campo,reflasheo").split(",") if t.strip()}
+ "ACTAS_REPETIBLES", "diagnostico-campo,diagnostico-banco,instalacion-campo,reflasheo,dano,envio,recepcion").split(",") if t.strip()}
# Subida de fotos
MAX_MB = int(os.environ.get("ADJUNTO_MAX_MB", "8"))
@@ -193,9 +193,11 @@ def crear():
os.makedirs(carpeta, exist_ok=True)
archivo = f"{serie}_{iso.replace(':', '-')}.json"
if es_repetible(tipo):
- # cada visita es un registro propio: sufijo único para que dos envíos
- # en el mismo segundo (doble toque) no se pisen
- archivo = f"{serie}_{iso.replace(':', '-')}_{uuid.uuid4().hex[:6]}.json"
+ # cada visita es un registro propio. El nombre lleva microsegundos para que el
+ # orden alfabético SEA el orden cronológico (dos envíos en el mismo segundo no
+ # se pisan ni se desordenan), y un sufijo aleatorio por si acaso.
+ micro = datetime.datetime.now().strftime("%f")
+ archivo = f"{serie}_{iso.replace(':', '-')}-{micro}_{uuid.uuid4().hex[:4]}.json"
with open(os.path.join(carpeta, archivo), "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False, indent=2)
@@ -290,7 +292,7 @@ def listar():
"resultado": d.get("resultado"), "recibido": d.get("_recibido")}
if isinstance(d.get("componentes"), dict):
fila["componentes"] = d["componentes"]
- for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad"):
+ for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad", "resumen"):
if d.get(k):
fila[k] = d[k]
if d.get("archivos"):
@@ -631,7 +633,9 @@ def expediente():
actas.sort(key=_clave, reverse=True)
relacionadas.sort(key=_clave, reverse=True)
fotos, logs = _listar_adjuntos(serie)
+ marca = estado_serie(serie)
return jsonify(ok=True, serie=serie, actas=actas, relacionadas=relacionadas,
+ marca=marca["marca"], marca_acta=marca["acta"],
fotos=fotos, logs=logs,
total=len(actas), total_fotos=len(fotos), total_logs=len(logs))
@@ -654,5 +658,64 @@ def series():
for k, v in sorted(vistas.items())])
+# ------------------------------------------------- estado de una serie (daños abiertos)
+# Un acta de tipo 'dano' MARCA la serie. La marca se levanta con otra acta 'dano' cuyo
+# campo 'accion' empiece por "Reparada" o "Baja": lo último que se dijo, manda.
+def estado_serie(serie):
+ """Devuelve {'marca': None|'DANADA'|'BAJA', 'acta': {...}|None} para una serie."""
+ carpeta = os.path.join(BASE, "dano")
+ ultimo = None
+ # listar_serie() ordena por nombre = cronológico (los repetibles llevan microsegundos).
+ # Manda la ÚLTIMA acta registrada, no la de fecha de visita más reciente: si alguien
+ # captura hoy un daño de la semana pasada, hoy es cuando la serie se marca.
+ for fn in listar_serie("dano", serie) if os.path.isdir(carpeta) else []:
+ try:
+ with open(os.path.join(carpeta, fn), encoding="utf-8") as f:
+ d = json.load(f)
+ except Exception:
+ continue
+ if "PRUEBA" in serie.upper():
+ continue
+ ultimo = {"archivo": fn, "fecha": d.get("fecha"), "recibido": d.get("_recibido"),
+ "tecnico": d.get("tecnico"), "clasificacion": d.get("clasificacion"),
+ "accion": d.get("accion"), "resumen": d.get("resumen")}
+ if not ultimo:
+ return {"marca": None, "acta": None}
+ acc = str(ultimo.get("accion") or "")
+ if acc.startswith("Baja"):
+ return {"marca": "BAJA", "acta": ultimo}
+ if acc.startswith("Reparada"):
+ return {"marca": None, "acta": ultimo}
+ return {"marca": "DANADA", "acta": ultimo}
+
+
+@app.get("/api/estado")
+def estado():
+ """Marca de daño/baja de una serie — lo consultan TODOS los formularios al elegirla."""
+ serie = slug(request.args.get("serie"), 24)
+ if not serie:
+ return jsonify(ok=False, error="falta serie"), 400
+ e = estado_serie(serie)
+ return jsonify(ok=True, serie=serie, marca=e["marca"], acta=e["acta"])
+
+
+@app.get("/api/danos")
+def danos():
+ """Todas las series con marca vigente (dañada o baja), para el tablero."""
+ out = []
+ carpeta = os.path.join(BASE, "dano")
+ if os.path.isdir(carpeta):
+ vistas = set()
+ for fn in sorted(os.listdir(carpeta)):
+ if fn.endswith(".json"):
+ ser = fn.split("_2")[0] if "_2" in fn else fn[:-5]
+ vistas.add(ser)
+ for ser in sorted(vistas):
+ e = estado_serie(ser)
+ if e["marca"]:
+ out.append({"serie": ser, "marca": e["marca"], "acta": e["acta"]})
+ return jsonify(ok=True, total=len(out), series=out)
+
+
if __name__ == "__main__":
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081")))
diff --git a/docs/actas/index.md b/docs/actas/index.md
index 2b6590e..456feef 100644
--- a/docs/actas/index.md
+++ b/docs/actas/index.md
@@ -49,6 +49,12 @@ visitas anteriores antes de empezar. Permite **subir fotos** desde el teléfono.
Procedimiento: [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) ·
Seguimiento: [Tablero de campo](../diagnostico/tablero-campo.html).
+**[▶ Acta de daño o incidente](../formularios/dano.html)** — cualquier serie. **Marca** la serie: todos los
+formularios avisan al elegirla. Se levanta con «Reparada»; «Baja» la hace permanente.
+
+**[▶ Acta de envío](../formularios/envio.html)** · **[▶ Acta de recepción](../formularios/recepcion.html)** — el
+tramo entre el banco y el puerto. Mapa completo: [Ciclo de vida](../procedimientos/ciclo-de-vida.md).
+
## Cómo se usan
1. Abre el formulario y **elige el número de serie** (o los módulos, en integración y ensamble).
diff --git a/docs/descargas/SHA256SUMS.txt b/docs/descargas/SHA256SUMS.txt
index 8da6d06..0c50b1e 100644
--- a/docs/descargas/SHA256SUMS.txt
+++ b/docs/descargas/SHA256SUMS.txt
@@ -1,4 +1,4 @@
-035ebeb348461765f099341603a9e99b94512f8477509efb78aa7caf953f4c2f hbox-fw-v3_3.0.4.bin
-a68a87ff1f6b34a8284e9e5e32c95da7bb8d8adc071c5fd64412a3e744e46b28 hbox-fw-v4_4.0.0.bin
+035ebeb348461765f099341603a9e99b94512f8477509efb78aa7caf953f4c2f hbox-tarjetaV3_3.0.4.bin
+a68a87ff1f6b34a8284e9e5e32c95da7bb8d8adc071c5fd64412a3e744e46b28 hbox-tarjetaV3_4.0.0.bin
a8de74b3d7768e9d5656ae0bff05475445c0cd2599ecfdc4ab081e517ed3d6ef hbox-v5-bringup_0.4.0.bin
e1f351084e803306c376989b284ca0f9f9f1a3445e59494f8a5ff7a9652ad44c hbox-v5-bringup_0.5.1.bin
diff --git a/docs/descargas/hbox-fw-v3_3.0.4.bin b/docs/descargas/hbox-tarjetaV3_3.0.4.bin
similarity index 100%
rename from docs/descargas/hbox-fw-v3_3.0.4.bin
rename to docs/descargas/hbox-tarjetaV3_3.0.4.bin
diff --git a/docs/descargas/hbox-fw-v4_4.0.0.bin b/docs/descargas/hbox-tarjetaV3_4.0.0.bin
similarity index 100%
rename from docs/descargas/hbox-fw-v4_4.0.0.bin
rename to docs/descargas/hbox-tarjetaV3_4.0.0.bin
diff --git a/docs/descargas/index.md b/docs/descargas/index.md
index b2fce1c..63804ba 100644
--- a/docs/descargas/index.md
+++ b/docs/descargas/index.md
@@ -21,19 +21,31 @@ Cada carga se registra en el **[acta de reflasheo](../formularios/reflasheo.html
Un binario incompleto o de otra generación puede dejar la tarjeta inservible.
`shasum -a 256 archivo` (macOS) · `sha256sum archivo` (Linux) · `certutil -hashfile archivo SHA256` (Windows).
-## Firmware del HBOX (por generación de tarjeta)
+## Firmware del HBOX — qué binario va en qué tarjeta
-| Generación | Archivo | Para qué sirve |
-|---|---|---|
-| **V3** — flota desplegada (RTK-003, RTK-006, RTK-014) | ⚠ [`hbox-fw-v3_3.0.4.bin`](hbox-fw-v3_3.0.4.bin) — compilado, **sin prueba de banco** | Mismo comportamiento que el 3.0.3 de campo, compilado con el core 3.x |
-| **V4** | ⚠ [`hbox-fw-v4_4.0.0.bin`](hbox-fw-v4_4.0.0.bin) — compilado, **sin prueba de banco** | Telemetría en banda: el `$PHLG` y el `$PHEV` llegan al servidor por el módem |
-| **V5** — EinarBoard v5 | ✅ `hbox-v5-bringup_0.5.1.bin` (abajo) | Bring-up de tarjeta en banco |
+**La versión del firmware no dice nada de la tarjeta.** `0.3.0` no es "para V3" ni `0.4.0` "para V4":
+son versiones viejas del firmware de **V5**. El binario lleva la tarjeta en el nombre y eso es lo que manda.
+
+| Tarjeta | MCU | Firmware (código) | Binario vigente |
+|---|---|---|---|
+| **V3** — EinarBoard 1.0, las 3 unidades en campo | Blackpill en zócalo (F411CE) | `blackpillrtkcode_v3` · `_v4` | [`hbox-tarjetaV3_3.0.4.bin`](hbox-tarjetaV3_3.0.4.bin) ⚠ sin prueba de banco |
+| **V3** con telemetría en banda | Blackpill en zócalo | `blackpillrtkcode_v4` (4.0.0) | [`hbox-tarjetaV3_4.0.0.bin`](hbox-tarjetaV3_4.0.0.bin) ⚠ sin prueba de banco |
+| **V4** — `hbox-watchdog-v4` | Blackpill en zócalo + PCF8574 · W5500 · SC16IS752 | **ninguno** | — |
+| **V5** — `hbox-watchdog-v5`, lote de 15 | STM32F411RET6 soldado | `hbox_v5_bringup` | [`hbox-v5-bringup_0.5.1.bin`](hbox-v5-bringup_0.5.1.bin) ✅ **en despliegue** |
+
+!!! danger "Lo que NO se puede cruzar"
+ - Un binario de **V5** en una **V3 o V4**: es otro MCU (soldado vs Blackpill), otro reloj y otro mapa
+ de pines. Lo más probable es que ni enumere por USB.
+ - `blackpillrtkcode` en una **V4**: maneja los LEDs de carga en PB12–PB15, que en la V4 son el SPI
+ del W5500. La V4 **no tiene firmware todavía**; hasta que lo tenga, no se flashea con nada.
+ - `hbox-v5-bringup_0.4.0` (o 0.3.0) en cualquier tarjeta: son versiones **superadas** de V5, sin
+ watchdog, ventilador ni recuperación de enlace. Solo se conservan por trazabilidad.
### Firmware de bring-up — EinarBoard v5
| Archivo | Versión | SHA-256 | Uso |
|---|---|---|---|
-| [**hbox-v5-bringup_0.4.0.bin**](hbox-v5-bringup_0.4.0.bin) | **0.4.0** (2026-09-17) — **campo, sin telemetría en banda** (probado en unidad) | `a8de74b3d7768e9d…` ([SHA256SUMS](SHA256SUMS.txt)) |
+| [`hbox-v5-bringup_0.4.0.bin`](hbox-v5-bringup_0.4.0.bin) | 0.4.0 (2026-09-17) — **superada**, sin telemetría en banda | `a8de74b3d7768e9d…` ([SHA256SUMS](SHA256SUMS.txt)) |
| [**hbox-v5-bringup_0.5.1.bin**](hbox-v5-bringup_0.5.1.bin) | **0.5.1** (2026-09-18) — **campo + Modo B / telemetría — EN PRUEBA** (reportado: etapa RTK no enciende) | `e1f351084e803306…` | Sección 5 del [procedimiento de prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md) y sección 5 del [ensamble](../procedimientos/v5-ensamble.md) (modo de banco `P` / `n`) |
Flasheo por DFU en **J11** (mantener **SW2**, pulsar y soltar **SW1**, soltar SW2):
@@ -44,14 +56,13 @@ dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.5.1.bin
o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x08000000`.
-!!! danger "18-sep, tarde: A/B entre 0.4.0 y 0.5.1"
- Se reportó que con **0.5.1** la etapa RTK no enciende y con **0.4.0** sí. Hasta cerrar el diagnóstico:
- **embarcar con 0.4.0**. Para diagnosticar 0.5.1 en UNA unidad: consola en J11, tap a SW1, guardar todo desde el banner
- hasta 2 min después; luego teclear `s`, `v` y `B` (bloquea Modo B) y anotar si la etapa enciende con `B`.
- Mandar el log completo — el diagnóstico sale de ahí, no del LED.
+!!! success "A/B 0.4.0 vs 0.5.1 — cerrado el 19-sep"
+ El 18-sep por la tarde se reportó que con 0.5.1 la etapa RTK no encendía. **Resuelto:** 0.5.1 ensambla,
+ enciende y reporta telemetría completa en el monitor desplegado. **Se embarca con 0.5.1.** `0.4.0`
+ sigue descargable solo como referencia de versiones anteriores.
!!! note "Tarjetas ya flasheadas con cualquier versión anterior"
- Re-flashear con 0.4.0 (o 0.5.1 solo para la prueba A/B) por el mismo procedimiento DFU. **La primera línea de la consola tras reset dice la versión**
+ Re-flashear con **0.5.1** por el mismo procedimiento DFU. **La primera línea de la consola tras reset dice la versión**
(`hbox-v5-bringup 0.5.1 built …`): si la línea `STAT` no termina en `stack=…`, la tarjeta sigue con 0.1.1.
!!! tip "Consola 0.5.1"
@@ -83,21 +94,19 @@ o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x
Notas de compilación y cambios: [README del firmware de bring-up](hbox-v5-bringup-README.md).
-### Firmware de campo — tarjetas V3 y V4 (detalle)
+### Firmware de campo — tarjeta V3 (detalle)
!!! danger "Estos binarios todavía NO se flashean en una unidad de campo"
Compilaron limpio el 19-sep y pasan el simulador, pero **ninguno ha corrido sobre una
EinarBoard 1.0 real**. Hasta que ingeniería los pruebe en banco (RTK-003 o RTK-006) y quite
- este aviso, solo se usan en la mesa de ingeniería. Esto aplica sobre todo a **V4**, que pone
+ este aviso, solo se usan en la mesa de ingeniería. Esto aplica sobre todo al **4.0.0**, que pone
el MCU en línea con el enlace que lleva las correcciones RTK.
-!!! warning "V4 y el A/B pendiente de 0.5.1"
- El fallo de **0.5.1** en v5 (la etapa RTK no enciende) tiene como sospechoso principal al
- **Modo B**, y **v4 activa el Modo B por omisión**. Hasta que el A/B de v5 dé una causa, la
- prueba de banco de v4 se hace **en par**: el binario publicado (Modo B encendido) y uno
- compilado sin Modo B, sobre la misma unidad, comparando si la etapa enciende y qué dice
- `v5min` en el momento en que K1 se energiza — una caída ahí sería la bobina de K1 hundiendo
- el riel hasta el PFI del LTC4040.
+!!! tip "Prueba de banco del 4.0.0: en par"
+ El 4.0.0 activa el Modo B por omisión. Aunque el A/B de V5 ya cerró bien, la primera prueba en
+ una V3 real se hace **en par**: el binario publicado y uno compilado con `-DENABLE_MODOB=0`,
+ sobre la misma unidad. Si el fix RTK cae al energizar K1, o `v5min` se hunde en ese instante,
+ el sospechoso es la bobina de K1 llevando el riel al PFI del LTC4040.
```bash
arduino-cli compile --fqbn "$FQBN" --build-property "compiler.cpp.extra_flags=-DENABLE_MODOB=0" \
--output-dir build/v4-sinB blackpillrtkcode_v4
@@ -108,8 +117,8 @@ bring-up del v5. Se usa en el [reflasheo en taller](../procedimientos/reflasheo-
| Generación | Archivo | Estado |
|---|---|---|
-| **V3** — flota desplegada | [`hbox-fw-v3_3.0.4.bin`](hbox-fw-v3_3.0.4.bin) · 62 464 B · `035ebeb3…` | ⚠ **compilado 19-sep, falta prueba de banco** |
-| **V4** | [`hbox-fw-v4_4.0.0.bin`](hbox-fw-v4_4.0.0.bin) · 64 352 B · `a68a87ff…` | ⚠ **compilado 19-sep, falta prueba de banco**. Validado en el simulador de host |
+| **V3** — flota desplegada | [`hbox-tarjetaV3_3.0.4.bin`](hbox-tarjetaV3_3.0.4.bin) · 62 464 B · `035ebeb3…` | ⚠ compilado 19-sep, falta prueba de banco |
+| **V3** con telemetría | [`hbox-tarjetaV3_4.0.0.bin`](hbox-tarjetaV3_4.0.0.bin) · 64 352 B · `a68a87ff…` | ⚠ compilado 19-sep, falta prueba de banco. El «4» es la versión del firmware, **no** la tarjeta |
!!! danger "Mientras diga «pendiente», no hay binario aprobado"
No flashees una unidad V3 o V4 con un archivo que no esté publicado aquí con su SHA-256. Los
@@ -182,7 +191,7 @@ binario mal compilado deja unidades muertas en el patio.
- [ ] Compilado con el `TARGET_` correcto para esa generación de tarjeta.
- [ ] **Probado en banco sobre hardware real**, no solo compilado sin errores.
- [ ] Verificada la polaridad del control de carga sobre la tarjeta.
-- [ ] Nombre con versión: `hbox-fw-v3_X.Y.Z.bin`.
+- [ ] Nombre con **tarjeta y versión**: `hbox-tarjetaV3_X.Y.Z.bin`. La versión del firmware nunca se infiere de la tarjeta ni al revés.
- [ ] Su línea en `SHA256SUMS.txt`.
- [ ] Anotado en el `CHANGELOG.md` del repositorio de firmware, con fecha y quién lo probó.
- [ ] Actualizada la tabla de esta página.
diff --git a/docs/diagnostico/tablero-campo.html b/docs/diagnostico/tablero-campo.html
index 519be9e..f949188 100644
--- a/docs/diagnostico/tablero-campo.html
+++ b/docs/diagnostico/tablero-campo.html
@@ -138,6 +138,7 @@ function render(actas){
title:'Ver el expediente completo',text:u.serie})),
el('div',{class:'m',text:(u.maquina||'')+(u.nota?' · '+u.nota:'')})));
const [cls,txt] = vs.length ? pill(vs[0]) : ['pend','Sin diagnóstico'];
+ if(DANOS[u.serie]) cab.append(el('span',{class:'pill falla',text:DANOS[u.serie].marca==='BAJA'?'⛔ BAJA':'⚠ DAÑADA'}));
cab.append(el('span',{class:'pill '+cls,text:txt}));
box.append(cab);
@@ -178,9 +179,11 @@ function render(actas){
});
}
+let DANOS = {};
async function cargar(){
document.getElementById('meta').textContent = 'Cargando…';
try{
+ try{ const d=await (await fetch('/api/danos',{cache:'no-store'})).json(); DANOS={}; (d.series||[]).forEach(x=>DANOS[x.serie]=x); }catch(e){}
const r = await fetch('/api/actas',{cache:'no-store'});
const j = await r.json();
render(j.actas||[]);
diff --git a/docs/formularios/acta.css b/docs/formularios/acta.css
index 28552ee..732e2f4 100644
--- a/docs/formularios/acta.css
+++ b/docs/formularios/acta.css
@@ -129,3 +129,9 @@ button.mini-btn{background:#fff;border:1px solid var(--linea);border-radius:6px;
.mini.doc .est{font-size:11.5px;flex:0 0 auto}
input[type=date]{width:100%;padding:11px 12px;border:1px solid var(--linea);border-radius:9px;
font-size:16px;background:#fff;color:var(--tinta)}
+
+/* marca de daño / baja: se muestra en cualquier acta al elegir la serie */
+.marca{display:none;margin:0 0 14px;padding:12px 14px;border-radius:10px;font-size:14.5px;line-height:1.45}
+.marca.danada{display:block;background:var(--aviso-bg);border:2px solid var(--aviso);color:#7a4a00}
+.marca.baja{display:block;background:var(--falla-bg);border:2px solid var(--falla);color:#8a1a1a}
+.marca a{color:inherit;font-weight:700}
diff --git a/docs/formularios/acta.js b/docs/formularios/acta.js
index 79a38c7..a61394d 100644
--- a/docs/formularios/acta.js
+++ b/docs/formularios/acta.js
@@ -131,6 +131,37 @@
wrap.append(banner);
const hist = $('div', { class: 'hist' });
if (cfg.repetible) wrap.append(hist);
+ // Marca de daño: la consultan TODOS los formularios (banco, campo, produccion).
+ // Una tarjeta con corto no debe poder entrar a un ensamble sin que alguien lo vea.
+ const marcaBox = $('div', { class: 'marca' });
+ wrap.append(marcaBox);
+ async function consultarMarca() {
+ marcaBox.innerHTML = ''; marcaBox.className = 'marca';
+ if (cfg.tipo === 'dano') return;
+ // En un acta compuesta (ensamble, integración) se revisa CADA módulo elegido, no solo
+ // la unidad calculada: una tarjeta con corto no debe poder entrar a un ensamble.
+ const series = compuesta ? Object.values(seleccion()).filter(v => v) : [serieActual()];
+ const aRevisar = series.filter(v => v && !esPrueba(v));
+ if (!aRevisar.length) return;
+ const avisos = [];
+ await Promise.all(aRevisar.map(async s => {
+ try {
+ const j = await (await fetch(`/api/estado?serie=${encodeURIComponent(s)}`, { cache: 'no-store' })).json();
+ if (j.marca) avisos.push({ s, j });
+ } catch (e) {}
+ }));
+ if (!avisos.length) return;
+ const baja = avisos.some(x => x.j.marca === 'BAJA');
+ marcaBox.className = 'marca ' + (baja ? 'baja' : 'danada');
+ marcaBox.innerHTML = avisos.map(({ s, j }) => {
+ const a = j.acta || {};
+ return (j.marca === 'BAJA' ? '⛔ ' + s + ' está dada de BAJA' : '⚠ ' + s + ' está marcada como DAÑADA')
+ + (a.fecha ? ' desde el ' + a.fecha : '') + (a.clasificacion ? ' — ' + a.clasificacion : '')
+ + (a.resumen ? '
' + a.resumen : '')
+ + '
Ver su expediente'
+ + (j.marca === 'BAJA' ? '. No debe usarse ni instalarse.' : '. Revisa el acta de daño antes de continuar.');
+ }).join('