diff --git a/DESPLIEGUE.md b/DESPLIEGUE.md
index 33737dd..7c39994 100644
--- a/DESPLIEGUE.md
+++ b/DESPLIEGUE.md
@@ -15,6 +15,7 @@ Servidor: `git.helgen.online` · dominio público: `https://drogheda.helgen.onli
| **Sitios** | `/srv/projects/drogheda/sitios` | las escribe el backend — **13-oct** |
| **Plan de producción** | `/srv/projects/drogheda/plan-produccion.json` | lo escribe el backend (admin) — **13-oct** |
| **Equipo (miembros, bitácora de accesos)** | `/srv/projects/drogheda/equipo` | las escribe el backend — **14-oct** |
+| **Enfoque de la semana (página de inicio)** | `/srv/projects/drogheda/enfoque.json` | lo escriben Leo y Adriana desde el inicio — **21-oct** |
| Usuarios (HTTP Basic) | `/etc/nginx/hbox-docs.htpasswd` | `htpasswd` |
**Lo importante:** `deploy.sh` publica **solo el sitio**. Si cambió `backend/app.py` —como en esta
@@ -260,6 +261,38 @@ pendiente sin responsable debe rechazarse; `backend/tests-1014.sh` reproduce tod
---
+## A4 · 21-oct: página de inicio, enfoque de la semana y actividad
+
+El inicio deja de ser una página escrita a mano: lee `docs/datos/proyecto.json` (fase, ruta a
+producción, fechas, sitios), el tablero y dos rutas nuevas del backend:
+
+- `GET /api/hoy` · `PUT /api/hoy`: el enfoque de la semana. Lo escriben `HOY_EDITORES` (por omisión
+ `leo,adriana`) y los administradores; se guarda en `enfoque.json` junto a `/actas`, con sus 20
+ versiones anteriores.
+- `GET /api/actividad?dias=7&limite=30`: lo que pasó, del historial de los pendientes y de las actas
+ recibidas (sin las series PRUEBA). La usan el inicio y la Bitácora.
+
+No hay carpetas ni variables nuevas que fijar (las dos vienen comentadas en el `.service`). Como
+cambió `backend/app.py`, después del deploy hay que reiniciar el servicio:
+
+```bash
+# Mac
+cd ~/Documents/Claude/Projects/Drogheda/hbox-docs && ./scripts/deploy.sh
+ssh leo@git.helgen.online 'sudo systemctl restart actas-hbox'
+
+# verificar (en el servidor)
+curl -s -H 'X-Remote-User: leo' http://127.0.0.1:8081/api/hoy
+curl -s -H 'X-Remote-User: leo' 'http://127.0.0.1:8081/api/actividad?dias=7&limite=3' | head -c 400
+```
+
+| Síntoma | Causa | Arreglo |
+|---|---|---|
+| En el inicio, el enfoque dice «El servidor no respondió» y la actividad, «HTTP 404» | backend viejo | el `restart` de arriba |
+| «El enfoque de la semana lo escriben adriana y leo» al guardar | ese usuario no está en `HOY_EDITORES` | así debe ser; para agregar a alguien, `Environment=HOY_EDITORES=leo,adriana,…` en el `.service` |
+| `PermissionError` al guardar el enfoque | `/srv/projects/drogheda` no es de `leo` | `chown` del paso 2 de A2 |
+
+---
+
## B · Verificación después de desplegar
```bash
diff --git a/backend/actas-hbox.service b/backend/actas-hbox.service
index 3a68294..ad66220 100644
--- a/backend/actas-hbox.service
+++ b/backend/actas-hbox.service
@@ -20,6 +20,9 @@ Environment=ACTAS_ADMINS=leo
#Environment=PLAN_FILE=/srv/projects/drogheda/plan-produccion.json
#Environment=EQUIPO_DIR=/srv/projects/drogheda/equipo
#Environment=HTPASSWD_LOCK=/srv/projects/drogheda/.htpasswd.lock
+# Página de inicio (21-oct): quién escribe el enfoque de la semana y dónde se guarda.
+#Environment=HOY_EDITORES=leo,adriana
+#Environment=HOY_FILE=/srv/projects/drogheda/enfoque.json
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
Environment=PASS_MIN=8
Environment=PORT=8081
diff --git a/backend/app.py b/backend/app.py
index 96b02ec..abe1bd0 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -37,6 +37,11 @@ autenticado crea y modifica, solo un administrador borra:
Equipo y accesos (14-oct): los miembros del equipo NO son lo mismo que los usuarios con login.
GET /api/equipo · POST /api/equipo (admin) · PATCH /api/equipo/miembro?id= (admin)
POST /api/usuarios (admin: crea login en el htpasswd + miembro) · DELETE /api/usuario?u= (admin: quita el login)
+Inicio (21-oct):
+ GET /api/hoy · PUT /api/hoy (solo HOY_EDITORES, por omisión leo y adriana, y los administradores):
+ el enfoque de la semana que se ve en la página de inicio; guarda las 20 versiones anteriores.
+ GET /api/actividad?dias=7&limite=30 → lo que pasó: cambios de pendientes (de su historial) y actas
+ recibidas, del más nuevo al más viejo. Las series PRUEBA no cuentan.
"""
import os, re, json, csv, datetime, uuid, hashlib, unicodedata, fcntl
from flask import Flask, request, jsonify, abort, send_file
@@ -1945,5 +1950,97 @@ def sitio_unidades():
total_actas=len(actas), danos=[u for u in lista if u["marca"]], admin=is_admin())
+# ------------------------------------------------------------------ inicio: enfoque de la semana y actividad (21-oct)
+# El enfoque es un texto corto que escriben Leo y Adriana (HOY_EDITORES) y se ve arriba en la página de
+# inicio. La actividad junta lo que ya guardan los pendientes (historial) y las actas (_recibido): no
+# hay bitácora aparte que mantener.
+HOY_FILE = os.environ.get("HOY_FILE", os.path.join(DATOS_BASE, "enfoque.json"))
+HOY_EDITORES = {u.strip().lower() for u in os.environ.get("HOY_EDITORES", "leo,adriana").split(",") if u.strip()}
+
+
+def _puede_enfoque():
+ u = current_user().lower()
+ return bool(u) and (u in HOY_EDITORES or u in ADMINS)
+
+
+@app.get("/api/hoy")
+def hoy_leer():
+ h = _leer_json(HOY_FILE)
+ if not isinstance(h, dict) or not h.get("titulo"):
+ h = None
+ else:
+ h = {k: h.get(k) for k in ("titulo", "puntos", "editado_por", "editado_en")}
+ return jsonify(ok=True, enfoque=h, puede_editar=_puede_enfoque(), editores=sorted(HOY_EDITORES),
+ user=current_user())
+
+
+@app.put("/api/hoy")
+def hoy_escribir():
+ u = quien()
+ if not u:
+ return _sin_usuario()
+ if not _puede_enfoque():
+ return jsonify(ok=False, error="El enfoque de la semana lo escriben " + " y ".join(sorted(HOY_EDITORES))
+ + ". Pídeselo a uno de ellos."), 403
+ d = _json_body()
+ if d is None:
+ return jsonify(ok=False, error="JSON inválido"), 400
+ titulo = _texto(d.get("titulo"), 160)
+ if not titulo:
+ return jsonify(ok=False, error="Falta la frase de la semana."), 400
+ puntos = d.get("puntos")
+ if isinstance(puntos, str):
+ puntos = puntos.split("\n")
+ puntos = [_texto(p, 400) for p in (puntos if isinstance(puntos, list) else []) if _texto(p, 400)][:8]
+ base = _leer_json(HOY_FILE) or {}
+ anteriores = list(base.get("historial") or [])
+ if base.get("titulo"):
+ anteriores.append({k: base.get(k) for k in ("titulo", "puntos", "editado_por", "editado_en")})
+ h = {"titulo": titulo, "puntos": puntos, "editado_por": u, "editado_en": ahora(), "historial": anteriores[-20:]}
+ _escribir_json(HOY_FILE, h)
+ return jsonify(ok=True, enfoque={k: h[k] for k in ("titulo", "puntos", "editado_por", "editado_en")})
+
+
+@app.get("/api/actividad")
+def actividad():
+ dias = max(1, min(_entero(request.args.get("dias"), 7), 60))
+ limite = max(1, min(_entero(request.args.get("limite"), 30), 200))
+ desde = (datetime.datetime.now() - datetime.timedelta(days=dias)).replace(microsecond=0).isoformat()
+ eventos = []
+ for t in _listar_json(TAREAS_DIR):
+ for h in (t.get("historial") or []):
+ cuando = str(h.get("cuando") or "")
+ if cuando >= desde:
+ eventos.append({"tipo": "pendiente", "cuando": cuando, "quien": h.get("quien") or "",
+ "cambio": _texto(h.get("cambio"), 300), "id": t.get("id"),
+ "titulo": t.get("titulo"), "pista": t.get("pista"), "estado": t.get("estado")})
+ if os.path.isdir(BASE):
+ for tipo in sorted(os.listdir(BASE)):
+ carpeta = os.path.join(BASE, tipo)
+ if tipo.startswith((".", "_")) or not os.path.isdir(carpeta):
+ continue
+ for fn in os.listdir(carpeta):
+ ruta = os.path.join(carpeta, fn)
+ if not fn.endswith(".json"):
+ continue
+ try: # filtro barato antes de abrir el archivo
+ if datetime.datetime.fromtimestamp(os.path.getmtime(ruta)).isoformat() < desde:
+ continue
+ except OSError:
+ continue
+ d = _leer_json(ruta)
+ if not isinstance(d, dict) or d.get("_prueba"):
+ continue
+ cuando = str(d.get("_recibido") or "")
+ if cuando < desde:
+ continue
+ eventos.append({"tipo": "acta", "cuando": cuando, "quien": d.get("_usuario") or "",
+ "tecnico": _texto(d.get("tecnico"), 80), "acta": tipo, "serie": d.get("serie"),
+ "resultado": d.get("resultado") or d.get("clasificacion") or ""})
+ eventos.sort(key=lambda e: e["cuando"], reverse=True)
+ equipo = {k: m.get("nombre") for k, m in _miembros().items()}
+ return jsonify(ok=True, desde=desde, total=len(eventos), eventos=eventos[:limite], equipo=equipo)
+
+
if __name__ == "__main__":
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081")))
diff --git a/docs/bitacora.md b/docs/bitacora.md
index cc3ff6a..e05c88a 100644
--- a/docs/bitacora.md
+++ b/docs/bitacora.md
@@ -1,16 +1,33 @@
---
title: "Bitácora"
-description: Los documentos del proyecto por fecha, del más nuevo al más viejo.
+description: Lo que pasó en el tablero y en las actas, y los documentos del proyecto por fecha.
+template: tablero.html
+ancho: 52rem
---
+
+
# Bitácora
-Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra solo lo vigente; aquí queda todo, incluido lo superado. Lo que pasa cada día en el tablero (quién movió qué) se verá aquí cuando el servidor lo publique; por ahora vive en el historial de cada [pendiente](tableros/pendientes.md).
+Arriba, lo que pasó en el tablero de pendientes y en las actas en los últimos 14 días: sale solo del historial de cada [pendiente](tableros/pendientes.md) y de las actas recibidas. Abajo, los documentos del proyecto por fecha; el menú lateral muestra solo lo vigente y aquí queda todo, incluido lo superado.
+
+## Actividad { #actividad }
+
+
…
+
+## Documentos por fecha { #documentos-por-fecha }
!!! info "Cómo se agrega un documento"
- Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado».
+ Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado». Si el documento trae `date:` en su encabezado, también sale solo en «Documentos recientes» de la página de inicio.
-## Octubre de 2026
+### Octubre de 2026
+
+**21 oct (noche)**
+: [Estado de la v6 y T25/T28 aplicados](hardware/v6-estado-21oct.md). T28 cerró 2 de los 8 bloqueantes; D34 quedó lejos del pin SW de U25 (lazo de ≈ 115 mm²) y MH8 cae encima de D34. Falta el push de Leo.
+: [Pasada 5 revisada](hardware/v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta): el sensado de RS1 dejó de ser Kelvin en las dos tarjetas; falta la pasada 6, ≈ 30 min por tarjeta.
+
+**21 oct**
+: [Auditoría de la pasada 4 de Michelle: V5.6 y V5.7](hardware/v56-v57-auditoria-21oct.md). El corto de R106 y lo que no pide mover piezas ya quedó con T32 y T33 (C130 aprobado por Leo); a Michelle le tocó la pasada 5.
**19 oct (tarde)**
: [V5.6: qué es crítico y qué se puede quedar](hardware/v56-clasificacion-19oct.md). Un solo crítico, nuevo: C129 en VSYS de U6. L7 y lo heredado de la V5.5 se quedan para el piloto y se miden en el banco.
@@ -43,7 +60,7 @@ Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra
**1 oct**
: [Hoja de datos de la V5.6](hardware/v56-datasheet.md).
-## Septiembre de 2026
+### Septiembre de 2026
**29 sep**
: [Pinout de la V5.6](hardware/v56-pinout.md) y [reglas de layout, del esquemático a la PCB](hardware/v56-layout-instrucciones.md).
@@ -63,3 +80,10 @@ Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra
: [Auditoría pre-fabricación de la v6](hardware/v6-auditoria-prefab.md) ([en inglés](hardware/v6-prefab-audit.md)) (superada por la del 28 sep).
Lo anterior a septiembre está en la [historia del proyecto](historia/index.md).
+
+
+
+
+
diff --git a/docs/datos/proyecto.json b/docs/datos/proyecto.json
new file mode 100644
index 0000000..14e810c
--- /dev/null
+++ b/docs/datos/proyecto.json
@@ -0,0 +1,74 @@
+{
+ "_como": "Datos fijos de la página de inicio y del tablero de Ingeniería (Ruta a producción y una tarjeta por versión). Los actualiza Claude o Leo cuando una tarjeta cambia de etapa o se mueve un hito; después, deploy. Estados de cada paso: hecho, ahora, bloqueado, falta. Cada paso es [estado, texto, debajo]. Tono de la tarjeta: hecho, ahora, aviso, bloqueado. Hitos: fecha AAAA-MM-DD (null si aún no hay fecha), fin opcional y «cuando», el texto que se ve. El enfoque de la semana NO va aquí: lo escriben Leo y Adriana en la página de inicio (/api/hoy); enfoque_inicial solo se muestra mientras nadie lo ha escrito.",
+ "actualizado": "2026-10-21",
+ "fase": { "nombre": "Pre-producción", "texto": "V5.6 y V5.7 en la pasada 6; la v6 en layout" },
+ "pedido_nota": "primero los pilotos: 5 V5.6 y 5 V5.7, luego 20 v6",
+ "gestion": { "id": "adriana", "texto": "Adriana lleva la gestión del proyecto" },
+ "etapas": ["Esquemático", "Layout", "Auditoría", "Gerber", "Piloto", "Banco", "Pedido"],
+ "tarjetas": [
+ {
+ "version": "V5.5", "clave": "v55", "estado": "En campo", "tono": "hecho",
+ "nuevo": "15 piezas; ya no se fabrica",
+ "imagen": "/img/tarjetas/v55.webp", "alt": "Cobre de la EinarBoard V5.5",
+ "pasos": [["hecho"], ["hecho"], ["hecho", "EMC 22 ago"], ["hecho"], ["hecho"], ["hecho", "0.6.0 en campo"], ["hecho", "Ago", "15 piezas"]],
+ "detalle": "En campo: 15 piezas (lote del 22-ago), firmware 0.6.x.",
+ "siguiente": "Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a la V5.6. Revisar U6 con lupa en cada tarjeta. Jesús mide en una unidad el límite de entrada del cargador con TP1 (≈ 1 A, auditoría del 16-oct, §4.5).",
+ "quien": "Jesús (pruebas) · Alán y Daniel (campo)",
+ "docs": [["Manual V5", "/manuales/manual-tarjeta-V5/"], ["Auditoría del 16-oct, §4", "/hardware/auditoria-produccion-16oct/"], ["Lecciones de la V5.5", "/hardware/v6-revision-lecciones-v55/"], ["Acta de tarjeta V5", "/formularios/v5.html"]]
+ },
+ {
+ "version": "V5.6", "clave": "v56", "estado": "Casi lista", "tono": "ahora",
+ "nuevo": "Lo nuevo: control del módem y acelerómetro. Pedido de 113",
+ "imagen": "/img/tarjetas/v56.webp", "alt": "Cobre de la EinarBoard V5.6; lo nuevo, en blanco",
+ "pasos": [["hecho", "28 sep"], ["ahora", "Pasada 6", "Michelle, ≈ 30 min"], ["falta", "Revisión del cobre", "Claude"], ["falta", "Gerber y BOM", "Leo y Claude"], ["falta", "5 piezas", "con las 5 de la V5.7"], ["falta", "Firmware 0.7.0", "Jesús; por escribir"], ["falta", "113"]],
+ "detalle": "Casi lista (21-oct, noche): la pasada 5 (6458c5d) dejó bien C129 y C130, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: el sensado ya no es Kelvin. Falta la pasada 6, ≈ 30 min.",
+ "siguiente": "Pasada 6 (Michelle, ≈ 30 min): la vía de entrada de RS1 a 0.2 mm o más del hilo de sensado, con una 2.ª vía unida en F.Cu y en B.Cu; 2.ª vía de la entrada de U7; exclusiones de MH7, MH9 y MH10; el tramo suelto de /RS485_B → revisión del cobre (Claude, sobre todo el sensado de RS1) → Gerber, BOM y CPL → pedido del piloto de 5, junto con los 5 de la V5.7 → banco con el firmware 0.7.0 → las 108 restantes, con SD-15B.",
+ "quien": "Michelle (layout) · Claude (revisión) · Leo (scripts, pedido, firmware) · Jesús (banco)",
+ "docs": [["Lo que falta: pasada 6 (21-oct, noche)", "/hardware/v56-v57-auditoria-21oct/#5-pasada-5-revisada-6458c5d-y-lo-que-falta"], ["Auditoría de la pasada 4 (21-oct)", "/hardware/v56-v57-auditoria-21oct/"], ["Qué es crítico (19-oct)", "/hardware/v56-clasificacion-19oct/"], ["Pasada 4", "/hardware/v56-v57-layout-pasada-4/"], ["Re-auditoría del 19-oct", "/hardware/re-auditoria-19oct/"], ["ECO V5.6", "/hardware/v56-eco/"], ["Hoja de datos", "/hardware/v56-datasheet/"], ["Pinout", "/hardware/v56-pinout/"]]
+ },
+ {
+ "version": "V5.7", "clave": "v57", "estado": "No lista", "tono": "aviso",
+ "nuevo": "Lo nuevo: fuente de 9 a 32 V a bordo. Pedido de 327",
+ "imagen": "/img/tarjetas/v57.webp", "alt": "Cobre de la EinarBoard V5.7; la fuente a bordo, en blanco",
+ "pasos": [["hecho", "13 oct"], ["ahora", "Pasada 6", "Michelle, ≈ 30 min"], ["falta", "Revisión del cobre", "Claude"], ["falta", "Con la V5.6", "Leo y Claude"], ["falta", "5 prototipos", "con las 5 de la V5.6"], ["falta", "9 a 32 V", "Jesús: polaridad y calor"], ["falta", "327"]],
+ "detalle": "No lista (21-oct, noche), pero ya sin el bloqueante de K1. La pasada 5 dejó bien C130 y las 2 vías de /VEH5V; quedan RS1 (igual que en la V5.6), C149/C150 sin girar (lazo de ≈ 56 mm², meta < 30) y el GND de C129.",
+ "siguiente": "Pasada 6 (Michelle, ≈ 30 min): RS1 como en la V5.6, 2.ª vía de la entrada de U7, 1 o 2 vías al GND de C129, C149/C150 girados hacia D34 y el EP de U25 (lazo de menos de 30 mm²), exclusiones y el tramo suelto de /24V → revisión del cobre (Claude) → 5 prototipos con el piloto de la V5.6 (ECO §6: 9, 12, 24 y 32 V, polaridad y temperatura) → pedido de 327. Entrada de 9 a 32 V.",
+ "quien": "Michelle (layout) · Claude (revisión) · Leo (pedido) · Jesús (prototipos)",
+ "docs": [["Lo que falta: pasada 6 (21-oct, noche)", "/hardware/v56-v57-auditoria-21oct/#5-pasada-5-revisada-6458c5d-y-lo-que-falta"], ["Auditoría del 21-oct, §3.2", "/hardware/v56-v57-auditoria-21oct/#32-v57-hbox-watchdog-v57"], ["Re-auditoría del 19-oct, §4", "/hardware/re-auditoria-19oct/#4-v57-punto-por-punto"], ["ECO V5.7: fuente a bordo", "/hardware/v57-eco-fuente/"]]
+ },
+ {
+ "version": "v6", "clave": "v6", "estado": "No lista", "tono": "bloqueado",
+ "nuevo": "Lo nuevo: GNSS y 4G en la tarjeta. Pedido de 130",
+ "imagen": "/img/tarjetas/v6.webp", "alt": "Cobre de la EinarBoard v6; el GNSS y el 4G, en blanco",
+ "pasos": [["hecho", "28 sep"], ["bloqueado", "6 bloqueantes", "T28 cerró 2; D34 lejos de U25"], ["falta", "25 oct y 1 nov", "Claude"], ["falta", "1 nov", "Claude (G1)"], ["falta", "20, el 2 nov", "Aldair y Leo"], ["falta", "21–22 nov", "Jesús y Leo (G2)"], ["falta", "130", "después de G2"]],
+ "detalle": "No lista (21-oct): T25 y T28 ya están aplicados (falta el push de Leo) y T28 cerró 2 de los 8 bloqueantes. Michelle adelantó la fuente U25, pero D34 quedó a 13 mm del pin SW (lazo caliente de ≈ 115 mm², antes ≈ 47) y MH8 cae encima de D34.",
+ "siguiente": "Leo: commit y push de T25 y T28; Michelle hace git pull --ff-only antes de abrir la v6. Sáb 24: Michelle regresa D34 junto a U25.8 (moviendo R53 y R54 junto a U6), libera MH8, MH4 y MH6, y sigue con los bloques A y B. Dom 25: bloques C, D y E (módem, GNSS y U12); en la noche, re-auditoría de Claude. Sáb 31: bloques F, G y H1 (pares, puertos serie e In1). Dom 1-nov: H2 y DRC final; en la tarde, re-auditoría final y Gerber (G1). Lun 2-nov: pedido del piloto de 20. Firmware los fines de semana del 7–8 y del 14–15 nov; banco del piloto el 21–22 nov (G2). No habrá v6.1.",
+ "quien": "Michelle (layout) · Leo (scripts y pedido) · Claude (re-auditoría y Gerber) · Aldair (pedido) · Jesús (banco)",
+ "docs": [["Estado de la v6 (21-oct)", "/hardware/v6-estado-21oct/"], ["Plan de fines de semana (rev. 1.1)", "/hardware/v6-plan-fines-de-semana/"], ["Auditoría de la v6 (16-oct)", "/hardware/v6-auditoria-produccion-16oct/"], ["Re-auditoría del 19-oct, §8", "/hardware/re-auditoria-19oct/#8-v6"], ["Plan de corrección (28-sep)", "/hardware/v6-plan-correccion/"], ["Manual v6", "/manuales/manual-tarjeta-v6/"], ["Plan de cobre", "/hardware/EinarBoard-v6_Plan-de-Cobre_ES.html"]]
+ }
+ ],
+ "hitos": [
+ { "fecha": null, "cuando": "Por fijar", "texto": "Pedido del piloto: 5 V5.6 y 5 V5.7, después de la pasada 6", "quien": ["leo"] },
+ { "fecha": "2026-10-24", "fin": "2026-10-25", "cuando": "24 y 25 oct", "texto": "v6, primer fin de semana de layout", "quien": ["michelle", "leo"] },
+ { "fecha": "2026-10-31", "fin": "2026-11-01", "cuando": "31 oct y 1 nov", "texto": "v6, segundo fin de semana y Gerber (G1)", "quien": ["michelle", "leo", "claude"] },
+ { "fecha": "2026-11-02", "cuando": "2 nov", "texto": "Pedido del piloto de 20 v6", "quien": ["aldair", "leo"] },
+ { "fecha": "2026-11-07", "fin": "2026-11-15", "cuando": "7 al 15 nov", "texto": "Firmware de la v6, dos fines de semana", "quien": ["leo", "claude"] },
+ { "fecha": "2026-11-21", "fin": "2026-11-22", "cuando": "21 y 22 nov", "texto": "Banco del piloto de la v6 (G2)", "quien": ["jesus", "leo"] }
+ ],
+ "enfoque_inicial": {
+ "titulo": "Esta semana: la pasada 6 y el piloto de V5.6 y V5.7.",
+ "puntos": [
+ "La pasada 5 dejó bien C129 y C130 en las dos tarjetas. Falta la pasada 6 (≈ 30 min por tarjeta): sacar la vía de RS1 de encima del hilo de sensado, la 2.ª vía de RS1 y de U7 y, en la V5.7, girar C149/C150.",
+ "Después, Claude revisa el cobre (sobre todo el sensado de RS1), Gerber y un solo pedido de 5 y 5.",
+ "v6: T25 y T28 ya están aplicados; falta el push de Leo. El sábado 24 se empieza por D34, que quedó lejos de U25, y por MH8."
+ ],
+ "nota": "Texto inicial tomado de la [auditoría del 21-oct](/hardware/v56-v57-auditoria-21oct/) y del [estado de la v6](/hardware/v6-estado-21oct/). Leo y Adriana lo cambian con «Editar»."
+ },
+ "sitios": [
+ { "id": "icave", "nombre": "ICAVE", "ciudad": "Veracruz", "x": 68.27, "y": 73.55, "etiqueta": "der", "nota": "se compara con el CalAmp" },
+ { "id": "lzc", "nombre": "LZC", "ciudad": "Lázaro Cárdenas", "x": 49.6, "y": 80.34, "etiqueta": "der" },
+ { "id": "eit", "nombre": "EIT", "ciudad": "Ensenada", "x": 9.34, "y": 7.92, "etiqueta": "der" },
+ { "id": "timsa", "nombre": "TIMSA", "ciudad": "Manzanillo", "x": 43.06, "y": 74.67, "etiqueta": "izq" }
+ ],
+ "oficina": { "nombre": "Helgen", "ciudad": "Querétaro", "x": 55.08, "y": 67.1 }
+}
diff --git a/docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png b/docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png
new file mode 100644
index 0000000..83ce75b
Binary files /dev/null and b/docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png differ
diff --git a/docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png b/docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png
new file mode 100644
index 0000000..c83a5bc
Binary files /dev/null and b/docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png differ
diff --git a/docs/hardware/index.md b/docs/hardware/index.md
index 13ec0e7..7e7d56d 100644
--- a/docs/hardware/index.md
+++ b/docs/hardware/index.md
@@ -6,19 +6,19 @@ title: "Tarjetas y hardware"
Estado de cada versión de la EinarBoard al **21-oct-2026**, con sus documentos vigentes, los históricos y el siguiente paso. ¿No sabes qué tarjeta tienes? [Manuales por tarjeta](../manuales/index.md) te la identifica por serigrafía, conectores y LEDs. Estado vivo (tareas, decisiones, responsables): [Tablero de ingeniería](../tableros/ingenieria.md).
-!!! warning "21-oct: pasada 4 auditada y, en la tarde, T32 + T33 — falta la pasada 5 de Michelle"
- - **V5.6 — todavía no se pide.** C129 quedó colocado y unido a U6, salió la rama de Dbk2, y U7 girado acortó el nodo SW de 13 a 10 mm. El corto `/MODEM_EN`–GND que dejó la pasada 4 en R106 y la separación de 0.09 mm bajo U7 **ya los quitó T33**, junto con las vías sencillas de VSYS y de `/3V3_MODEM`. **C130** (100 nF en la entrada de U7, D-21-1) ya está en el esquemático (T32). Falta (≈ 1 h): F8, C130 junto a U7.2, el GND de C129 y la entrada de RS1 ([pasada 5](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5)).
- - **V5.7 — ya sin el bloqueante de K1.** `/SYS5V` salió de In2 y L22 quedó junto a D34; T33 reforzó 4 cambios de capa del 5 V y quitó las vías sueltas. Falta acercar C149/C150 a U25 (lazo de 52 mm²), C130 y la vía de (168.0, 110.6): ≈ medio día ([§3.2](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57)).
- - **v6 — sin cambio** desde el 19-oct. Plan de fines de semana: 24–25 oct y 31 oct–1 nov.
+!!! warning "21-oct, noche: pasada 5 de V5.6/V5.7 revisada y T25 + T28 aplicados en la v6"
+ - **V5.6 — todavía no se pide.** La pasada 5 (`6458c5d`) colocó C130 junto a U7.2 y la vía del GND de C129, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: **el sensado ya no es Kelvin**. Falta (≈ 30 min): esa vía y una 2.ª de RS1 bien unida, la 2.ª vía de U7.2 y las exclusiones del DRC ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)).
+ - **V5.7 — ya sin el bloqueante de K1.** Lo mismo en RS1 y en U7.2, más C149/C150 sin girar (lazo de ≈ 56 mm²) y el GND de C129 ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)).
+ - **v6 — T25 y T28 aplicados** (falta el push). Michelle adelantó la fuente U25, pero D34 quedó lejos del pin SW (lazo de ≈ 115 mm²) y MH8 cae encima de D34: [**▶ estado de la v6 al 21-oct**](v6-estado-21oct.md).
Detalle y lista para Michelle: [**▶ Auditoría del 21-oct**](v56-v57-auditoria-21oct.md). Antes: [re-auditoría del 19-oct](re-auditoria-19oct.md) · [qué es crítico en la V5.6](v56-clasificacion-19oct.md) · [plan v6 (rev. 1.1)](v6-plan-fines-de-semana.md).
| Tarjeta | Estado | Pedido | Proyecto KiCad | Firmware |
|---|---|---|---|---|
| V5.5 | En campo (15 pzas, 22-ago). No se fabrica más. | — | `hbox-watchdog-v5` (tag `v5.5-as-fabbed`) | `hbox-v5-bringup_0.6.0` CAMPO · 0.6.3 banco |
-| V5.6 | Pasada 4 auditada el 21-oct (`85e7602`) y T32 + T33 en la tarde: **no lista**. El corto de R106 ya salió; faltan C130 junto a U7.2, el GND de C129 y la entrada de RS1: ≈ 1 h ([pasada 5](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5)). C129 ya está, con T31 (`3daff19`). | 113 (piloto de 5 primero) | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), no existe |
-| V5.7 | Pasada 4 auditada el 21-oct (`1416dcb`) y T32 + T33 en la tarde: **NO LISTA**, pero ya sin el bloqueante de K1. Faltan C149/C150 junto a U25, C130 y una vía del 5 V: ≈ medio día ([§3.2](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57)). Entrada 9–32 V. | 327 (5 prototipos primero) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) |
-| v6 | Re-auditada el 19-oct: **NO LISTA**, sin cambio (`00a9af4`: solo 15 vías de tierra); siguen los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md) (rev. 1.1). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe |
+| V5.6 | Pasada 5 revisada el 21-oct en la noche (`6458c5d`): **no lista**. C129 y C130 ya están; la vía de entrada de RS1 quedó encima del hilo de sensado (ya no es Kelvin). Falta ≈ 30 min ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). | 113 (piloto de 5 primero) | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), no existe |
+| V5.7 | Pasada 5 revisada el 21-oct en la noche (`6458c5d`): **NO LISTA**, pero ya sin el bloqueante de K1. Igual que la V5.6 en RS1, más C149/C150 sin girar (lazo de ≈ 56 mm²) y el GND de C129 ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). Entrada 9–32 V. | 327 (5 prototipos primero) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) |
+| v6 | Revisada el 21-oct (`9255b84` + T25/T28): **NO LISTA**. T28 cerró 2 de los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md) y T25 regresó los barrenos; Michelle adelantó la fuente U25, pero D34 quedó lejos del pin SW y MH8 cae encima ([estado del 21-oct](v6-estado-21oct.md)). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe |
| V4 | En prueba y campo. Sin producción nueva. | — | `hbox-watchdog-v4` | `hbox-tarjetaV4_0.6.0` CAMPO |
| V3 | 3 unidades en campo. Registro congelado. | — | `hbox-watchdog-v3-PROD` (no editar) | `hbox-tarjetaV3_4.0.0` |
@@ -52,7 +52,8 @@ Estado de cada versión de la EinarBoard al **21-oct-2026**, con sus documentos
- **Ya lo hizo T30** (`7650b96`, 19-oct): vías en paralelo en los cambios de capa de potencia (18 vías), las 4 vías «micro» a vías normales, JP/TP/MOD1 fuera del BOM y del CPL, cajetín Rev 5.6 y stackup JLC04161H-7628.
- **Pasada 4 entregada el 21-oct (`85e7602`):** C129 colocado (T31 + F8), rama de Dbk2 fuera, U7 girado (L7.1 a 4.0 mm de U7.8), vías de `/MAIN5V` hacia P11 de vuelta.
- **T32 + T33 (21-oct, tarde):** C130 en el esquemático; fuera el corto de R106 y la separación de 0.09 mm; 2.ª vía de VSYS; 3 vías en cada cambio de capa de `/3V3_MODEM` y de `/SYS5V` hacia U12; vías sueltas fuera. DRC: solo los 3 errores de MOD1, 0 sin conectar.
-- **Falta (pasada 5, ≈ 1 h):** F8 y C130 junto a U7.2 con la 2.ª vía de la entrada de U7, vías al GND de C129 y la 2.ª vía de RS1. Más las exclusiones del DRC: [lista](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5).
+- **Pasada 5 (`6458c5d`, 21-oct, noche):** C130 junto a U7.2 con su vía a GND y la vía al GND de C129. **Nuevo:** la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6 (ya no es Kelvin) y la «2.ª vía» de RS1 no está unida en B.Cu.
+- **Falta (pasada 6, ≈ 30 min):** esa vía fuera del sensado y una 2.ª vía de RS1 unida en F.Cu y B.Cu, la 2.ª vía de la entrada de U7, las exclusiones del DRC y un tramo suelto de `/RS485_B`: [lista](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta).
L7 y lo heredado de la V5.5 se pueden quedar para el piloto, y se miden en el banco: [**▶ qué es crítico y qué se puede quedar**](v56-clasificacion-19oct.md).
@@ -78,7 +79,8 @@ Lleva Mean Well SD-15B-05 externa (igual que V5.5).
- **Lo hace T30:** lo mismo que en la V5.6, más 11 vías de tierra en la entrada y la salida de U25, y borra 12 cobres sueltos.
- **Pasada 4, auditada el 21-oct (`1416dcb`):** hechos `/SYS5V` fuera de In2, L22 junto a D34, U7 girado (L7.1 a 0.4 mm), C129 y la serigrafía de Cbst2.
- **T32 + T33 (21-oct, tarde):** C130 en el esquemático, 2.ª vía de VSYS y otros 3 cambios de capa del 5 V reforzados, vías sueltas fuera. DRC: solo los 3 errores de MOD1, 0 sin conectar.
-- **Falta (≈ medio día):** C149/C150 junto a U25, C130 junto a U7.2, la vía de (168.0, 110.6), el GND de C129 y la entrada de RS1: [lista](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57).
+- **Pasada 5 (`6458c5d`, 21-oct, noche):** C130 junto a U7.2 con su vía a GND, 2 vías en (168.0, 110.6) y C149/C150 a 1.0 mm de U25.2. **Nuevo:** igual que en la V5.6, la vía de entrada de RS1 quedó encima del hilo de sensado.
+- **Falta (pasada 6):** RS1 como en la V5.6, C149/C150 girados (lazo de ≈ 56 mm², meta < 30), el GND de C129, la 2.ª vía de U7.2, las exclusiones y un tramo suelto de `/24V`: [lista](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta).
| | Documentos |
|---|---|
@@ -104,11 +106,15 @@ Lleva Mean Well SD-15B-05 externa (igual que V5.5).
- 96 vías de tierra (meta ≥ 400; eran 81 el 16-oct).
- Stackup genérico → T28.
-El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a probar sobre `00a9af4` el 19-oct: aplican igual (con los 14 errores esperados de MH4, que se liberan a mano). El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**.
+El sensado de RS1 ya está bien (hay que conservarlo).
+
+**21-oct (`9255b84`, Michelle):** adelantó el bloque A (L22 a 2.2 mm del pin SW de U25, 6 vías en su EP) y llevó `/VEH5V` de L22 a RS1 todo por F.Cu. Pero D34 quedó a 13 mm del pin SW: el lazo caliente pasó de ≈ 47 a ≈ 115 mm². **T25 y T28 ya están aplicados** (falta el push): cierran los bloqueantes 1 y 8 y regresan los barrenos; el DRC da 22 errores en MH4 y MH8, que Michelle libera. Detalle: [**▶ estado de la v6 al 21-oct**](v6-estado-21oct.md).
+
+El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**.
| | Documentos |
|---|---|
-| Vigentes | [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.1: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) |
+| Vigentes | [**▶ Estado al 21-oct (T25 + T28)**](v6-estado-21oct.md) · [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.1: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) |
| Detalle (28-sep) | [Auditoría completa](v6-auditoria-completa.md) ([inglés](v6-full-audit.md)) · [Pasada 1 — cambios al esquemático](v6-cambios-esquematico.md) · [Pasada 2 — puertos (inglés)](v6-puertos-sin-nueva-version.md) · [Pasada 3 — revisión final (inglés)](v6-revision-final.md) |
| Históricos | [Próximos pasos para ordenar (29-sep)](v6-proximos-pasos.md) — superado por el plan de fines de semana · [Auditoría V5.6 · V5.7 · v6 del 16-oct (mañana), §8](auditoria-produccion-16oct.md#8-v6-que-falta) · [Revisión contra lecciones V5.5](v6-revision-lecciones-v55.md) — superada por la auditoría completa · [Auditoría pre-fab 25-sep](v6-auditoria-prefab.md) ([inglés](v6-prefab-audit.md)) — superada por la del 28-sep |
| Cantidad | La auditoría de septiembre hablaba de **5 prototipos**; el plan del 13-oct son **130 unidades y todos los reemplazos futuros**. Recomendado: piloto de 20 que pase el banco con el firmware 0.7.x antes de las 130. |
@@ -116,8 +122,8 @@ El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a
**Siguiente paso** ([plan de fines de semana, rev. 1.1](v6-plan-fines-de-semana.md); recorrido una semana el 19-oct):
- **Lun 19 o mar 20:** pre-órdenes en JLC (THVD1410DR, TPS7A2033PDBVR, J14). No se recorren.
-- **Sáb 24, 8:00:** Leo corre T25 `--set v6` y T28.
-- **Sáb 24:** Michelle trabaja la potencia y libera MH4/MH6.
+- **21-oct:** T25 y T28 ya aplicados; falta el push de Leo.
+- **Sáb 24:** Michelle regresa D34 junto a U25, libera MH4, MH6 y MH8, y sigue con la potencia ([estado del 21-oct, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir)).
- **Dom 25:** RF del módem, U20, GNSS y U12.
- **Sáb 31 y dom 1-nov:** pares, puertos, In1, vías de tierra (T29), serigrafía y DRC final. P7/P8 se quitan del esquemático (decisión del 9-oct).
- **Lun 2-nov:** pedido del piloto de 20.
diff --git a/docs/hardware/v56-v57-auditoria-21oct.md b/docs/hardware/v56-v57-auditoria-21oct.md
index 62957a8..f58f7d5 100644
--- a/docs/hardware/v56-v57-auditoria-21oct.md
+++ b/docs/hardware/v56-v57-auditoria-21oct.md
@@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v5.6, v5.7, layout, auditor
project: Project Drogheda
author: Helgen Technologies (revisión: Claude; pedido: Leo)
date: 2026-10-21
-version: 1.1
-status: ACTIVO — audita HBox_PCB 85e7602 (V5.6) y 1416dcb (V5.7) · 21-oct tarde: T32 (C130) y T33 (arreglos por script) aplicados · lo que queda para Michelle en la sección 3
+version: 1.2
+status: ACTIVO — audita HBox_PCB 85e7602 (V5.6) y 1416dcb (V5.7) · 21-oct tarde: T32 (C130) y T33 (arreglos por script) aplicados · 21-oct noche: pasada 5 (6458c5d) revisada, lo que falta en la sección 5
audience: Leo, Michelle (layout)
---
@@ -17,6 +17,8 @@ audience: Leo, Michelle (layout)
**Actualización (21-oct, tarde):** el corto, la separación, la 2.ª vía de VSYS y casi todas las vías de potencia ya quedaron con **T33**, y C130 ya está en los dos esquemáticos con **T32**. A Michelle le queda lo que pide mover pistas: C130 junto a U7.2, el GND de C129, la entrada de RS1 y, en la V5.7, C149/C150 ([sección 3](#3-para-michelle-pasada-5-despues-de-t32-y-t33)).
+**Actualización (21-oct, noche):** Michelle ya hizo la pasada 5 (`6458c5d`). C130 quedó bien en las dos, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: **el sensado ya no es Kelvin**. Lo que falta está en la [sección 5](#5-pasada-5-revisada-6458c5d-y-lo-que-falta).
+
| Tarjeta | Commit | DRC (errores): commit → con T33 | Sin conectar | Paridad | Veredicto (con T32 y T33) |
|---|---|---|---|---|---|
| **V5.6** (`hbox-watchdog-v5`) | `85e7602` | **5 nuevos** (un corto y una separación) + los 3 de MOD1 → **solo los 3 de MOD1** | 0 | solo PF* (+ C130 hasta el F8) | **No lista:** el corto y la separación ya salieron. Faltan C130 junto a U7.2, el GND de C129 y la entrada de RS1 (≈ 1 h). Luego Gerber. |
@@ -140,6 +142,48 @@ Avísale a Leo con los dos hashes: Claude revisa el cobre antes de pedir Gerber.
- Si no cabe junto a U7.2, el punto 3 queda en 🟡 y se mide en el piloto: zumbido en U7.2 y caída del riel del módem en J7.1.
- **Giro de U7: aceptarlo.** Mejora el nodo SW de 13 a 6–10 mm; lo que empeora, se arregla con los puntos 3 y D-21-1.
+## 5. Pasada 5 revisada (`6458c5d`) y lo que falta
+
+
+
+**DRC de las dos:** solo los 3 errores de MOD1 (todavía sin excluir), 0 sin conectar y la paridad solo con los PF*: C130 ya entró con F8. Nada protegido se movió.
+
+| Punto de la sección 3 | V5.6 | V5.7 |
+|---|---|---|
+| F8 y C130 junto a U7.2, con su vía a GND | ✅ a 1.8 mm del pin, unido por F.Cu, con su vía | ✅ igual |
+| 2.ª vía de la entrada de U7 | ❌ sigue una sola, en (139.4, 117.2) | ❌ sigue una sola, en (139.57, 116.57) |
+| Vía al GND de C129 | ✅ 1 vía a ≤ 0.5 mm | ❌ ninguna |
+| RS1: 2.ª vía de entrada | ❌ la vía nueva de (149.48, 118.8) solo toca F.Cu; el DRC la marca como colgando | ❌ igual |
+| RS1: vía de entrada fuera del pad | 0.51 mm del pad, **pero encima del hilo de sensado de U6.6** 🔴 | 0.12 mm (meta ≥ 0.2), **y encima del hilo de sensado** 🔴 |
+| C149/C150 junto a U25, lazo < 30 mm² | — | 🟠 a 1.0 mm de U25.2, pero sin girar: lazo ≈ 56 mm² (antes ≈ 52) |
+| 2.ª vía en `/VEH5V` (168.0, 110.6) | — | ✅ 2 vías |
+| Exclusiones de MH7, MH9 y MH10 | ❌ | ❌ |
+| Opcional: compensación de U7 más cerca | sin cambio | sin cambio |
+
+!!! danger "El sensado de RS1 ya no es Kelvin en ninguna de las dos"
+ La vía de entrada de RS1 sí se movió fuera del pad, pero quedó **encima del hilo de 0.25 mm que va de U6.6 (VIN del LTC4040) al pad 1 de RS1**.
+
+ - **Qué cambia:** U6.6 ya no mide el voltaje en el pad sino en la vía. A los 10 mΩ de RS1 se suman unas décimas de mΩ de cobre (≈ 0.3–0.4 mΩ), así que el límite de entrada baja ≈ 3–4 %: ≈ 2.4 A en vez de 2.5 A. Además, parte de la corriente de entrada pasa por el hilo de sensado.
+ - **Qué tan grave es:** no es el defecto de la V5.5 (la carga limitada a ≈ 1 A), pero es justo lo que T27 corrigió, y depende de dónde caiga la vía en cada tarjeta.
+ - **Por qué el DRC no lo marca:** la vía y el hilo son de la misma red.
+
+**Para Michelle — pasada 6** (≈ 30 min por tarjeta):
+
+1. **RS1, en las dos:**
+ - recorre la vía de entrada de `/VEH5V` para que quede a **≥ 0.2 mm del hilo de sensado** de U6.6 y a ≥ 0.2 mm del pad de RS1.1, por ejemplo del lado izquierdo del pad;
+ - pon una **2.ª vía** de entrada unida en F.Cu **y** en B.Cu al `/VEH5V` que llega;
+ - la vía de (149.48, 118.8) únela al `/VEH5V` de B.Cu o bórrala;
+ - el hilo de sensado no se mueve: sigue entrando al pad de RS1.1 por su lado, sin tocar nada más.
+2. **2.ª vía de la entrada de U7, en las dos:** junto a (139.4, 117.2) en la V5.6 y a (139.57, 116.57) en la V5.7.
+3. **V5.7, GND de C129:** 1–2 vías a ≤ 0.5 mm del pad (152.2, 112.58).
+4. **V5.7, C149/C150:** gíralos para que su tierra mire a D34 y al EP de U25 (sección 3.2, paso 2). Meta: lazo < 30 mm².
+5. **Exclusiones** de MH7, MH9 y MH10 en el DRC, en las dos.
+6. **Tramos sueltos:**
+ - V5.6: el tramo de 0.01 mm de `/RS485_B` en (136.59, 108.89), que regresó;
+ - V5.7: el tramo de 3.6 mm de `/24V` en B.Cu, junto a (182.4, 68.11).
+
+Cierre igual que en la sección 3. Antes de pedir Gerber, Claude revisa el cobre, sobre todo el sensado de RS1.
+
## Cómo se hizo
- **Archivos:** HBox_PCB `1416dcb` (V5.6 de `85e7602`, V5.7 de `1416dcb`) contra `3daff19`, cuyos PCB son los de `e4aa6e9`. La diferencia se sacó por UUID.
@@ -160,3 +204,4 @@ Avísale a Leo con los dos hashes: Claude revisa el cobre antes de pedir Gerber.
- vías de corte por red, otra vez: en las redes de potencia solo quedan las que le tocan a Michelle, las que se aceptaron el 19-oct (rama de U12, medición de `/BAT_PLUS` y, en la V5.7, las dos de `/24V` hacia U25) y ramas sin corriente (divisores, pull-ups, pines de medición).
Después se aplicaron a HBox_PCB: los scripts revisan que el archivo sea el auditado y, si cambió, no escriben. Cada uno deja su respaldo (`.T32bak`, `.T33bak`).
+- **Pasada 5 (noche):** `6458c5d` contra el resultado de T33 (`71499c1`), con los mismos scripts y el mismo DRC. El sensado de RS1 se probó recortando el área del pad del resto del cobre: si U6.6 llega a la fuente sin pasar por el pad, no es Kelvin.
diff --git a/docs/hardware/v6-auditoria-produccion-16oct.md b/docs/hardware/v6-auditoria-produccion-16oct.md
index 917d24b..b7c60da 100644
--- a/docs/hardware/v6-auditoria-produccion-16oct.md
+++ b/docs/hardware/v6-auditoria-produccion-16oct.md
@@ -18,6 +18,9 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
!!! note "19-oct: sin cambios en los bloqueantes"
El commit del sábado 17 (`00a9af4`) solo agregó 15 vías de tierra (81 → 96) y mejoró la cobertura de In1 bajo la RF LTE. Todo lo de esta auditoría sigue vigente; T25 y T28 se volvieron a probar sobre `00a9af4` y aplican igual. El plan de fines de semana se recorrió una semana: [**rev. 1.1**](v6-plan-fines-de-semana.md) (24–25 oct y 31 oct–1 nov). Detalle: [re-auditoría del 19-oct, §8](re-auditoria-19oct.md#8-v6).
+!!! note "21-oct: T25 y T28 aplicados; avance del bloque A"
+ T25 y T28 ya se aplicaron sobre `9255b84` (falta el push de Leo): quedan cerrados los bloqueantes 1 y 8, y los barrenos ya están en la tarjeta. Michelle adelantó el bloque A, pero D34 quedó lejos del pin SW de U25 y MH8 cae encima de D34. Detalle: [estado de la v6 al 21-oct](v6-estado-21oct.md).
+
**El plan para cerrarla** está en un documento aparte: [**▶ Plan de fines de semana — v6 lista para producción**](v6-plan-fines-de-semana.md).
!!! danger "Veredicto: la v6 todavía no se puede mandar a fabricar"
diff --git a/docs/hardware/v6-estado-21oct.md b/docs/hardware/v6-estado-21oct.md
new file mode 100644
index 0000000..cfd8228
--- /dev/null
+++ b/docs/hardware/v6-estado-21oct.md
@@ -0,0 +1,102 @@
+---
+title: "Estado de la v6 al 21-oct y scripts T25/T28 aplicados"
+tags: [project-drogheda, hardware, hbox, einarboard, v6, auditoria, t25, t28, barrenos, u25, layout, michelle]
+project: Project Drogheda
+author: Helgen Technologies (revisión: Claude; pedido: Leo)
+date: 2026-10-21
+version: 1.0
+status: ACTIVO — HBox_PCB 9255b84 (Michelle, 21-oct 18:14) con T25 y T28 aplicados por Claude · falta commit y push de Leo
+audience: Leo, Michelle (layout)
+---
+
+# Estado de la v6 al 21-oct · T25 y T28 aplicados
+
+**Pregunta (Leo, 21-oct):** ¿T28 ya quedó o hay que correrlo? ¿Cómo va la v6?
+
+**Respuesta corta:**
+
+- **T28 nunca se había corrido, y T25 tampoco.** Se probaron el 16 y el 19-oct para correrlos el sábado 24 a las 8:00 ([plan, §2](v6-plan-fines-de-semana.md#2-sabado-24-800-leo-scripts-t25-y-t28-15-min)). Hoy los probé en una copia del commit de Michelle (`9255b84`) y los apliqué a los archivos de HBox_PCB. **Falta que Leo haga commit y push.**
+- **La v6 todavía no se puede pedir.** Michelle adelantó el bloque A (fuente U25) y parte del B (`/VEH5V`). Hay avance real, pero **D34 quedó lejos del pin SW de U25**: el lazo caliente creció de ≈ 47 a ≈ 115 mm². Además, **MH8 cae encima de D34** y de 2 vías de GND. No se veía porque los barrenos no estaban en la tarjeta.
+- **DRC de hoy, con T25 y T28:** 22 errores, todos en MH4 y MH8; 0 sin conectar; la paridad solo marca los 19 PWR_FLAG.
+
+
+
+## 1. T25 y T28: qué cambiaron hoy
+
+| Script | Qué cambió en `hbox-watchdog-v6` |
+|---|---|
+| **T25 `--set v6`** | **5 barrenos** de 3.2 mm con máscara de 6 mm: MH5 (65.2, 45.3), MH6 (65.2, 135.3) y MH4 (185.2, 90.3), los de la placa base de la V5.5, más MH7 (195.2, 45.3) y MH8 (195.2, 135.3) en las esquinas. Van como piezas «solo de tarjeta», así que *Actualizar PCB* ya no los borra. |
+| **T28** | U25 sin el código del AP64350: queda solo LCSC C524806 (TPS54360B) · pasta del EP de U6 al 50 % (6 ventanas de 0.95 × 0.85 mm) · conexión sólida a la zona en 114 pads de tierra de potencia y RF · stackup JLC04161H-7628 con ENIG y control de impedancia · cajetín «EinarBoard v6 (HBOX 6.0)», rev 6.0, y textos de serigrafía · clase RF a 0.35 mm en el `.kicad_pro`. |
+
+**Cómo se probó.**
+
+- Dos copias del commit `9255b84` en la Mac: una tal cual y otra con T25 + T28.
+- DRC de KiCad 10.0.4 con el `.kicad_dru`, «Rellenar zonas» y «Comprobar paridad»:
+ - sin los scripts: 0 errores;
+ - con los scripts: 22 errores, todos en MH4 y MH8; 0 sin conectar; la paridad igual (19 PWR_FLAG).
+- Luego se aplicaron a los archivos de HBox_PCB, que eran idénticos a la copia, y el DRC dio lo mismo.
+- Correr T28 otra vez no cambia nada, y T25 se niega si los barrenos ya están.
+
+!!! warning "Antes de que Michelle vuelva a abrir la v6"
+ T25 y T28 cambian `hbox-watchdog-v6.kicad_pcb` y `.kicad_pro`. Git ya no mezcla archivos de KiCad (`merge=binary`): si Michelle guarda la v6 sobre su copia anterior y la sube, se pierde uno de los dos trabajos. El orden es: **Leo sube → Michelle hace `git pull --ff-only` → abre la v6.**
+
+## 2. Los 8 bloqueantes del 16-oct, hoy
+
+Medido en el cobre con las zonas rellenadas. «19-oct» = `00a9af4`; «hoy» = `9255b84` con T25 y T28.
+
+| # | Bloqueante del 16-oct | 19-oct | Hoy | Estado |
+|---|---|---|---|---|
+| 1 | U25 con el código del AP64350 | igual | solo C524806 | ✅ T28 |
+| 2 | Sin barrenos | sin barrenos | 5 barrenos; **MH4 y MH8 con piezas y cobre dentro** (22 errores) | 🟠 liberar MH4 y MH8 |
+| 3 | 5 V por capas internas | `/VEH5V` 12 mm por In1 a 0.5 mm; `/VIN_SW` 22 mm por In2 | `/VEH5V` de L22 a RS1 **todo en F.Cu de 1.0 mm, sin vías**: 38 mV y +11 °C a 2.5 A (meta ≥ 2 mm). `/VIN_SW` de RS1 a Q7 **igual**: 22 mm por In2 a 0.5–0.8 mm, con 2 vías; Q7 sigue a 10.6 mm de RS1 | 🟠 mitad |
+| 4 | Cargador U6 | `/SW_BAT` y `/BAT_PLUS` de 0.5 mm; 1 vía en el EP | `/SW_BAT` de 0.7–0.75 mm (meta: polígono ≥ 2 mm); `/BAT_PLUS` igual; 1 vía en el EP (meta 9) | ❌ |
+| 5 | Fuente U25 | L22 a 13 mm; EP sin vías | L22 a 2.2 mm ✅ · 6 vías en el EP ✅ · C149 a 0.75 mm de VIN ✅ · 5 vías GND junto al ánodo de D34 ✅ · **D34 a 13 mm del pin SW** (antes 3.8) ❌ · **lazo caliente ≈ 115 mm²** (antes ≈ 47) ❌ · nodo SW de 45 mm² (meta ≤ 25) | 🔴 regresión en D34 |
+| 6 | RF LTE por In1 | 21.9 mm por In1 | igual | ❌ |
+| 7 | Vías de GND | 96 (meta ≥ 400) | 111: 1 bajo U19 y 0 bajo U18; In1 con 1 656 mm de pistas | ❌ |
+| 8 | Stackup genérico | igual | JLC04161H-7628, ENIG, control de impedancia | ✅ T28 |
+
+**Lo demás** (bloques C a H: módem, GNSS, U12, pares diferenciales, puertos, In1, serigrafía) no cambió desde el 19-oct.
+
+**El sensado de RS1 sigue como el 16-oct:** VIN es Kelvin y CLN es «casi Kelvin», como se aceptó. Hay que conservarlo al rehacer `/VIN_SW`.
+
+**Otros cambios de hoy:**
+
+- `/24V` (la rama de Q10) pasó de In1 a B.Cu; el plan pedía borrarlo.
+- Rig1 quedó a 4.8 mm de P1.3 (meta ≤ 3 mm; antes 8.4).
+- P1 se recorrió 1.46 mm al oeste.
+
+## 3. Lo que tiene que corregir Michelle antes de seguir
+
+Después del `git pull`. Primero esto y luego el resto de los bloques A y B del [plan](v6-plan-fines-de-semana.md#3-sabado-24-bloques-a-y-b-el-camino-de-la-energia).
+
+1. **D34 de regreso junto a U25.8** (bloque A, paso 1 del plan):
+ - el cátodo de D34 a **2–3 mm del pin 8** de U25, y el ánodo hacia la tierra de C149/C150 y del EP;
+ - L22 se queda donde está (L22.1 a 2.2 mm del pin 8 ✅);
+ - el cobre de `/SW_PWRIN` (pin 8 → D34.1 → L22.1) en **≤ 25 mm²**, sin pasar por debajo del cuerpo de L22;
+ - el lazo caliente, menor que los 47 mm² del 19-oct; mejor si queda < 30;
+ - **para hacer lugar:** R53 y R54 (el divisor BSTFB del LTC4040) quedaron junto a U25, en (177.0, 125.7) y (175.4, 126.9). Van a **≤ 3 mm de U6.18** (bloque B, paso 6 del plan). Al moverlos se libera el lugar de D34, abajo del pin 8.
+2. **MH8 (195.2, 135.3):** nada de cobre ni de piezas dentro del círculo de 4 mm. Hoy están dentro D34 y 2 vías de GND, en (194.6, 133.7) y (196.2, 133.7). Con el paso 1, D34 sale de ahí; las 2 vías se recorren.
+3. **MH4 (185.2, 90.3):** igual que el 16-oct ([plan, §3.4](v6-plan-fines-de-semana.md#34-barrenos-mh4-y-mh6-1-h)):
+ - D23, D24 y D25 junto a J14 y sus resistencias de 22 Ω;
+ - `/NTC` (In2) a ≥ 2.5 mm del centro;
+ - `/SIM_RST`, `/SIM_CLK` y la pista GND fuera del círculo de 4 mm.
+4. **MH6 (65.2, 135.3):** la pista `/3V3` a ≥ 4 mm del centro (hoy 3.0 mm).
+5. **No mover los barrenos:** MH4–MH6 son los de la placa base de la V5.5, y MH7 y MH8 son las esquinas de la v6.
+
+**Cierre:** **B** y DRC con «Rellenar zonas» y «Comprobar paridad»: **0 errores** y 0 sin conectar. Commit y push con «v6 bloque A» en el mensaje.
+
+## 4. Calendario
+
+El paso del sábado 24 a las 8:00 (Leo corre T25 y T28) ya está hecho. Lo demás del [plan de fines de semana](v6-plan-fines-de-semana.md) sigue igual:
+
+- sábado 24: bloques A y B, empezando por la sección 3 de aquí;
+- domingo 25: bloques C, D y E;
+- entre semana: T29;
+- lunes 2-nov: pedido del piloto.
+
+## Cómo se hizo
+
+- **Archivos:** HBox_PCB `9255b84` (Michelle, 21-oct 18:14) contra `00a9af4` (19-oct). El esquemático de la v6 no cambió desde el 28-sep (`d58a89ba…`).
+- **Scripts:** `kicad/tools/t25_mounting_holes_v55.py --set v6` y `kicad/tools/t28_v6_mecanicos.py`, primero en una copia y luego en los archivos de HBox_PCB. Los respaldos `.T25bak` y `.T28bak` no se suben.
+- **DRC:** KiCad 10.0.4, `pcb drc --schematic-parity --refill-zones --severity-all`, con el `.kicad_dru` del proyecto.
+- **Medidas:** el script de la auditoría del 16-oct sobre el cobre rellenado (distancias, series de cobre, vías por red y por EP, lazo por centros de pad), y la prueba Kelvin de RS1 con el área del pad recortada.
diff --git a/docs/hardware/v6-plan-fines-de-semana.md b/docs/hardware/v6-plan-fines-de-semana.md
index ca1d028..ee9b460 100644
--- a/docs/hardware/v6-plan-fines-de-semana.md
+++ b/docs/hardware/v6-plan-fines-de-semana.md
@@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v6, plan, layout, firmware,
project: Project Drogheda
author: Helgen Technologies (plan: Claude; revisión: Leo)
date: 2026-10-16
-version: 1.1
-status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00
+version: 1.2
+status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00 · 21-oct: T25 y T28 ya aplicados (falta el push)
audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana (PM)
---
@@ -16,6 +16,9 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
!!! note "Rev. 1.1 (19-oct): el plan se recorre una semana"
El fin de semana del 17–18 se fue en la pasada 3 de V5.6/V5.7; a la v6 solo entraron 15 vías de tierra (`00a9af4`), así que los 8 bloqueantes de la [auditoría del 16-oct](v6-auditoria-produccion-16oct.md) siguen igual ([re-auditoría del 19-oct, §8](re-auditoria-19oct.md#8-v6)). Mismo trabajo, una semana después: **24–25 oct** y **31 oct–1 nov**; pedido del piloto el **lun 2-nov**; G2 ≈ **22-nov**. T25 y T28 se volvieron a probar sobre `00a9af4` y aplican igual. Lo único que no se recorre son las **pre-órdenes en JLC**: se piden ya.
+!!! warning "21-oct: T25 y T28 ya están aplicados y Michelle adelantó el bloque A"
+ El paso del §2 (sábado 24, 8:00) se hizo el 21-oct sobre `9255b84`; falta el commit y push de Leo. Michelle ya movió la fuente U25 (bloque A), pero D34 quedó lejos del pin SW y MH8 cae encima de D34. Lo primero del sábado: [estado de la v6 al 21-oct, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir).
+
**Dos metas:**
| Meta | Qué significa | Fecha objetivo |
@@ -39,7 +42,7 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
| Día | Quién | Qué | Listo cuando |
|---|---|---|---|
-| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2) | push hecho; Michelle avisada |
+| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2). **Ya se corrieron el 21-oct: falta el push** | push hecho; Michelle avisada |
| **Sáb 24-oct** | Michelle | **Bloques A + B:** el camino de la energía (U25, RS1, Q7, U6, L6) + liberar MH4 y MH6 (§3) | criterios del §3.5 |
| **Dom 25-oct** | Michelle | **Bloque C:** módem (RF LTE, U20, VBAT). **Bloque D:** GNSS. **Bloque E:** U12 (§4) | criterios del §4.4 |
| **Dom 25-oct, noche** | Claude | Re-auditoría del commit del domingo; lista corta para el lunes | lista enviada |
@@ -59,6 +62,8 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
## 2. Sábado 24, 8:00 — Leo: scripts T25 y T28 (15 min)
+**21-oct:** ya se corrieron sobre `9255b84` ([estado del 21-oct](v6-estado-21oct.md)). El DRC da 22 errores, en MH4 y en MH8, y no solo los 14 de MH4: D34 quedó dentro de MH8. Solo falta el commit y push de abajo.
+
Con KiCad **cerrado** y todo subido. Los dos scripts se probaron juntos en copias del commit `2612891` (16-oct) y del `00a9af4` (el del sábado 17, probado el 19-oct): el DRC solo muestra los errores esperados en MH4.
!!! warning "Antes: árbol limpio y nadie más con la v6 abierta"
diff --git a/docs/img/mexico.svg b/docs/img/mexico.svg
new file mode 100644
index 0000000..bb7d2a0
--- /dev/null
+++ b/docs/img/mexico.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/docs/img/tarjetas/portada-v56.webp b/docs/img/tarjetas/portada-v56.webp
new file mode 100644
index 0000000..fa12b44
Binary files /dev/null and b/docs/img/tarjetas/portada-v56.webp differ
diff --git a/docs/img/tarjetas/v55.webp b/docs/img/tarjetas/v55.webp
new file mode 100644
index 0000000..bf11356
Binary files /dev/null and b/docs/img/tarjetas/v55.webp differ
diff --git a/docs/img/tarjetas/v56.webp b/docs/img/tarjetas/v56.webp
new file mode 100644
index 0000000..c29bd10
Binary files /dev/null and b/docs/img/tarjetas/v56.webp differ
diff --git a/docs/img/tarjetas/v57.webp b/docs/img/tarjetas/v57.webp
new file mode 100644
index 0000000..1057c87
Binary files /dev/null and b/docs/img/tarjetas/v57.webp differ
diff --git a/docs/img/tarjetas/v6.webp b/docs/img/tarjetas/v6.webp
new file mode 100644
index 0000000..e4405ca
Binary files /dev/null and b/docs/img/tarjetas/v6.webp differ
diff --git a/docs/index.md b/docs/index.md
index 1dcce79..0889e2e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,18 +1,116 @@
---
-title: "HBOX — Documentación técnica"
+title: "Inicio"
+description: "Drogheda: rastreadores HBOX RTK para el equipo de patio de Hutchison Ports. Dónde va cada tarjeta, el enfoque de la semana, los pendientes y los atajos."
+template: inicio.html
+ancho: 64rem
hide:
- toc
---
-# HBOX — Documentación técnica
+
-Sitio interno de Helgen para producir, probar, instalar y diagnosticar el rastreador **HBOX RTK** (tarjeta EinarBoard). Todo trabajo deja un **acta**; toda unidad tiene un **expediente**. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md).
+
-
-La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento.
+
+
-## ¿Qué necesitas hacer?
+
+
+
Drogheda
+
Rastreadores HBOX RTK para el equipo de patio de Hutchison Ports. Aquí se diseña, se produce, se prueba y se instala.
+
+
+
+
+
Fase
+
…
+
Pedido
+
…
+
Próximo hito
+
…
+
Equipo
+
…
+
+
+
+
+
+
+
+
Enfoque de la semana
+
…
+
+
+
+
+
+
Ruta a producción
+ Línea continua: hecho. Punteada: lo que falta. El anillo marca el paso en el que va cada tarjeta. En cada imagen, lo nuevo de la revisión va en blanco.
+ Tablero de Ingeniería
+
Antes de proponer un cambio, revisa que no choque con ninguna de estas reglas.
+
+
Primero la seguridad en campo.La unidad vive a más de 45 °C y en batería; todo se prueba en banco antes de llevarlo a campo.
+
Una etapa probada vale más que una nueva.Los zócalos se quedan; la fuente de la V5.7 viene del bloque ya auditado de la v6.
+
Redundancia solo con causa.Por eso salió P12 y entró el acelerómetro U26.
+
Las reglas de fabricación no se negocian.Pasta de U6 al 50 % o menos, nada de potencia por capas internas, lupa en U6 antes de energizar.
+
Cambios con prueba y rastro.Scripts verificados contra la netlist, una persona por tarjeta a la vez y no habrá v6.1.
+
+
+
+
+
+
+
+
+## ¿Qué necesitas hacer? { #que-necesitas-hacer }
| Quiero… | Abre | Acta |
|---|---|---|
@@ -21,7 +119,7 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J
| **Diagnosticar** una unidad que falla en la máquina | [Diagnóstico en campo](diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](formularios/diagnostico.html) |
| **Diagnosticar** una unidad en la mesa | [Diagnóstico en banco](procedimientos/diagnostico-banco.md) | [Banco](formularios/banco.html) |
| **Instalar en sitio** (ICAVE, LZC, EIT, TIMSA) | [Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md) | [Instalación en campo](formularios/instalacion.html) |
-| **Ver los tableros** (pendientes, ingeniería, producción, actas) | [Tableros](tableros/index.md) | — |
+| **Ver los tableros** (ingeniería, producción, actas, inventario) | [Tableros](tableros/index.md) | — |
| **Inventario** (existencias, entradas y salidas) | [Inventario](tableros/inventario.md) | — |
| **Pedir una tarjeta o saber qué versión va** | [Tarjetas y hardware](hardware/index.md) | — |
| **Descargar firmware** (por tarjeta, con SHA-256) | [Descargas](descargas/index.md) | [Reflasheo](formularios/reflasheo.html) |
@@ -31,31 +129,6 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J
| Saber qué tarjeta tengo en la mano | [Manuales por tarjeta](manuales/index.md) | — |
| Entender cómo llegamos aquí | [Historia del proyecto](historia/index.md) | — |
-## Tarjetas — estado al 19-oct-2026
+La lupa de arriba busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md). Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
-| Tarjeta | Estado | Pedido | Firmware | Dónde leer |
-|---|---|---|---|---|
-| V5.5 | **En campo.** 15 pzas fabricadas el 22-ago; en ensamble, taller y puertos. No se fabrica más. Hallazgo del 16-oct: el cargador queda limitado a ≈ 1 A de entrada (medir en banco). | — | 0.6.0 CAMPO · 0.6.3 en prueba de banco | [Manual V5](manuales/manual-tarjeta-V5.md) |
-| V5.6 | **Casi lista.** T30 aplicado; falta la pasada 4, con un crítico nuevo: **C129 en VSYS de U6** (un 0603, sin mover piezas). Después Gerber/BOM/CPL y el piloto de 5. Falta también el firmware 0.7.0. Con Mean Well SD-15B externa. | **113** (piloto de 5 primero) | 0.7.0 (`board_v56.h`), no existe | [Qué es crítico](hardware/v56-clasificacion-19oct.md) · [Re-auditoría 19-oct](hardware/re-auditoria-19oct.md) · [Pasada 4](hardware/v56-v57-layout-pasada-4.md) |
-| V5.7 | **NO LISTA.** Fuente a bordo (entrada 9–32 V). Pasada 3 hecha: ya replica la V5.6. Bloqueante: `/SYS5V` por In2 bajo K1; falta compactar la fuente U25. T30 + ≈ medio día de la pasada 4; después los 5 prototipos. | **327** (5 prototipos primero) | el mismo 0.7.0 | [Re-auditoría 19-oct, §4](hardware/re-auditoria-19oct.md#4-v57-punto-por-punto) |
-| v6 | **NO LISTA**, sin cambio desde el 16-oct (solo 15 vías de tierra nuevas): potencia por capas internas, RF del módem por In1, sin barrenos. Plan recorrido una semana: fines de semana 24–25 oct y 31 oct–1 nov; piloto de 20 el lun 2-nov. | **130** + reemplazos (piloto de 20) | 0.7.x (`board_v6.h`), no existe | [Auditoría v6](hardware/v6-auditoria-produccion-16oct.md) · [Plan de fines de semana (rev. 1.1)](hardware/v6-plan-fines-de-semana.md) |
-| V4 | En prueba y campo (Blackpill + Ethernet/serie). Sin producción nueva. | — | 0.6.0 CAMPO · 0.6.3 en prueba | [Manual V4](manuales/manual-tarjeta-V4.md) |
-| V3 | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) |
-| firmware | El binario se elige **por tarjeta**, nunca por número de versión. 0.6.3 en banco desde el 1-oct, no para campo. | | | [Descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) · [notas de versión](descargas/notas-de-version.md) |
-
-Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el plan: [Tablero de producción](tableros/produccion.md).
-
-## Hoy — 19 de octubre de 2026
-
-- **V5.6 (tarde): qué es crítico y qué se puede quedar** ([clasificación](hardware/v56-clasificacion-19oct.md)). Hay un solo crítico, y es nuevo: U6 no tiene capacitor en sus pines VSYS (viene desde la V5.5). Se agrega C129 (10 µF 0603) sin mover piezas. Además hay que regresar 4 vías que se perdieron en el guardado de las 18:59. L7 y lo heredado de la V5.5 se pueden quedar para el piloto, y se miden en el banco.
-- **Re-auditoría de las tres tarjetas** sobre la pasada 3 de Michelle: [Re-auditoría del 19-oct](hardware/re-auditoria-19oct.md). **V5.6 casi lista** (T30 + ≈ 1 h); **V5.7** con un bloqueante (`/SYS5V` por In2) y la fuente U25 por compactar; **v6** sin avance en sus 8 bloqueantes.
-- **Script T30** listo y probado en copias con el DRC de KiCad: vías en paralelo, vías «micro», BOM/CPL, cajetín y stackup de V5.6 y V5.7. Lo corre Leo hoy: [pasada 4, §0](hardware/v56-v57-layout-pasada-4.md#0-antes-de-empezar-leo-10-min).
-- **[Pasada 4](hardware/v56-v57-layout-pasada-4.md)** (Michelle): parte A (V5.6, ≈ 1 h) el martes 20; parte B (V5.7, ≈ medio día) el 20–21. Después, un solo pedido: 5 V5.6 + 5 V5.7 (≈ 21–22-oct).
-- **Merge del lunes resuelto** (`240e2c3`; `.gitattributes` con `merge=binary`) y verificado objeto por objeto. Para que no se repita: el auto-sync de las 18:00 no debe subir archivos de KiCad, y una persona por tarjeta a la vez ([§2](hardware/re-auditoria-19oct.md#2-el-merge-del-lunes-que-paso-y-como-evitarlo)).
-- **v6:** [plan de fines de semana, rev. 1.1](hardware/v6-plan-fines-de-semana.md): 24–25 oct y 31 oct–1 nov, piloto de 20 el lun 2-nov, G2 ≈ 22-nov. Las **pre-órdenes en JLC** (THVD1410DR, TPS7A2033PDBVR, J14) no esperan: hoy o mañana.
-- **Decisiones de Leo (D-19-1…4):** puertos de V5.6/V5.7 (si van, entran al piloto), Dbk2, auto-sync y clase HV24: [§7](hardware/re-auditoria-19oct.md#7-decisiones-para-leo).
-- **V5.7 sin Mean Well acepta 9–32 V** (máquinas de 12 y 24 V). Recomendado: TVS bidireccional para que la polaridad invertida no abra el fusible.
-- **Jesús:** medir en una V5.5 el límite del cargador con TP1 ([§4.5 de la auditoría del 16-oct](hardware/auditoria-produccion-16oct.md#45-prueba-definitiva-con-tp1-clprog-cualquier-tarjeta-cualquier-corriente)): (TP1 ÷ 32) ÷ voltaje en RS1 ≈ 3.1 confirma el defecto.
-- **Plan de 570 unidades** (113 / 327 / 130). Pendiente: la caja plástica del HBOX, donde el cable Ethernet no cabe. Tareas y responsables en [Pendientes](tableros/pendientes.md).
-
-Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
+
Arriba, una tarjeta por versión de EinarBoard: estado, siguiente paso, responsable y sus documentos. Es texto fijo: cuando cambie una decisión, pide a Leo o a Claude que lo actualice aquí.
+
Arriba, la ruta a producción (los siete pasos de cada versión) y una tarjeta por versión de EinarBoard: estado, siguiente paso, responsables y sus documentos. Es texto fijo de docs/datos/proyecto.json, el mismo que usa la página de inicio: cuando cambie una decisión, pide a Leo o a Claude que lo actualice ahí.
En medio, los pendientes de la pista Ingeniería: son los mismos del tablero de pendientes, filtrados. Puedes moverlos desde aquí.
Abajo, las últimas decisiones con fecha y la filosofía de diseño. Antes de proponer un cambio, revisa que no contradiga una decisión ya tomada.
Para una tarjeta nueva o un cambio de pedido, primero el pendiente, luego el trabajo: así queda el rastro de quién y cuándo.
+
Ruta a producción
+
Línea continua: hecho. Punteada: lo que falta. El anillo marca el paso en el que va cada tarjeta. En cada imagen, lo nuevo de la revisión va en blanco.
+
…
+
@@ -33,6 +37,8 @@ hide:
Últimas decisiones
Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.
+
21-oct (noche) — Pasada 5 revisada (6458c5d): C129 y C130 quedaron bien en las dos, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: el sensado ya no es Kelvin (el límite de entrada baja ≈ 3–4 %). Falta la pasada 6, ≈ 30 min por tarjeta; en la V5.7, además, girar C149/C150 (lazo de ≈ 56 mm²). v6: T25 y T28 aplicados sobre 9255b84 (falta el push de Leo); T28 cerró 2 de los 8 bloqueantes, pero D34 quedó a 13 mm del pin SW de U25 (lazo caliente de ≈ 115 mm², antes ≈ 47) y MH8 cae encima de D34. Pasada 5 revisada · Estado de la v6
+
21-oct (tarde) — Auditoría de la pasada 4 de Michelle (HBox_PCB 85e7602 V5.6, 1416dcb V5.7). V5.6: la pasada dejó un corto nuevo (/MODEM_EN a GND en R106) y una pista de /SYS5V a 0.09 mm de una vía; el giro de U7 mejoró el nodo SW (L7 a 4 mm de U7). V5.7: ya sin el bloqueante de K1. D-21-1, aprobada por Leo: C130 de 100 nF en la entrada de U7, en las dos. En la tarde quedaron T32 (C130 en los esquemáticos) y T33 (el corto, la separación, la 2.ª vía de VSYS y las vías de potencia): DRC solo con los 3 errores de MOD1. Falta la pasada 5 de Michelle (V5.6 ≈ 1 h, V5.7 ≈ medio día); después, revisión del cobre y Gerber. Auditoría del 21-oct
19-oct (19:30) — V5.6: qué es crítico y qué se puede quedar (archivo de trabajo de las 18:59, sobre T30). Un solo crítico, nuevo: U6 (LTC4040) no tiene capacitor en sus pines VSYS; el más cercano, C34, está a 11.5 mm (viene desde la V5.5). La hoja de datos lo pone por encima de todo en el layout. Se agrega C129 (10 µF 0603, C19702) junto a los pines 1/24, sin mover piezas. Regresar 2 vías de /MAIN5V y 2 de /3V3_MODEM que se perdieron al guardar. L7 lejos de U7 y lo heredado de la V5.5 (L6, L8, Dbk1) se quedan para el piloto, y se miden en el banco. Clasificación
19-oct (17:10) — Re-auditoría V5.6 / V5.7 / v6 sobre la pasada 3 de Michelle (HBox_PCB 4f4681f). V5.6 casi lista: /BAT_PLUS fuera de capas internas, /3V3_MODEM a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto; faltan T30 + ≈ 1 h (L7/Dbk2, salida VSYS de U6, 3 vías, exclusiones del DRC). V5.7 NO LISTA: ya replica la V5.6, pero /SYS5V va 10.9 mm por In2 a 0.5 mm bajo K1 (bloqueante) y el lazo de entrada de U25 mide 52 mm². v6 sin avance en sus 8 bloqueantes (solo 15 vías GND): plan recorrido una semana, piloto de 20 el lun 2-nov; pre-órdenes de JLC ya. Script T30 probado en copias en la Mac (DRC sin infracciones nuevas, 0 sin conectar). Merge del lunes (40decf8) arreglado por Leo (240e2c3 + merge=binary) y verificado objeto por objeto. Decisiones para Leo: D-19-1 puertos (si van, entran al piloto), D-19-2 Dbk2, D-19-3 sacar KiCad del auto-sync de las 18:00, D-19-4 clase HV24. Re-auditoría · Pasada 4 · Plan v6 rev. 1.1
16-oct (17:30) — Auditoría de producción de la v6: NO LISTA. Ruteo completo (2612891, DRC 0 errores), pero: U25 con el código del AP64350, sin barrenos, 5 V por In1/In2 a 0.5 mm, cargador con pistas de 0.5 mm y 1 vía en el EP, RF del módem por In1, 81 vías de tierra, stackup genérico. Scripts listos: T25 --set v6 y T28 (probados con DRC). Plan de fines de semana: layout el 17–18 y el 24–25 oct, pedido del piloto de 20 el 26-oct, firmware y banco hasta ≈ 15-nov. P7/P8 se quitan (decisión del 9-oct). Auditoría v6 · Plan
@@ -54,7 +60,7 @@ hide:
-
+
Filosofía de diseño
Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en Tarjetas y hardware.
@@ -67,49 +73,52 @@ hide:
+
diff --git a/docs/tableros/kanban.js b/docs/tableros/kanban.js
index 5ddd153..791ff6b 100644
--- a/docs/tableros/kanban.js
+++ b/docs/tableros/kanban.js
@@ -5,11 +5,20 @@
Uso: const kb = Kanban.montar(document.getElementById('kb'), {
pista: 'ingenieria', // opcional: fija la pista (oculta su filtro y la precarga en "Nuevo")
filtros: true, // barra de filtros (responsable / versión / buscar / pista / solo los míos)
- compacto: false, // tarjetas más pequeñas (tableros que solo muestran un extracto)
+ compacto: false, // tarjetas sin ligas ni etiquetas (la página de inicio)
botonNuevoEn: elemento, // opcional: dónde va el botón «Nuevo pendiente» (p. ej. el encabezado)
- onCargado: (tareas, meta) => {} // callback después de cada carga
+ onCargado: (tareas, meta) => {}, // callback después de cada carga
+ // agregados el 21-oct (página de inicio y vista por persona):
+ vistas: true, // botones «Tablero · Por persona» (?vista=persona en la dirección)
+ limite: 4, // máximo de pendientes por columna; el resto, «N más» → enlaceMas
+ limitePersona: 3, // máximo por celda en la vista por persona
+ enlaceMas: '/tableros/pendientes/', // a dónde llevan los «N más» (sin él, se despliegan ahí)
+ nuevoPorColumna: true, // el «+ Nuevo» al pie de cada columna
+ barraEn: elemento, // los botones de la barra van dentro de este elemento (filtros: 'min')
+ onPintado: (visibles, info) => {} // después de cada pintado: lo que se ve con los filtros
});
kb.recargar();
+ filtros: true = barra completa · 'min' = solo vistas, «Solo los míos» y «Nuevo» · false = sin barra.
Servidor: GET /api/tareas · POST /api/tareas · PATCH /api/tarea?id= · POST /api/tareas/orden ·
DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial.
@@ -51,7 +60,8 @@
flecha: 'M5 12h14M13 6l6 6-6 6',
unidad: 'M5 7.5 12 4l7 3.5v9L12 20l-7-3.5zM5 7.5l7 3.5 7-3.5M12 11v9',
actualizar: 'M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4',
- imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z'
+ imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z',
+ columnas: 'M5 4.5h14a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 18V6A1.5 1.5 0 0 1 5 4.5zM9.3 4.5v15M14.7 4.5v15'
};
const icono = (n, cls) => '';
// Íconos de estado de las columnas (con relleno; colores de los tokens).
@@ -66,14 +76,16 @@
const norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim().replace(/[^a-z0-9_\-.]/g, '');
let EQUIPO = {};
let equipoProm = null;
+ // junta los datos de cada persona sin borrar lo que ya se sabía (p. ej. el rol)
+ function fundir(mapa) { Object.keys(mapa || {}).forEach(id => { EQUIPO[id] = Object.assign({}, EQUIPO[id] || {}, mapa[id]); }); return EQUIPO; }
function cargarEquipo() {
if (!equipoProm) equipoProm = fetch('/api/equipo?todos=1', { cache: 'no-store' }).then(r => r.json()).then(j => {
- const m = {}; (j.miembros || []).forEach(x => m[x.id] = { nombre: x.nombre, activo: x.activo !== false, area: x.area, tiene_login: x.tiene_login });
+ const m = {}; (j.miembros || []).forEach(x => m[x.id] = { nombre: x.nombre, activo: x.activo !== false, area: x.area, rol: x.rol || '', tiene_login: x.tiene_login });
return m;
- }).catch(() => ({})).then(m => { if (!Object.keys(m).length) RESPONSABLES.forEach(id => m[id] = { nombre: id, activo: true }); EQUIPO = Object.assign(EQUIPO, m); return EQUIPO; });
+ }).catch(() => ({})).then(m => { if (!Object.keys(m).length && !Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => m[id] = { nombre: id, activo: true }); return fundir(m); });
return equipoProm;
}
- function usarEquipo(mapa) { if (mapa && Object.keys(mapa).length) EQUIPO = Object.assign(EQUIPO, mapa); if (!Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => EQUIPO[id] = { nombre: id, activo: true }); return EQUIPO; }
+ function usarEquipo(mapa) { if (mapa && Object.keys(mapa).length) fundir(mapa); if (!Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => EQUIPO[id] = { nombre: id, activo: true }); return EQUIPO; }
const partesResp = t => String((t && t.responsable) || '').split(/[,;\/]+/).map(x => x.trim()).filter(Boolean);
// nombres a mostrar: cada parte del campo responsable → nombre del miembro, o el texto tal cual
function nombresResp(t, eq) {
@@ -86,6 +98,12 @@
return partesResp(t).map(p => { const a = norm(p.replace(/\s+/g, '')), b = norm(p); return eq[a] ? a : (eq[b] ? b : null); }).filter(Boolean);
}
const libresResp = (t, eq) => { eq = eq || EQUIPO; return partesResp(t).filter(p => !eq[norm(p.replace(/\s+/g, ''))] && !eq[norm(p)]); };
+ // nombre corto para frases («Leo movió…»): el primer nombre, con los apodos de la casa
+ const APODOS = { leopoldo: 'Leo' };
+ function nombreCorto(id) {
+ const n = String((EQUIPO[id] || {}).nombre || id || '').trim().split(/\s+/)[0];
+ return APODOS[norm(n)] || (EQUIPO[id] ? n : n.charAt(0).toUpperCase() + n.slice(1));
+ }
// ---- avatares: iniciales sobre el color de la persona
function iniciales(nombre) {
@@ -140,8 +158,28 @@
let tareas = [], admin = false, user = '', porId = {};
const q0 = new URLSearchParams(location.search);
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1' };
+ const vistasOn = !!opts.vistas;
+ let vista = vistasOn && (q0.get('vista') === 'persona' || (!q0.get('vista') && opts.vista === 'persona')) ? 'persona' : 'tablero';
+ const desplegadas = new Set(); // celdas de la vista por persona que ya muestran todo
root.classList.add('kanban-root');
const contenedor = root.closest('.tb') || root;
+ // los filtros que vale la pena compartir viajan en la dirección (?mios=1, ?vista=persona)
+ function ponURL(k, v) {
+ if (opts.url === false) return;
+ const u = new URL(location.href);
+ if (v) u.searchParams.set(k, v); else u.searchParams.delete(k);
+ history.replaceState(null, '', u.pathname + u.search + u.hash);
+ }
+ // dirección del tablero completo con lo que se está viendo (para los «N más» de la página de inicio)
+ function enlaceCompleto(extra) {
+ const u = new URLSearchParams();
+ if (filtro.mios) u.set('mios', '1');
+ if (vista === 'persona') u.set('vista', 'persona');
+ if (opts.pista) u.set('pista', opts.pista);
+ Object.entries(extra || {}).forEach(([k, v]) => v && u.set(k, v));
+ const q = u.toString();
+ return (opts.enlaceMas || '/tableros/pendientes/') + (q ? '?' + q : '');
+ }
// ---------- barra de filtros ----------
const barra = el('div', { class: 'kb-barra' });
@@ -151,16 +189,29 @@
const selVer = el('select', { title: 'Versión de tarjeta', 'aria-label': 'Versión de tarjeta' }); selVer.append(el('option', { value: '', text: 'Tarjeta: todas' }));
VERSIONES.filter(v => v !== '—').forEach(v => selVer.append(el('option', { value: v, text: v })));
const selHechas = el('select', { title: 'Hechos', 'aria-label': 'Hechos' });
- [['recientes', 'Hechos: últimos 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
+ [['recientes', 'Hechos: 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
const buscar = el('input', { type: 'search', placeholder: 'Buscar en pendientes', 'aria-label': 'Buscar en pendientes' });
const btnMios = el('button', { type: 'button', class: 'kb-solo', 'aria-pressed': String(filtro.mios), html: icono('persona') + 'Solo los míos' });
const btnNuevo = el('button', { type: 'button', class: 'kb-nuevo', html: icono('mas') + 'Nuevo pendiente' });
- if (opts.filtros !== false) {
+ const selVista = el('div', { class: 'kb-vistas', role: 'group', 'aria-label': 'Vista' });
+ [['tablero', 'Tablero', 'columnas'], ['persona', 'Por persona', 'equipo']].forEach(([k, n, ic]) => {
+ const b = el('button', { type: 'button', 'data-vista': k, 'aria-pressed': String(vista === k), html: icono(ic) + n });
+ b.addEventListener('click', () => { if (vista === k) return; vista = k; ponURL('vista', k === 'persona' ? 'persona' : null); pintar(); });
+ selVista.append(b);
+ });
+ const modo = opts.filtros === undefined ? true : opts.filtros;
+ if (modo) {
+ if (vistasOn) barra.append(selVista);
barra.append(btnMios);
- if (!opts.pista) barra.append(selPista);
- barra.append(selResp, selVer, selHechas, buscar);
+ if (modo !== 'min') { if (!opts.pista) barra.append(selPista); barra.append(selResp, selVer, selHechas, buscar); }
if (opts.botonNuevoEn) opts.botonNuevoEn.append(btnNuevo); else barra.append(btnNuevo);
- root.append(barra);
+ if (opts.barraEn) {
+ // la página ya puso ahí sus propios elementos (p. ej. una liga): las vistas y «Solo los míos»
+ // van antes, «Nuevo pendiente» al final
+ const nodos = [...barra.childNodes];
+ nodos.filter(n => n !== btnNuevo).reverse().forEach(n => opts.barraEn.prepend(n));
+ if (nodos.includes(btnNuevo)) opts.barraEn.append(btnNuevo);
+ } else root.append(barra);
}
const aviso = el('div', { class: 'msg', role: 'status' }); root.append(aviso);
function mensaje(txt, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = txt; clearTimeout(aviso._t); aviso._t = setTimeout(() => { aviso.className = 'msg'; }, bien ? 3500 : 8000); }
@@ -172,7 +223,7 @@
ESTADOS.forEach(([k, nombre]) => {
const c = el('div', { class: 'col e-' + k, 'data-estado': k });
c.append(el('h3', { html: ESTADO_SVG[k] + '' + nombre + '0' }));
- if (k !== 'hecho') {
+ if (k !== 'hecho' && opts.nuevoPorColumna !== false) {
const mas = el('button', { type: 'button', class: 'kb-mas', html: icono('mas') + 'Nuevo' });
mas.addEventListener('click', () => abrir(null, k));
c.append(mas);
@@ -191,6 +242,8 @@
cols[k] = c; kb.append(c);
});
root.append(kb);
+ const per = el('div', { class: 'kb-personas', hidden: '' });
+ root.append(per);
// ---------- filtrado y pintado ----------
function visibles() {
@@ -235,13 +288,13 @@
const bl = bloqueosAbiertos(t);
if (bl.length) c.append(el('div', { class: 'nota', html: icono('aviso') + 'Espera: ' + esc(bl.map(b => corto(b.titulo.split(':')[0], 42)).join(' · ')) + '' }));
// ligas
- if ((t.enlaces || []).length) {
+ if (!opts.compacto && (t.enlaces || []).length) {
const e = el('div', { class: 'enl' });
t.enlaces.slice(0, 2).forEach(x => { const a = el('a', { href: x.url, html: icono('liga') + '' + esc(x.titulo || x.url) + '' }); a.addEventListener('click', ev => ev.stopPropagation()); e.append(a); });
if (t.enlaces.length > 2) e.append(el('span', { class: 'vacio', text: '+' + (t.enlaces.length - 2) + ' más' }));
c.append(e);
}
- if ((t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); }
+ if (!opts.compacto && (t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); }
// responsables y fecha
const pie = el('div', { class: 'pie' });
pie.append(t.responsable ? avatares(t) : el('span', { class: 'vacio', text: 'Sin responsable' }));
@@ -261,12 +314,21 @@
function pintar() {
porId = {}; tareas.forEach(t => porId[t.id] = t);
const vis = visibles();
- ESTADOS.forEach(([k]) => {
- const c = cols[k]; c.querySelectorAll('.card').forEach(x => x.remove());
+ selVista.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vista === vista)));
+ kb.hidden = vista === 'persona';
+ per.hidden = vista !== 'persona';
+ if (vista === 'persona') pintarPersonas(vis);
+ else ESTADOS.forEach(([k]) => {
+ const c = cols[k]; c.querySelectorAll('.card, .kb-mas-n').forEach(x => x.remove());
const lst = vis.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
+ // con tope (página de inicio), en Hecho van primero los más recientes
+ if (opts.limite && k === 'hecho') lst.sort((a, b) => String(b.modificado_en || '').localeCompare(String(a.modificado_en || '')));
c.querySelector('.n').textContent = lst.length;
const mas = c.querySelector('.kb-mas');
- lst.forEach(t => mas ? c.insertBefore(tarjeta(t), mas) : c.append(tarjeta(t)));
+ const pon = x => mas ? c.insertBefore(x, mas) : c.append(x);
+ const tope = !opts.limite ? Infinity : (typeof opts.limite === 'object' ? (opts.limite[k] || opts.limite.otros || 4) : opts.limite);
+ lst.slice(0, tope).forEach(t => pon(tarjeta(t)));
+ if (lst.length > tope) pon(el('a', { class: 'kb-mas-n', href: enlaceCompleto(), text: (lst.length - tope) + ' más' }));
});
// responsables presentes en los datos (más los conocidos)
selResp.querySelectorAll('option:not(:first-child)').forEach(o => o.remove());
@@ -278,6 +340,86 @@
btnMios.setAttribute('aria-pressed', String(filtro.mios));
btnMios.disabled = !user;
btnMios.title = user ? 'Solo los pendientes de ' + ((EQUIPO[norm(user)] || {}).nombre || user) : 'No se sabe quién eres (falta X-Remote-User)';
+ if (opts.onPintado) opts.onPintado(vis, { vista, filtro, user, admin });
+ }
+
+ // ---------- vista por persona: una fila por responsable, una columna por estado ----------
+ function filtrosActivos() { return !!((filtro.pista && !opts.pista) || filtro.responsable || filtro.version || filtro.buscar.trim() || filtro.mios); }
+ function itemPersona(t) {
+ const b = el('button', { type: 'button', class: 'kb-p-item' + (t.vencida ? ' vencida' : '') + (t.estado === 'hecho' ? ' e-hecho' : ''), title: t.titulo });
+ b.append(el('span', { class: 'kb-p-tit', text: t.titulo }));
+ const f = fechaDe(t);
+ if (f) b.append(el('span', { class: 'fecha ' + f.cls, text: f.texto }));
+ b.addEventListener('click', () => abrir(t));
+ return b;
+ }
+ function pintarPersonas(vis) {
+ per.innerHTML = '';
+ const grupos = new Map();
+ const agrega = (clave, t, libre) => { if (!grupos.has(clave)) grupos.set(clave, { clave, libre, tareas: [] }); grupos.get(clave).tareas.push(t); };
+ vis.forEach(t => {
+ const ids = idsResp(t), libres = libresResp(t);
+ ids.forEach(id => agrega(id, t));
+ libres.forEach(x => agrega('libre:' + norm(x), t, x));
+ if (!ids.length && !libres.length) agrega('_nadie', t, 'Sin responsable');
+ });
+ const abiertos = g => g.tareas.filter(t => t.estado !== 'hecho').length;
+ const nombre = g => g.libre || (EQUIPO[g.clave] || {}).nombre || g.clave;
+ const filas = [...grupos.values()].sort((a, b) => (abiertos(b) - abiertos(a)) || nombre(a).localeCompare(nombre(b)));
+
+ const cab = el('div', { class: 'kb-p-fila kb-p-cab', 'aria-hidden': 'true' });
+ cab.append(el('span', { text: 'Persona' }));
+ ESTADOS.forEach(([k, n]) => cab.append(el('span', { html: ESTADO_SVG[k] + '' + (k === 'hecho' && filtro.hechas === 'recientes' ? 'Hecho, 14 días' : n) + '' })));
+ per.append(cab);
+ if (!filas.length) per.append(el('p', { class: 'vacio', text: 'Ningún pendiente con estos filtros.' }));
+
+ const N = opts.limitePersona || 3;
+ filas.forEach(g => {
+ const fila = el('div', { class: 'kb-p-fila' });
+ const quien = el('div', { class: 'kb-p-quien' });
+ quien.append(g.libre ? avatar(null, g.libre) : avatar(g.clave));
+ const m = EQUIPO[g.clave] || {};
+ const ab = abiertos(g), bl = g.tareas.filter(t => t.estado === 'bloqueado').length, ve = g.tareas.filter(t => t.vencida).length;
+ const carga = ab ? ab + (ab === 1 ? ' abierto' : ' abiertos') + (bl ? ', ' + bl + (bl === 1 ? ' bloqueado' : ' bloqueados') : '') + (ve ? ', ' + ve + (ve === 1 ? ' vencido' : ' vencidos') : '') : 'Nada abierto';
+ const txt = el('span', { class: 'kb-p-nom' });
+ txt.append(el('b', { text: nombre(g) }));
+ if (m.rol) txt.append(el('span', { class: 'kb-p-rol', text: m.rol }));
+ txt.append(el('span', { class: 'kb-p-carga' + (ve ? ' mal' : (bl ? ' aviso' : '')), text: carga }));
+ quien.append(txt);
+ fila.append(quien);
+ ESTADOS.forEach(([k, n]) => {
+ const celda = el('div', { class: 'kb-p-celda e-' + k, 'data-col': n });
+ let lst = g.tareas.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
+ if (k === 'hecho') lst = lst.sort((a, b) => String(b.modificado_en || '').localeCompare(String(a.modificado_en || '')));
+ const clave = g.clave + '|' + k;
+ const todo = desplegadas.has(clave);
+ (todo ? lst : lst.slice(0, N)).forEach(t => celda.append(itemPersona(t)));
+ if (!todo && lst.length > N) {
+ if (opts.enlaceMas) celda.append(el('a', { class: 'kb-mas-n', href: enlaceCompleto(g.libre ? {} : { responsable: g.clave }), text: (lst.length - N) + ' más' }));
+ else {
+ const b = el('button', { type: 'button', class: 'kb-mas-n', text: (lst.length - N) + ' más' });
+ b.addEventListener('click', () => { desplegadas.add(clave); pintar(); });
+ celda.append(b);
+ }
+ }
+ if (!lst.length) celda.classList.add('vacia');
+ fila.append(celda);
+ });
+ per.append(fila);
+ });
+
+ // quienes no tienen nada abierto (solo sin filtros: con filtros sería engañoso)
+ if (!filtrosActivos()) {
+ const con = new Set(filas.filter(g => abiertos(g)).map(g => g.clave));
+ const libres = Object.entries(EQUIPO).filter(([id, m]) => m.activo !== false && !con.has(id) && m.nombre)
+ .sort((a, b) => a[1].nombre.localeCompare(b[1].nombre));
+ if (libres.length) {
+ const f = el('div', { class: 'kb-p-libres' });
+ f.append(el('span', { class: 'kb-p-libres-t', text: 'Sin pendientes abiertos' }));
+ libres.forEach(([id]) => f.append(el('span', { class: 'persona' }, avatar(id), document.createTextNode(nombreCorto(id)))));
+ per.append(f);
+ }
+ }
}
async function recargar() {
@@ -416,19 +558,16 @@
selVer.addEventListener('change', () => { filtro.version = selVer.value; pintar(); });
selHechas.addEventListener('change', () => { filtro.hechas = selHechas.value; pintar(); });
buscar.addEventListener('input', () => { filtro.buscar = buscar.value; pintar(); });
- btnMios.addEventListener('click', () => {
- filtro.mios = !filtro.mios; pintar();
- const u = new URL(location.href); if (filtro.mios) u.searchParams.set('mios', '1'); else u.searchParams.delete('mios');
- history.replaceState(null, '', u.pathname + u.search + u.hash);
- });
+ btnMios.addEventListener('click', () => { filtro.mios = !filtro.mios; ponURL('mios', filtro.mios ? '1' : null); pintar(); });
btnNuevo.addEventListener('click', () => abrir(null));
if (q0.get('pista') && !opts.pista) { selPista.value = q0.get('pista'); filtro.pista = q0.get('pista'); }
if (q0.get('responsable')) { filtro.responsable = q0.get('responsable').toLowerCase(); }
if (q0.get('id')) setTimeout(() => { const t = porId[q0.get('id')]; if (t) abrir(t); }, 600);
+ if (vistasOn) cargarEquipo().then(() => { if (vista === 'persona' && tareas.length) pintar(); });
recargar();
- return { recargar, abrir, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; } };
+ return { recargar, abrir, pintar, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; }, get vista() { return vista; } };
}
- window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, avatar, avatares, icono, fechaDe, get equipo() { return EQUIPO; } };
+ window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, nombreCorto, avatar, avatares, icono, fechaDe, fechaCorta, get equipo() { return EQUIPO; } };
})();
diff --git a/docs/tableros/pendientes.md b/docs/tableros/pendientes.md
index 21182ec..ccb4b01 100644
--- a/docs/tableros/pendientes.md
+++ b/docs/tableros/pendientes.md
@@ -22,6 +22,7 @@ hide:
Columna Bloqueado: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.
Al terminar, muévelo a Hecho. No lo borres: solo el administrador borra, y el historial sirve de bitácora.
Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.
+
Por persona muestra una fila por responsable con sus pendientes en cada estado: sirve para ver quién está cargado y quién está libre. La dirección (?vista=persona, ?mios=1) se puede compartir tal cual.
@@ -32,6 +33,7 @@ hide:
(function () {
Kanban.montar(document.getElementById('kb'), {
filtros: true,
+ vistas: true,
botonNuevoEn: document.getElementById('acciones-kb'),
onCargado: (tareas, meta) => {
const abiertas = tareas.filter(t => t.estado !== 'hecho').length;
diff --git a/docs/tableros/ruta.js b/docs/tableros/ruta.js
new file mode 100644
index 0000000..c48deff
--- /dev/null
+++ b/docs/tableros/ruta.js
@@ -0,0 +1,96 @@
+/* Ruta a producción: una fila por versión de EinarBoard y sus siete pasos
+ (esquemático → layout → auditoría → Gerber → piloto → banco → pedido).
+ Los datos salen de /datos/proyecto.json («tarjetas» y «etapas»); este archivo solo dibuja.
+ Lo usan la página de inicio y el tablero de Ingeniería.
+
+ Uso: Ruta.montar(contenedor, datos, { enlace: t => '/tableros/ingenieria/#' + t.clave });
+
+ Cómo se lee: línea continua = hecho; punteada = lo que falta; el anillo de color marca el paso
+ en el que va cada tarjeta (cian = en curso, coral = detenida). En el teléfono los textos de cada
+ paso se esconden y queda una línea con el paso actual y el que sigue.
+ Estilos: tableros/tablero.css (sección «Ruta a producción»). */
+(function () {
+ const TONO = { hecho: 'Lista', ahora: 'En curso', aviso: 'Con pendientes', bloqueado: 'Detenida' };
+ const ESTADO_PASO = { hecho: 'hecho', ahora: 'en curso', bloqueado: 'detenido', falta: 'falta' };
+ const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
+
+ const el = (t, a, ...k) => {
+ const e = document.createElement(t);
+ if (a) for (const p in a) { if (p === 'text') e.textContent = a[p]; else if (p === 'html') e.innerHTML = a[p]; else e.setAttribute(p, a[p]); }
+ k.forEach(c => c != null && e.append(c));
+ return e;
+ };
+
+ // «Layout · Pasada 5 · Michelle, ≈ 1 h»
+ const describe = (etapa, p) => [etapa, p[1], p[2]].filter(Boolean).join(' · ');
+
+ function resumen(t, etapas) {
+ const pasos = t.pasos || [];
+ const i = pasos.findIndex(p => p[0] === 'ahora' || p[0] === 'bloqueado');
+ if (i < 0) {
+ if (pasos.length && pasos.every(p => p[0] === 'hecho')) return 'Todos los pasos hechos.';
+ const j = pasos.findIndex(p => p[0] === 'falta');
+ return j < 0 ? '' : 'Sigue: ' + describe(etapas[j], pasos[j]);
+ }
+ let s = (pasos[i][0] === 'bloqueado' ? 'Detenida en ' : 'Ahora: ') + describe(etapas[i], pasos[i]);
+ const j = pasos.findIndex((p, k) => k > i && p[0] === 'falta');
+ if (j > 0) s += '. Sigue: ' + [etapas[j], pasos[j][1]].filter(Boolean).join(' · ');
+ return s + '.';
+ }
+
+ function montar(cont, datos, opts) {
+ opts = opts || {};
+ const etapas = datos.etapas || [];
+ const tarjetas = datos.tarjetas || [];
+ cont.innerHTML = '';
+ const caja = el('div', { class: 'ruta', style: '--ruta-pasos:' + etapas.length });
+
+ const cab = el('div', { class: 'ruta-fila ruta-cab', 'aria-hidden': 'true' });
+ const nombres = el('div', { class: 'ruta-pasos-cab' });
+ etapas.forEach(e => nombres.append(el('span', { text: e })));
+ cab.append(el('span', { class: 'ruta-cab-t', text: 'Tarjeta' }), nombres);
+ caja.append(cab);
+
+ tarjetas.forEach(t => {
+ const fila = el('div', { class: 'ruta-fila tono-' + (t.tono || 'falta'), role: 'group', 'aria-label': 'EinarBoard ' + t.version + ': ' + (t.estado || '') });
+ if (opts.anclas) fila.id = t.clave;
+
+ const href = opts.enlace ? opts.enlace(t) : (t.liga || '#');
+ const info = el('a', { class: 'ruta-tarjeta', href });
+ if (t.imagen) info.append(el('img', { src: t.imagen, alt: t.alt || ('EinarBoard ' + t.version), width: '420', height: '300', loading: 'lazy', decoding: 'async' }));
+ const txt = el('span', { class: 'ruta-info' });
+ txt.append(el('span', { class: 'ruta-ver' },
+ el('span', { class: BADGE[t.version] || 'badge-v4', text: t.version }),
+ el('span', { class: 'ruta-estado', text: t.estado || TONO[t.tono] || '' })));
+ if (t.nuevo) txt.append(el('span', { class: 'ruta-nuevo', text: t.nuevo }));
+ info.append(txt);
+ fila.append(info);
+
+ const pasos = t.pasos || [];
+ const lista = el('ol', { class: 'ruta-pasos' });
+ pasos.forEach((p, i) => {
+ const est = p[0] || 'falta';
+ const llega = est === 'hecho' || est === 'ahora' || est === 'bloqueado';
+ const sale = est === 'hecho' && pasos[i + 1] && pasos[i + 1][0] !== 'falta';
+ const cls = ['ruta-paso', 'p-' + est];
+ if (i === 0) cls.push('primero');
+ if (i === pasos.length - 1) cls.push('ultimo');
+ if (llega) cls.push('entra');
+ if (sale || (est === 'hecho' && i === pasos.length - 1)) cls.push('sale');
+ const li = el('li', { class: cls.join(' '), title: describe(etapas[i], p) + ' (' + ESTADO_PASO[est] + ')' });
+ li.append(el('span', { class: 'ruta-punto', 'aria-hidden': 'true' }));
+ li.append(el('span', { class: 'vh', text: (etapas[i] || '') + ', ' + ESTADO_PASO[est] + '. ' }));
+ li.append(el('span', { class: 'ruta-txt', text: p[1] || '' }));
+ if (p[2]) li.append(el('span', { class: 'ruta-sub', text: p[2] }));
+ lista.append(li);
+ });
+ fila.append(lista);
+ fila.append(el('p', { class: 'ruta-resumen', text: resumen(t, etapas) }));
+ caja.append(fila);
+ });
+ cont.append(caja);
+ return caja;
+ }
+
+ window.Ruta = { montar, resumen };
+})();
diff --git a/docs/tableros/tablero.css b/docs/tableros/tablero.css
index 4f64d7e..13394ff 100644
--- a/docs/tableros/tablero.css
+++ b/docs/tableros/tablero.css
@@ -245,7 +245,7 @@
/* ---- Tablero de pendientes ---------------------------------------------- */
.tb .kb-barra { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 14px; }
.tb .kb-barra select, .tb .kb-barra input { padding: 7px 10px; font-size: 13.5px; }
-.tb .kb-barra input[type="search"] { flex: 0 1 210px; min-width: 140px; }
+.tb .kb-barra input[type="search"] { flex: 1 1 150px; min-width: 130px; max-width: 280px; }
.tb .kb-barra .cnt { color: var(--hg-texto-3); font-size: 13px; }
.tb .kb-barra .kb-nuevo { margin-left: auto; }
.tb .kb-solo {
@@ -338,6 +338,7 @@
.tb .card .mover { display: flex; gap: 6px; align-items: center; }
.tb .card .mover select { padding: 5px 8px; font-size: 13px; flex: 1; }
@media (min-width: 981px) { .tb .card .mover { display: none; } .tb .kb.movil .card .mover { display: flex; } }
+.tb .kb.compacto .card .mover { display: none; } /* el extracto del inicio: se mueve desde el detalle o el tablero completo */
/* ---- Diálogo (detalle y alta) -------------------------------------------- */
.tb dialog {
@@ -524,6 +525,147 @@
.tb .visita .cl { font-weight: 700; }
.tb .marca-unidad { border-radius: 10px; padding: 10px 14px; margin: 0 0 14px; font-size: 14.5px; }
+/* ---- Vistas del tablero y «N más» (21-oct) -------------------------------- */
+.tb .kb-vistas { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--hg-linea-2); border-radius: 10px; background: var(--hg-plano-2); }
+.tb .kb-vistas button { background: transparent; border: 0; color: var(--hg-texto-2); font-size: 13.5px; font-weight: 500; padding: 5px 11px; border-radius: 7px; }
+.tb .kb-vistas button[aria-pressed="true"] { background: var(--hg-elevado); color: var(--hg-texto-fuerte); font-weight: 600; }
+.tb .kb-vistas button:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
+.tb .kb-vistas .i { width: 15px; height: 15px; }
+.tb .kb-mas-n { align-self: flex-start; padding: 4px; font-size: 13px; color: var(--hg-texto-2); text-decoration: none; }
+.tb a.kb-mas-n:hover { color: var(--hg-liga-hover); text-decoration: underline; }
+.tb button.kb-mas-n { background: none; border: 0; font-weight: 500; }
+.tb button.kb-mas-n:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
+.tb [hidden] { display: none !important; }
+
+/* ---- Vista por persona: una fila por responsable --------------------------- */
+.tb .kb-personas { border: 1px solid var(--hg-linea); border-radius: 12px; background: var(--hg-plano-2); }
+.tb .kb-p-fila { display: grid; grid-template-columns: minmax(200px, 1.15fr) repeat(4, minmax(0, 1fr)); gap: 0 14px; align-items: start; padding: 12px 16px; border-top: 1px solid var(--hg-linea); }
+.tb .kb-p-fila:first-child { border-top: 0; }
+.tb .kb-p-cab { padding-top: 11px; padding-bottom: 11px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); }
+.tb .kb-p-cab > span { display: inline-flex; align-items: center; gap: 7px; }
+.tb .kb-p-cab svg { width: 15px; height: 15px; flex: none; }
+.tb .kb-p-quien { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
+.tb .kb-p-quien .av { width: 30px; height: 30px; font-size: 11px; }
+.tb .kb-p-nom { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
+.tb .kb-p-nom b { font-weight: 600; color: var(--hg-texto-fuerte); }
+.tb .kb-p-rol { font-size: 12.5px; color: var(--hg-texto-3); }
+.tb .kb-p-carga { margin-top: 2px; font-size: 12.5px; color: var(--hg-texto-2); }
+.tb .kb-p-carga.mal { color: var(--hg-bloqueado); }
+.tb .kb-p-carga.aviso { color: var(--hg-aviso); }
+.tb .kb-p-celda { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
+.tb button.kb-p-item {
+ display: flex; flex-direction: column; align-items: stretch; gap: 3px;
+ width: 100%; padding: 8px 10px; border-radius: 8px;
+ border: 1px solid var(--hg-linea); background: var(--hg-plano); color: var(--hg-texto);
+ font-size: 13.5px; font-weight: 500; line-height: 1.35; text-align: left; white-space: normal;
+}
+.tb button.kb-p-item:hover:not(:disabled) { filter: none; border-color: var(--hg-linea-2); color: var(--hg-texto-fuerte); }
+.tb button.kb-p-item.vencida { border-color: color-mix(in srgb, var(--hg-bloqueado) 45%, var(--hg-linea)); }
+.tb button.kb-p-item.e-hecho { color: var(--hg-texto-2); }
+.tb .kb-p-tit { overflow-wrap: anywhere; }
+.tb .kb-p-item .fecha { font-size: 12px; font-weight: 400; color: var(--hg-texto-3); }
+.tb .kb-p-item .fecha.hoy { color: var(--hg-acento); font-weight: 600; }
+.tb .kb-p-item .fecha.mal { color: var(--hg-bloqueado); font-weight: 600; }
+.tb .kb-p-libres { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 12px 16px; border-top: 1px solid var(--hg-linea); font-size: 13px; color: var(--hg-texto-2); }
+.tb .kb-p-libres-t { color: var(--hg-texto-3); margin-right: 2px; }
+@media (max-width: 900px) {
+ .tb .kb-p-cab { display: none; }
+ .tb .kb-p-cab + .kb-p-fila { border-top: 0; }
+ .tb .kb-p-fila { grid-template-columns: 1fr 1fr; gap: 10px 12px; }
+ .tb .kb-p-quien { grid-column: 1 / -1; }
+ .tb .kb-p-celda::before { content: attr(data-col); font-size: 12px; font-weight: 600; color: var(--hg-texto-3); }
+ .tb .kb-p-celda.vacia { display: none; }
+}
+@media (max-width: 520px) { .tb .kb-p-fila { grid-template-columns: 1fr; } }
+
+/* ---- Ruta a producción (inicio e Ingeniería) -------------------------------
+ Línea continua = hecho (blanco en oscuro, tinta en claro); punteada = lo que falta;
+ anillo cian = el paso en curso; coral = detenido. */
+.tb .ruta { border: 1px solid var(--hg-linea); border-radius: 14px; background: var(--hg-plano-2); padding: 2px 18px; }
+.tb .ruta-fila { display: grid; grid-template-columns: minmax(230px, 1.4fr) minmax(0, 5fr); gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--hg-linea); }
+.tb .ruta-fila:first-child { border-top: 0; }
+.tb .ruta-cab { padding: 12px 0 8px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); align-items: end; }
+.tb .ruta-cab-t { color: var(--hg-texto-3); font-weight: 500; }
+/* (Material pone display: flow-root a .md-typeset ol:not([hidden]); por eso el selector largo) */
+.tb .ruta-pasos-cab,
+.md-typeset .tb ol.ruta-pasos:not([hidden]) { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(var(--ruta-pasos, 7), minmax(0, 1fr)); }
+.tb .ruta-pasos-cab span { text-align: center; padding: 0 4px; }
+.md-typeset .tb ol.ruta-pasos { list-style: none; margin: 0; padding: 0; }
+.md-typeset .tb .ruta-pasos li { margin: 0; }
+.tb .ruta-paso { position: relative; display: flex; flex-direction: column; align-items: center; min-width: 0; padding: 0 4px; text-align: center; }
+.tb .ruta-paso::before,
+.tb .ruta-paso::after { content: ""; position: absolute; top: 11px; width: 50%; border-top: 1px dashed var(--hg-linea-2); }
+.tb .ruta-paso::before { left: 0; }
+.tb .ruta-paso::after { right: 0; }
+.tb .ruta-paso.entra::before,
+.tb .ruta-paso.sale::after { top: 10px; border-top: 3px solid var(--hg-ruta-linea); }
+.tb .ruta-paso.primero::before,
+.tb .ruta-paso.ultimo::after { display: none; }
+.tb .ruta-punto { position: relative; z-index: 1; box-sizing: border-box; width: 16px; height: 16px; margin: 3px 0 8px; border-radius: 50%; border: 2px solid var(--hg-linea-2); background: var(--hg-plano-2); }
+.tb .p-hecho .ruta-punto { border: 3.5px solid var(--hg-ruta); }
+.tb .p-ahora .ruta-punto,
+.tb .p-bloqueado .ruta-punto { width: 22px; height: 22px; margin: 0 0 5px; border: 4px solid var(--hg-acento); box-shadow: 0 0 0 5px var(--hg-halo-curso); }
+.tb .p-bloqueado .ruta-punto { border-color: var(--hg-bloqueado); box-shadow: 0 0 0 5px var(--hg-halo-bloqueado); }
+.tb .ruta-txt { font-size: 13px; line-height: 1.3; color: var(--hg-texto-2); overflow-wrap: anywhere; }
+.tb .p-hecho .ruta-txt { color: var(--hg-texto-suave); }
+.tb .p-ahora .ruta-txt { color: var(--hg-acento); font-weight: 700; }
+.tb .p-bloqueado .ruta-txt { color: var(--hg-bloqueado); font-weight: 700; }
+.tb .ruta-sub { margin-top: 2px; font-size: 12px; line-height: 1.3; color: var(--hg-texto-3); overflow-wrap: anywhere; }
+.tb a.ruta-tarjeta { display: flex; gap: 12px; align-items: center; min-width: 0; color: inherit; text-decoration: none; }
+.tb a.ruta-tarjeta img { flex: none; width: 100px; height: auto; aspect-ratio: 7 / 5; object-fit: cover; border-radius: 7px; border: 1px solid var(--hg-linea); background: #0E1524; filter: brightness(1.2) contrast(1.05); }
+.tb a.ruta-tarjeta:hover img { border-color: var(--hg-linea-2); }
+.tb a.ruta-tarjeta:hover .ruta-nuevo { color: var(--hg-texto-suave); }
+.tb .ruta-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
+.tb .ruta-ver { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
+.tb .ruta-estado { font-size: 14px; font-weight: 700; color: var(--hg-texto); }
+.tb .tono-hecho .ruta-estado { color: var(--hg-texto-fuerte); }
+.tb .tono-ahora .ruta-estado { color: var(--hg-acento); }
+.tb .tono-aviso .ruta-estado { color: var(--hg-aviso); }
+.tb .tono-bloqueado .ruta-estado { color: var(--hg-bloqueado); }
+.tb .ruta-nuevo { font-size: 13px; line-height: 1.35; color: var(--hg-texto-2); }
+.md-typeset .tb p.ruta-resumen { display: none; }
+@media (max-width: 1000px) {
+ .tb .ruta-fila { grid-template-columns: 1fr; gap: 12px; }
+ .tb .ruta-cab { display: none; }
+ .tb .ruta-cab + .ruta-fila { border-top: 0; }
+}
+@media (max-width: 600px) {
+ .tb .ruta { padding: 2px 14px; }
+ .tb .ruta-txt, .tb .ruta-sub { display: none; }
+ .tb .ruta-punto { margin-bottom: 0; }
+ .md-typeset .tb p.ruta-resumen { display: block; margin: 0; font-size: 13.5px; line-height: 1.4; color: var(--hg-texto-suave); }
+}
+
+/* ---- Actividad (inicio y Bitácora) ------------------------------------------ */
+.md-typeset .tb ol.act-lista { list-style: none; margin: 0; padding: 0; }
+.md-typeset .tb .act-lista li { margin: 0; }
+.tb .act { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1px 10px; padding: 9px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 14px; line-height: 1.4; }
+.tb .act:first-child { border-top: 0; }
+.tb .act .av { grid-row: span 2; width: 26px; height: 26px; margin-top: 1px; font-size: 10px; }
+.tb .act-t { color: var(--hg-texto-suave); overflow-wrap: anywhere; }
+.tb .act-t b { font-weight: 600; color: var(--hg-texto-fuerte); }
+.tb .act-t a { color: var(--hg-texto); text-decoration: none; }
+.tb .act-t a:hover { color: var(--hg-liga-hover); text-decoration: underline; }
+.tb .act-nota { display: block; margin-top: 4px; padding: 6px 9px; border-radius: 8px; background: var(--hg-plano); font-size: 13px; color: var(--hg-texto-2); }
+.tb .act-res { padding: 1px 7px; border-radius: 5px; font-size: 12px; font-weight: 600; background: var(--hg-hecho-bg); color: var(--hg-hecho); white-space: nowrap; }
+.tb .act-res.mal { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); }
+.tb .act-f { grid-column: 2; font-size: 12.5px; color: var(--hg-texto-3); }
+.md-typeset .tb h3.act-dia { margin: 18px 0 2px; font-family: var(--hg-fuente); font-size: 13px; font-weight: 600; color: var(--hg-texto-3); }
+.md-typeset .tb h3.act-dia:first-child { margin-top: 0; }
+.tb .act-mas { display: inline-block; margin-top: 8px; font-size: 13px; }
+
+/* ---- Ingeniería: la ruta arriba; Bitácora: página de lectura ------------------- */
+.md-typeset .tb h2.ing-h { margin: 4px 0 4px; }
+.tb .ing-ruta { margin-bottom: 16px; }
+.tb .versiones .ver { scroll-margin-top: 4.5rem; }
+.md-typeset .tb-bitacora h2 { margin: 30px 0 10px; }
+.md-typeset .tb-bitacora h3 { margin: 24px 0 8px; font-family: var(--hg-fuente-titulo); font-size: 18px; font-weight: 600; }
+.tb-bitacora #bit-actividad { padding: 8px 16px 12px; border: 1px solid var(--hg-linea); border-radius: 12px; background: var(--hg-plano-2); }
+.md-typeset .tb-bitacora dd { margin-bottom: 6px; }
+
+/* texto solo para lectores de pantalla */
+.tb .vh { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
+
@media print {
.tb .tb-cab__acc, .tb .buscar, .tb button, .tb .kb-barra, .tb details.tb-como { display: none !important; }
.tb section.bloque, .tb .kpi, .tb .card, .tb .cel, .tb .cadena .paso { background: #fff !important; border-color: #999 !important; color: #000 !important; }
diff --git a/hooks/documentos.py b/hooks/documentos.py
new file mode 100644
index 0000000..159535e
--- /dev/null
+++ b/hooks/documentos.py
@@ -0,0 +1,52 @@
+"""Documentos con fecha para la página de inicio («Documentos recientes»), 21-oct-2026.
+
+Cada página .md con `date:` en su encabezado (AAAA-MM-DD) entra en `datos/documentos.json`
+del sitio construido, del más nuevo al más viejo. Lo lee docs/inicio/inicio.js.
+Las páginas que en el menú están bajo «Historia» (lo superado) no entran.
+No toca ningún archivo de docs/: el JSON solo existe en site/.
+"""
+import datetime
+import json
+import logging
+import os
+import re
+
+log = logging.getLogger("mkdocs.hooks.documentos")
+_DOCS = []
+_FECHA = re.compile(r"^\d{4}-\d{2}-\d{2}$")
+# fecha al final del título: «… (21-oct-2026)», «… · 21-oct-2026», «… — 16 oct»
+_COLA = re.compile(r"\s*[(·—–-]\s*\d{1,2}[-\s](?:ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)[a-z]*\.?(?:[-\s]\d{4})?\s*\)?\s*$", re.I)
+# o un paréntesis final con fecha: «… (19-oct-2026, tarde)», «… (24-oct → 22-nov-2026)»
+_PAREN = re.compile(r"\s*\([^()]*\d{1,2}[-\s](?:ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)[^()]*\)\s*$", re.I)
+
+
+def on_pre_build(config, **kwargs):
+ _DOCS.clear()
+
+
+def _fecha(valor):
+ if isinstance(valor, (datetime.date, datetime.datetime)):
+ return valor.isoformat()[:10]
+ texto = str(valor or "")[:10]
+ return texto if _FECHA.match(texto) else None
+
+
+def on_page_markdown(markdown, page, config, files, **kwargs):
+ fecha = _fecha(page.meta.get("date"))
+ if not fecha:
+ return markdown
+ if any(getattr(a, "title", "") == "Historia" for a in (page.ancestors or [])):
+ return markdown
+ titulo = str(page.meta.get("title") or page.title or "").strip()
+ titulo = _COLA.sub("", _PAREN.sub("", titulo)).strip() or titulo
+ _DOCS.append({"titulo": titulo, "fecha": fecha, "url": "/" + page.url})
+ return markdown
+
+
+def on_post_build(config, **kwargs):
+ docs = sorted(_DOCS, key=lambda d: (d["fecha"], d["titulo"]), reverse=True)
+ destino = os.path.join(config["site_dir"], "datos", "documentos.json")
+ os.makedirs(os.path.dirname(destino), exist_ok=True)
+ with open(destino, "w", encoding="utf-8") as f:
+ json.dump({"total": len(docs), "documentos": docs[:60]}, f, ensure_ascii=False, indent=1)
+ log.info("documentos.json: %d documentos con fecha", len(docs))
diff --git a/mkdocs.yml b/mkdocs.yml
index 99fc7f5..81c5a53 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -56,6 +56,7 @@ extra_javascript:
hooks:
- hooks/iconos.py # íconos del menú lateral (también para páginas que llegan por sync-docs.sh)
- hooks/frontmatter.py # encabezados YAML con «: » sin comillas: se leen igual (no toca los archivos)
+ - hooks/documentos.py # «Documentos recientes» del inicio: site/datos/documentos.json (páginas con date:)
plugins:
- search:
@@ -150,6 +151,7 @@ nav:
- hardware/index.md
- Manuales por tarjeta: manuales/index.md
- Auditorías y pasadas:
+ - 21 oct · Estado v6 (T25 + T28): hardware/v6-estado-21oct.md
- 21 oct · Auditoría pasada 4: hardware/v56-v57-auditoria-21oct.md
- 19 oct · Críticos V5.6: hardware/v56-clasificacion-19oct.md
- 19 oct · Re-auditoría: hardware/re-auditoria-19oct.md
diff --git a/overrides/inicio.html b/overrides/inicio.html
new file mode 100644
index 0000000..5247498
--- /dev/null
+++ b/overrides/inicio.html
@@ -0,0 +1,17 @@
+{#-
+ Página de inicio (docs/index.md con `template: inicio.html`, 21-oct-2026).
+ Extiende la plantilla de los tableros (tablero.css y kanban.js, porque el inicio lleva el
+ tablero de pendientes) y agrega lo suyo: inicio/inicio.css, la ruta a producción
+ (tableros/ruta.js), la actividad (tableros/actividad.js) e inicio/inicio.js, que llena la página
+ con docs/datos/proyecto.json y con el servidor (/api/...).
+-#}
+{% extends "tablero.html" %}
+
+{% block extrahead %}
+ {{ super() }}
+
+
+
+
+
+{% endblock %}
diff --git a/overrides/tablero.html b/overrides/tablero.html
index 71b5aa9..445071d 100644
--- a/overrides/tablero.html
+++ b/overrides/tablero.html
@@ -2,7 +2,8 @@
Plantilla de los tableros (Pendientes, Ingeniería, Producción, Actas, Inventario,
Sitios, Equipo, Expediente y Visitas de campo). Se elige con `template: tablero.html`
en el front matter de la página.
- - Usa todo el ancho (sin la columna de texto de los documentos).
+ - Usa todo el ancho (sin la columna de texto de los documentos). Una página puede pedir
+ otro ancho en su front matter, p. ej. `ancho: 52rem` (la Bitácora) o `ancho: 64rem` (inicio).
- Carga tableros/tablero.css (estilos del tablero, sobre los tokens de helgen.css)
y tableros/kanban.js (el tablero de pendientes) antes del contenido, para que
el script de cada página pueda llamar a Kanban.montar(...).
@@ -12,6 +13,6 @@
{% block extrahead %}
{{ super() }}
-
+
{% endblock %}
diff --git a/scripts/sync-docs.sh b/scripts/sync-docs.sh
index 55150b4..a437a35 100755
--- a/scripts/sync-docs.sh
+++ b/scripts/sync-docs.sh
@@ -130,6 +130,10 @@ declare -a MAP=(
"03-Architecture-Hardware/2026-10-21_Drogheda_Auditoria-Pasada-4_V5.6-V5.7_ES_v1.0.md|docs/hardware/v56-v57-auditoria-21oct.md"
"03-Architecture-Hardware/img/2026-10-21_Drogheda_Auditoria-Pasada-4-V56-V57_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_Auditoria-Pasada-4-V56-V57_v1.0.png"
"03-Architecture-Hardware/img/2026-10-21_Drogheda_T33-Arreglos-y-Pendientes_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_T33-Arreglos-y-Pendientes_v1.0.png"
+ # 2026-10-21 (noche): estado de la v6 con T25 + T28 aplicados y revisión de la pasada 5 de V5.6/V5.7
+ "03-Architecture-Hardware/2026-10-21_Drogheda_Estado-v6-T25-T28_ES_v1.0.md|docs/hardware/v6-estado-21oct.md"
+ "03-Architecture-Hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png"
+ "03-Architecture-Hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png"
)
n=0
diff --git a/scripts/tarjetas/README.md b/scripts/tarjetas/README.md
new file mode 100644
index 0000000..0e756d2
--- /dev/null
+++ b/scripts/tarjetas/README.md
@@ -0,0 +1,22 @@
+# Imágenes de las tarjetas (portada y ruta a producción)
+
+Las imágenes de `docs/img/tarjetas/` (la franja de la portada del inicio y una miniatura por versión) se
+dibujan con el cobre real de cada `.kicad_pcb`, en blanco sobre el fondo del sitio. Lo nuevo de cada
+revisión (las redes que no estaban en la versión anterior) va en blanco sólido; el resto, tenue.
+Decisión de Leo del 21-oct-2026: «la tarjeta en blanco, no en verde».
+
+Las del 21-oct salieron del cobre del 20-oct. Para rehacerlas (en la fase 3 esto lo hará `sync-docs.sh`):
+
+```bash
+cd hbox-docs/scripts/tarjetas
+K=../../../03-Architecture-Hardware/kicad
+python3 leer_pcb.py $K/hbox-watchdog-v5.kicad_pcb /tmp/tarjetas/pcb_v56.json
+python3 leer_pcb.py $K/hbox-watchdog-v57.kicad_pcb /tmp/tarjetas/pcb_v57.json
+python3 leer_pcb.py $K/hbox-watchdog-v6.kicad_pcb /tmp/tarjetas/pcb_v6.json
+# la V5.5 sale del commit con el que se pidió (22-ago): git show :hbox-watchdog-v5.kicad_pcb > v55.kicad_pcb
+python3 leer_pcb.py v55.kicad_pcb /tmp/tarjetas/pcb_v55.json
+python3 tarjetas_blanco.py /tmp/tarjetas /tmp/tarjetas/svg
+python3 rasterizar.py ../../docs/img/tarjetas /tmp/tarjetas/svg/*.svg # necesita Pillow y Playwright (Chromium)
+```
+
+Solo con KiCad cerrado y sobre archivos ya subidos (no a media pasada de layout).
diff --git a/scripts/tarjetas/leer_pcb.py b/scripts/tarjetas/leer_pcb.py
new file mode 100644
index 0000000..b08536c
--- /dev/null
+++ b/scripts/tarjetas/leer_pcb.py
@@ -0,0 +1,63 @@
+"""Lee un .kicad_pcb (KiCad 8/9/10) sin KiCad y guarda en JSON lo que hace falta para dibujarlo:
+pistas, arcos, vías, pads (con su red y su capa), contorno y zonas rellenadas.
+Uso: python3 leer_pcb.py tarjeta.kicad_pcb pcb_v56.json"""
+import re, json, math, sys
+src = open(sys.argv[1], encoding='utf-8').read()
+tok_re = re.compile(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()"]+')
+def parse(s):
+ stack=[[]]
+ for m in tok_re.finditer(s):
+ t=m.group(0)
+ if t=='(':
+ stack.append([])
+ elif t==')':
+ x=stack.pop(); stack[-1].append(x)
+ else:
+ if t.startswith('"'): t=t[1:-1]
+ stack[-1].append(t)
+ return stack[0][0]
+root=parse(src)
+def find(node, key):
+ return [c for c in node if isinstance(c,list) and c and c[0]==key]
+def one(node,key):
+ r=find(node,key); return r[0] if r else None
+out={'segments':[], 'vias':[], 'pads':[], 'edges':[], 'zones':[], 'arcs':[]}
+for seg in find(root,'segment'):
+ s=one(seg,'start'); e=one(seg,'end'); w=one(seg,'width'); l=one(seg,'layer'); n=one(seg,'net')
+ out['segments'].append([float(s[1]),float(s[2]),float(e[1]),float(e[2]),float(w[1]),l[1],n[1] if n else ''])
+for a in find(root,'arc'):
+ s=one(a,'start'); m=one(a,'mid'); e=one(a,'end'); w=one(a,'width'); l=one(a,'layer'); n=one(a,'net')
+ out['arcs'].append([float(s[1]),float(s[2]),float(m[1]),float(m[2]),float(e[1]),float(e[2]),float(w[1]),l[1],n[1] if n else ''])
+for v in find(root,'via'):
+ at=one(v,'at'); sz=one(v,'size'); n=one(v,'net')
+ out['vias'].append([float(at[1]),float(at[2]),float(sz[1]),n[1] if n else ''])
+for fp in find(root,'footprint'):
+ at=one(fp,'at'); fx,fy=float(at[1]),float(at[2]); frot=float(at[3]) if len(at)>3 else 0.0
+ layer=one(fp,'layer')[1]
+ ref=''
+ for p in find(fp,'property'):
+ if len(p)>2 and p[1]=='Reference': ref=p[2]
+ for pad in find(fp,'pad'):
+ pat=one(pad,'at'); px,py=float(pat[1]),float(pat[2]); prot=float(pat[3]) if len(pat)>3 else 0.0
+ size=one(pad,'size'); w,h=float(size[1]),float(size[2])
+ shape=pad[3] if len(pad)>3 else 'rect'
+ typ=pad[2] if len(pad)>2 else 'smd'
+ n=one(pad,'net'); net=n[1] if n and len(n)>1 else ''
+ # KiCad: pad position rotated by footprint rotation (clockwise in screen coords since y down)
+ r=math.radians(-frot)
+ ax=fx+px*math.cos(r)-py*math.sin(r); ay=fy+px*math.sin(r)+py*math.cos(r)
+ lay=one(pad,'layers'); lays=lay[1:] if lay else []
+ out['pads'].append([round(ax,3),round(ay,3),w,h,prot,shape,typ,net,ref,layer,lays])
+for g in root:
+ if isinstance(g,list) and g and g[0] in ('gr_line','gr_rect','gr_arc','gr_circle','gr_poly'):
+ l=one(g,'layer')
+ if l and l[1]=='Edge.Cuts':
+ out['edges'].append(g)
+for z in find(root,'zone'):
+ n=one(z,'net'); net=n[1] if n and len(n)>1 else ''
+ for fpoly in find(z,'filled_polygon'):
+ l=one(fpoly,'layer'); pts=one(fpoly,'pts')
+ xy=[(float(p[1]),float(p[2])) for p in pts[1:] if isinstance(p,list) and p[0]=='xy']
+ out['zones'].append([l[1] if l else '', net, xy])
+json.dump(out, open(sys.argv[2] if len(sys.argv) > 2 else 'pcb.json', 'w'))
+print({k: len(v) for k, v in out.items()})
diff --git a/scripts/tarjetas/rasterizar.py b/scripts/tarjetas/rasterizar.py
new file mode 100644
index 0000000..9adad5b
--- /dev/null
+++ b/scripts/tarjetas/rasterizar.py
@@ -0,0 +1,26 @@
+"""SVG → WebP con Chromium (Playwright) y Pillow, para que el texto y los trazos salgan iguales que en el navegador.
+Uso: python3 rasterizar.py SALIDA_DIR a.svg [b.svg …]"""
+import asyncio, io, os, re, sys
+from PIL import Image
+from playwright.async_api import async_playwright
+
+OUT = sys.argv[1]
+
+
+async def main():
+ async with async_playwright() as p:
+ b = await p.chromium.launch()
+ for f in sys.argv[2:]:
+ txt = open(f).read()
+ w, h = map(int, re.search(r'width="(\d+)" height="(\d+)"', txt).groups())
+ pg = await b.new_page(viewport={'width': w, 'height': h})
+ await pg.set_content('' + txt + '')
+ png = await pg.screenshot(clip={'x': 0, 'y': 0, 'width': w, 'height': h})
+ await pg.close()
+ im = Image.open(io.BytesIO(png)).convert('RGB')
+ dst = os.path.join(OUT, os.path.basename(f)[:-4] + '.webp')
+ im.save(dst, 'WEBP', quality=84, method=6)
+ print(dst, im.size, os.path.getsize(dst), 'bytes')
+ await b.close()
+
+asyncio.run(main())
diff --git a/scripts/tarjetas/tarjetas_blanco.py b/scripts/tarjetas/tarjetas_blanco.py
new file mode 100644
index 0000000..796ce67
--- /dev/null
+++ b/scripts/tarjetas/tarjetas_blanco.py
@@ -0,0 +1,131 @@
+"""Cobre de las EinarBoard en blanco sobre noche (21-oct): portada (V5.6) y miniaturas por versión.
+Lo nuevo de cada revisión va en blanco sólido; el resto del cobre en blanco tenue.
+Uso: python3 tarjetas_blanco.py DIR_JSON DIR_SALIDA (pcb_v55/v56/v57/v6.json, de leer_pcb.py)
+Genera SVG; la conversión a WebP la hace rasterizar.py."""
+import json, os, re, sys
+
+SRC, OUT = sys.argv[1], sys.argv[2]
+D = {v: json.load(open(os.path.join(SRC, f'pcb_{v}.json'))) for v in ('v55', 'v56', 'v57', 'v6')}
+
+
+def nets(d):
+ return {p[7] for p in d['pads'] if p[7]} | {s[6] for s in d['segments'] if s[6]}
+
+
+def ok(n):
+ return n and n != 'GND' and not n.startswith('unconnected')
+
+
+NUEVO = {'v55': set(),
+ 'v56': {n for n in nets(D['v56']) - nets(D['v55']) if ok(n)},
+ 'v57': {n for n in nets(D['v57']) - nets(D['v56']) if ok(n)},
+ 'v6': {n for n in nets(D['v6']) - nets(D['v57'])
+ if ok(n) and re.search(r'GNSS|MODEM|4G|ANT|RF_|NETLIGHT|1V8|F9P|LTE', n)}}
+
+
+def contorno(d):
+ xs, ys = [], []
+ for e in d['edges']:
+ for k in e[1:]:
+ if isinstance(k, list) and k and k[0] in ('start', 'end'):
+ xs.append(float(k[1])); ys.append(float(k[2]))
+ return min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)
+
+
+# opacidades: (zonas, B.Cu, capas internas, F.Cu, pads, vías) y lo nuevo (F.Cu, internas, pads)
+TEMAS = {
+ 'mini': dict(fondo='#111A2C', z=0.035, b=0.12, i=0.08, f=0.28, pad=0.38, via=0.32, nf=1.0, ni=0.55, npad=0.95, min_w=0.55),
+ 'portada': dict(fondo='#0E1524', z=0.03, b=0.10, i=0.07, f=0.22, pad=0.30, via=0.26, nf=0.95, ni=0.5, npad=0.9, min_w=0.8),
+}
+
+
+def svg(d, hl, crop, size, tema):
+ T = TEMAS[tema]
+ x0, y0, W, H = crop
+ OW, OH = size
+ s = max(OW / W, OH / H) # llena el recuadro (la portada recorta arriba y abajo)
+ ox, oy = (OW - W * s) / 2, (OH - H * s) / 2
+ X = lambda x: ox + (x - x0) * s
+ Y = lambda y: oy + (y - y0) * s
+ o = [f'')
+ return '\n'.join(o)
+
+
+os.makedirs(OUT, exist_ok=True)
+for v, d in D.items():
+ bx, by, bw, bh = contorno(d)
+ m = 2.0
+ open(os.path.join(OUT, f'{v}.svg'), 'w').write(svg(d, NUEVO[v], (bx - m, by - m, bw + 2 * m, bh + 2 * m), (420, 300), 'mini'))
+ print(v, 'nuevo:', len(NUEVO[v]), 'redes; contorno', round(bw, 1), 'x', round(bh, 1), 'mm')
+
+# portada: franja de la V5.6 donde más cobre nuevo hay (ventana de 20 mm de alto)
+d = D['v56']; hl = NUEVO['v56']
+bx, by, bw, bh = contorno(d)
+W = bw - 16; H = W * 400 / 2400
+mejor = max((sum(abs(s[2] - s[0]) + abs(s[3] - s[1]) for s in d['segments'] if s[6] in hl and y <= (s[1] + s[3]) / 2 <= y + H), y)
+ for y in [by + 1 + k * 0.5 for k in range(int((bh - H - 2) / 0.5))])
+y0 = mejor[1]
+print('portada: y0 =', round(y0, 1), 'mm; mm de cobre nuevo en la franja:', round(mejor[0], 1))
+open(os.path.join(OUT, 'portada-v56.svg'), 'w').write(svg(d, hl, (bx + 8, y0, W, H), (2400, 400), 'portada'))