Sitio: página de inicio con enfoque de la semana, ruta a producción y actividad (fase 2a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
This commit is contained in:
Leopoldo Rodríguez 2026-10-21 19:21:47 -06:00
parent 4f89669e5a
commit 5f2f7a1f46
38 changed files with 2011 additions and 124 deletions

View File

@ -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

View File

@ -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

View File

@ -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")))

View File

@ -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
---
<div class="tb tb-bitacora" markdown>
# 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 }
<div id="bit-actividad"><p class="vacio">…</p></div>
## 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).
</div>
<script src="/tableros/actividad.js"></script>
<script>
Actividad.montar(document.getElementById('bit-actividad'), { dias: 14, limite: 80, porDia: true });
</script>

74
docs/datos/proyecto.json Normal file
View File

@ -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 }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

View File

@ -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 |
|---|---|---|---|---|
| <span class="badge-v55">V5.5</span> | 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 |
| <span class="badge-v56">V5.6</span> | 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 |
| <span class="badge-v57">V5.7</span> | 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) |
| <span class="badge-v6">v6</span> | 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 |
| <span class="badge-v56">V5.6</span> | 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 |
| <span class="badge-v57">V5.7</span> | 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) |
| <span class="badge-v6">v6</span> | 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 |
| <span class="badge-v4">V4</span> | En prueba y campo. Sin producción nueva. | — | `hbox-watchdog-v4` | `hbox-tarjetaV4_0.6.0` CAMPO |
| <span class="badge-v3">V3</span> | 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.

View File

@ -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
![Pasada 5 de Michelle: lo que quedó y lo que falta](img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png)
**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.

View File

@ -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"

View File

@ -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.
![v6 al 21-oct: la fuente U25 y los barrenos](img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png)
## 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.

View File

@ -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"

6
docs/img/mexico.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
docs/img/tarjetas/v55.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/img/tarjetas/v56.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/img/tarjetas/v57.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/img/tarjetas/v6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -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
<!--
Página de inicio (21-oct-2026). Casi todo se llena solo:
- Fase, ruta a producción, fechas y sitios: docs/datos/proyecto.json (lo actualiza Claude o Leo).
- Enfoque de la semana: lo escriben Leo y Adriana con «Editar» (se guarda en el servidor, /api/hoy).
- Pendientes y actividad: del tablero y de las actas (/api/tareas, /api/actividad).
- Documentos recientes: los .md con «date:» en su encabezado (hooks/documentos.py).
Aquí solo se edita el texto fijo: atajos, «Cómo trabajamos» y la tabla «¿Qué necesitas hacer?».
-->
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).
<div class="tb hg-inicio" id="inicio">
<div class="buscar-aviso" markdown>
La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento.
<div class="in-portada">
<img src="img/tarjetas/portada-v56.webp" alt="Cobre de la EinarBoard V5.6; lo nuevo de la revisión, en blanco" width="2400" height="400" fetchpriority="high">
</div>
## ¿Qué necesitas hacer?
<header class="in-cab">
<img class="in-logo" src="img/helgen-mascara.svg" alt="" width="200" height="215">
<h1>Drogheda</h1>
<p class="in-desc">Rastreadores HBOX RTK para el equipo de patio de Hutchison Ports. Aquí se diseña, se produce, se prueba y se instala.</p>
</header>
<div class="in-arriba">
<dl class="in-props">
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 7.5V12l3 2"/></svg>Fase</dt>
<dd id="in-fase"><span class="vacio">…</span></dd>
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9zM4 7.5l8 4.5 8-4.5M12 12v9"/></svg>Pedido</dt>
<dd id="in-pedido"><span class="vacio">…</span></dd>
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h12a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zM8 3v4M16 3v4M4 10h16"/></svg>Próximo hito</dt>
<dd id="in-hito"><span class="vacio">…</span></dd>
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5.3a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4zM3.5 19.5c.6-3.2 2.8-5 5.5-5s4.9 1.8 5.5 5M16.8 7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM15.8 14.6c2.3 0 4.2 1.6 4.7 4.4"/></svg>Equipo</dt>
<dd id="in-equipo"><span class="vacio">…</span></dd>
</dl>
<nav class="in-atajos" aria-label="Atajos">
<a href="/actas/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5h10a2 2 0 0 1 2 2V19a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6.5a2 2 0 0 1 2-2zM9 3.5h6v2.5H9zM8.5 11h7M8.5 15h5"/></svg><span><b>Llenar un acta</b><span>Banco, ensamble o campo</span></span></a>
<a href="/tableros/unidad/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM20 20l-4.4-4.4"/></svg><span><b>Buscar una unidad</b><span>Su expediente, por H00xx o por serie</span></span></a>
<a href="/descargas/#firmware-del-hbox-que-binario-va-en-que-tarjeta"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7.5 10.5 12 15l4.5-4.5M5 20h14"/></svg><span><b>Firmware por tarjeta</b><span>Con su SHA-256</span></span></a>
<a href="/tableros/pendientes/?mios=1"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM5 20c.8-3.8 3.6-6 7-6s6.2 2.2 7 6"/></svg><span><b>Mis pendientes</b><span>Lo que tengo asignado</span></span></a>
</nav>
</div>
<section class="in-enfoque" id="in-enfoque" aria-labelledby="enfoque-h">
<img class="in-enfoque-logo" src="img/helgen-mascara.svg" alt="" width="200" height="215">
<div class="in-enfoque-c">
<div class="in-enfoque-cab"><span class="in-enfoque-cuando">Enfoque de la semana</span></div>
<h2 id="enfoque-h">…</h2>
</div>
</section>
<section class="in-sec" aria-labelledby="ruta-h">
<div class="in-sec-cab">
<h2 id="ruta-h">Ruta a producción</h2>
<span class="in-leyenda">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.</span>
<a class="in-ver" href="/tableros/ingenieria/">Tablero de Ingeniería</a>
</div>
<div id="in-ruta"><p class="vacio">…</p></div>
</section>
<section class="in-sec" aria-labelledby="pend-h">
<div class="in-sec-cab in-pend-cab">
<div class="in-pend-t"><h2 id="pend-h">Pendientes</h2><span class="in-resumen" id="in-resumen"></span></div>
<div class="in-pend-acc" id="in-pend-acc"><a class="boton sec" href="/tableros/pendientes/">Abrir el tablero completo</a></div>
</div>
<div id="in-kb"></div>
</section>
<div class="in-tres">
<section class="in-caja" aria-labelledby="hitos-h">
<div class="in-caja-cab"><h2 id="hitos-h">Próximas fechas</h2></div>
<div id="in-hitos"><p class="vacio">…</p></div>
</section>
<section class="in-caja" aria-labelledby="act-h">
<div class="in-caja-cab"><h2 id="act-h">Actividad</h2><a href="/bitacora/">Bitácora</a></div>
<div id="in-actividad"></div>
</section>
<section class="in-caja" aria-labelledby="docs-h">
<div class="in-caja-cab"><h2 id="docs-h">Documentos recientes</h2><a href="/bitacora/#documentos-por-fecha">Todos</a></div>
<div id="in-docs"><p class="vacio">…</p></div>
</section>
</div>
<div class="in-dos">
<section class="in-caja in-donde" aria-labelledby="mapa-h">
<div class="in-caja-cab"><h2 id="mapa-h">Dónde estamos</h2><a href="/tableros/sitio/">Sitios</a></div>
<div class="in-mapa" id="in-mapa"></div>
<ul class="in-sitios" id="in-sitios"></ul>
</section>
<section class="in-caja" aria-labelledby="como-h">
<div class="in-caja-cab"><h2 id="como-h">Cómo trabajamos</h2><a href="/tableros/ingenieria/#filosofia">Con detalle</a></div>
<p class="in-nota">Antes de proponer un cambio, revisa que no choque con ninguna de estas reglas.</p>
<ul class="in-reglas">
<li><b>Primero la seguridad en campo.</b> <span>La unidad vive a más de 45 °C y en batería; todo se prueba en banco antes de llevarlo a campo.</span></li>
<li><b>Una etapa probada vale más que una nueva.</b> <span>Los zócalos se quedan; la fuente de la V5.7 viene del bloque ya auditado de la v6.</span></li>
<li><b>Redundancia solo con causa.</b> <span>Por eso salió P12 y entró el acelerómetro U26.</span></li>
<li><b>Las reglas de fabricación no se negocian.</b> <span>Pasta de U6 al 50 % o menos, nada de potencia por capas internas, lupa en U6 antes de energizar.</span></li>
<li><b>Cambios con prueba y rastro.</b> <span>Scripts verificados contra la netlist, una persona por tarjeta a la vez y no habrá v6.1.</span></li>
</ul>
</section>
</div>
</div>
<div class="tb hg-inicio in-que" markdown>
## ¿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 |
|---|---|---|---|---|
| <span class="badge-v55">V5.5</span> | **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) |
| <span class="badge-v56">V5.6</span> | **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) |
| <span class="badge-v57">V5.7</span> | **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) |
| <span class="badge-v6">v6</span> | **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) |
| <span class="badge-v4">V4</span> | 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) |
| <span class="badge-v3">V3</span> | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) |
| <span class="badge-fw">firmware</span> | 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
</div>

261
docs/inicio/inicio.css Normal file
View File

@ -0,0 +1,261 @@
/* ==========================================================================
Página de inicio (overrides/inicio.html, 21-oct-2026)
Va sobre los tokens de stylesheets/helgen.css. El tablero de pendientes, la ruta a
producción y la actividad traen sus estilos de tableros/tablero.css (todo bajo .tb).
========================================================================== */
/* ---- Portada: el cobre de la V5.6 en blanco ------------------------------ */
.hg-inicio .in-portada {
position: relative;
overflow: hidden;
border-radius: 14px;
border: 1px solid var(--hg-linea);
background: #0E1524; /* la imagen es oscura en los dos modos */
}
.hg-inicio .in-portada img { display: block; width: 100%; height: 168px; object-fit: cover; object-position: center 45%; }
.hg-inicio .in-portada::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(14, 21, 36, 0) 50%, rgba(14, 21, 36, 0.6) 100%);
pointer-events: none;
}
/* ---- Encabezado ------------------------------------------------------------ */
.hg-inicio .in-cab { position: relative; }
.hg-inicio .in-logo {
position: relative;
z-index: 1;
display: block;
width: 80px;
height: auto;
margin: -46px 0 0 24px;
padding: 5px;
border-radius: 50%;
background: var(--hg-fondo);
}
.md-typeset .hg-inicio .in-cab h1 {
margin: 12px 0 6px;
font-family: var(--hg-fuente-titulo);
font-weight: 700;
font-size: 46px;
line-height: 1.02;
letter-spacing: -0.012em;
color: var(--hg-texto-fuerte);
}
.md-typeset .hg-inicio .in-desc { margin: 0; max-width: 46rem; font-size: 16.5px; line-height: 1.5; color: var(--hg-texto-2); }
/* ---- Propiedades del proyecto y atajos --------------------------------------- */
.hg-inicio .in-arriba { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px 36px; margin-top: 22px; }
.md-typeset .hg-inicio dl.in-props {
flex: 1 1 520px;
min-width: 0;
display: grid;
grid-template-columns: 136px minmax(0, 1fr);
gap: 13px 16px;
margin: 0;
}
.md-typeset .hg-inicio .in-props dt {
display: flex; align-items: center; gap: 8px;
margin: 0; font-size: 14px; font-weight: 400; color: var(--hg-texto-3);
}
.md-typeset .hg-inicio .in-props dt .i { width: 16px; height: 16px; stroke-width: 1.8; }
.md-typeset .hg-inicio .in-props dd { margin: 0; min-width: 0; font-size: 14.5px; line-height: 1.45; color: var(--hg-texto); }
.hg-inicio .in-gris { color: var(--hg-texto-2); }
.hg-inicio .in-chip {
display: inline-block;
padding: 2px 9px;
border-radius: 6px;
background: var(--hg-elevado);
color: var(--hg-texto-fuerte);
font-size: 13px;
font-weight: 600;
}
.hg-inicio .in-pedido-l b { font-weight: 700; color: var(--hg-texto-fuerte); }
.hg-inicio .in-barra { display: flex; gap: 3px; height: 8px; max-width: 440px; margin: 8px 0 5px; }
.hg-inicio .in-seg { position: relative; overflow: hidden; border-radius: 3px; background: color-mix(in srgb, currentColor 24%, transparent); }
.hg-inicio .in-seg > span { position: absolute; inset: 0 auto 0 0; background: currentColor; }
.hg-inicio .in-barra-l { display: flex; gap: 3px; max-width: 440px; font-size: 12.5px; }
.hg-inicio .in-barra-l span { min-width: 0; white-space: nowrap; }
.hg-inicio .s-v55 { color: var(--hg-v55); }
.hg-inicio .s-v56 { color: var(--hg-v56); }
.hg-inicio .s-v57 { color: var(--hg-v57); }
.hg-inicio .s-v6 { color: var(--hg-v6); }
.hg-inicio .s-v4 { color: var(--hg-v4); }
.hg-inicio .in-mini { display: inline-block; margin-top: 3px; font-size: 12.5px; }
.hg-inicio a.in-caras { text-decoration: none; vertical-align: middle; margin-right: 6px; }
.hg-inicio a.in-caras .av { width: 26px; height: 26px; font-size: 10px; }
.hg-inicio a.in-caras .av + .av { margin-left: -5px; }
.hg-inicio .in-atajos { flex: 1 1 300px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.hg-inicio .in-atajos a {
display: flex; align-items: flex-start; gap: 11px;
padding: 12px 13px;
border-radius: 12px;
border: 1px solid var(--hg-linea);
background: var(--hg-plano-2);
color: var(--hg-texto);
text-decoration: none;
}
.hg-inicio .in-atajos a:hover { border-color: var(--hg-linea-2); background: var(--hg-plano); }
.hg-inicio .in-atajos .i { width: 20px; height: 20px; margin-top: 1px; stroke-width: 1.8; color: var(--hg-ruta); }
.hg-inicio .in-atajos b { display: block; font-size: 14.5px; font-weight: 600; line-height: 1.25; color: var(--hg-texto-fuerte); }
.hg-inicio .in-atajos span span { display: block; margin-top: 2px; font-size: 12.5px; line-height: 1.3; color: var(--hg-texto-2); }
/* ---- Enfoque de la semana ---------------------------------------------------- */
.hg-inicio .in-enfoque {
display: flex;
gap: 16px;
margin-top: 28px;
padding: 20px 22px;
border-radius: 14px;
border: 1px solid var(--hg-linea-2);
background: var(--hg-plano);
}
.hg-inicio .in-enfoque-logo { flex: none; align-self: flex-start; width: 32px; height: auto; margin-top: 3px; }
.hg-inicio .in-enfoque-c { flex: 1; min-width: 0; }
.hg-inicio .in-enfoque-cab { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; }
.hg-inicio .in-enfoque-cuando { font-size: 13px; color: var(--hg-texto-2); }
.md-typeset .hg-inicio .in-enfoque h2 {
margin: 6px 0 10px;
font-size: 30px;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.01em;
text-wrap: balance;
}
.md-typeset .hg-inicio .in-enfoque ul { margin: 0 0 0 1.15em; }
.md-typeset .hg-inicio .in-enfoque li { margin: 0 0 5px; font-size: 15px; line-height: 1.5; color: var(--hg-texto-suave); }
.md-typeset .hg-inicio .in-enfoque li code { font-size: 0.86em; }
.md-typeset .hg-inicio .in-enfoque-pie { margin: 10px 0 0; font-size: 12.5px; color: var(--hg-texto-3); }
.hg-inicio button.in-editar { padding: 5px 11px; font-size: 13px; }
.hg-inicio .in-enfoque-ed input, .hg-inicio .in-enfoque-ed textarea { width: 100%; }
.hg-inicio .in-enfoque-ed textarea { min-height: 140px; line-height: 1.45; }
.hg-inicio .in-enfoque-botones { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
/* ---- Secciones grandes ----------------------------------------------------------- */
.hg-inicio .in-sec { margin-top: 40px; }
.hg-inicio .in-sec-cab { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; margin-bottom: 12px; }
.md-typeset .hg-inicio .in-sec-cab h2 { margin: 0; font-size: 25px; line-height: 1.1; }
.hg-inicio .in-leyenda { flex: 1 1 320px; font-size: 13.5px; color: var(--hg-texto-2); }
.hg-inicio .in-ver { margin-left: auto; font-size: 14px; }
.hg-inicio .in-pend-cab { align-items: center; }
.hg-inicio .in-pend-t { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.hg-inicio .in-resumen { font-size: 14px; color: var(--hg-texto-2); }
.hg-inicio .in-pend-acc { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-left: auto; }
.hg-inicio .in-pend-acc .boton.sec { padding: 7px 12px; font-size: 13.5px; }
/* ---- Cajas de abajo --------------------------------------------------------------- */
.hg-inicio .in-tres { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 40px; }
.hg-inicio .in-dos { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 16px; margin-top: 16px; }
.hg-inicio .in-caja { min-width: 0; padding: 16px 18px; border-radius: 14px; border: 1px solid var(--hg-linea); background: var(--hg-plano-2); }
.hg-inicio .in-caja-cab { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.md-typeset .hg-inicio .in-caja-cab h2 { margin: 0; font-size: 20px; line-height: 1.15; }
.hg-inicio .in-caja-cab a { font-size: 13.5px; white-space: nowrap; }
.md-typeset .hg-inicio ol.in-fechas,
.md-typeset .hg-inicio ol.in-docs,
.md-typeset .hg-inicio ul.in-sitios,
.md-typeset .hg-inicio ul.in-reglas { list-style: none; margin: 0; padding: 0; }
.md-typeset .hg-inicio .in-fechas li,
.md-typeset .hg-inicio .in-docs li,
.md-typeset .hg-inicio .in-sitios li,
.md-typeset .hg-inicio .in-reglas li { margin: 0; }
.hg-inicio .in-fechas li {
display: grid;
grid-template-columns: 88px minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
padding: 8px 0;
border-top: 1px solid var(--hg-linea-suave);
font-size: 14px;
line-height: 1.35;
}
.hg-inicio .in-fechas li:first-child,
.hg-inicio .in-docs li:first-child,
.hg-inicio .in-sitios li:first-child,
.hg-inicio .in-reglas li:first-child { border-top: 0; }
.hg-inicio .in-f { font-size: 13px; color: var(--hg-texto-2); white-space: nowrap; }
.hg-inicio .in-f.hoy { color: var(--hg-acento); font-weight: 600; }
.hg-inicio .in-f.sin { color: var(--hg-aviso); }
.hg-inicio .in-t { min-width: 0; color: var(--hg-texto); }
.hg-inicio a.in-t { text-decoration: none; }
.hg-inicio a.in-t:hover { color: var(--hg-liga-hover); }
.hg-inicio .es-hito .in-t { font-weight: 600; color: var(--hg-texto-fuerte); }
.hg-inicio .in-docs li { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 8px 0; border-top: 1px solid var(--hg-linea-suave); }
.hg-inicio .in-docs a { display: flex; align-items: flex-start; gap: 9px; min-width: 0; font-size: 14px; line-height: 1.35; color: var(--hg-texto); text-decoration: none; }
.hg-inicio .in-docs a .i { width: 16px; height: 16px; margin-top: 2px; color: var(--hg-texto-3); }
.hg-inicio .in-docs a:hover { color: var(--hg-liga-hover); }
/* ---- Dónde estamos ------------------------------------------------------------------ */
.hg-inicio .in-mapa { position: relative; margin: 6px 0 8px; }
.hg-inicio .in-mapa svg { display: block; width: 100%; height: auto; }
.hg-inicio .in-mapa.sin-mapa { min-height: 180px; border-radius: 10px; background: var(--hg-plano); }
[data-md-color-scheme="default"] .hg-inicio .in-mapa path[fill="#141E33"] { fill: #E6EAF1; stroke: #D3D9E4; }
[data-md-color-scheme="default"] .hg-inicio .in-mapa path[fill="#1B2742"] { fill: #D5DCE8; }
[data-md-color-scheme="default"] .hg-inicio .in-mapa path[stroke="#3A507C"] { stroke: #9AA7BE; }
.hg-inicio .in-pin { position: absolute; display: flex; align-items: center; transform: translate(-50%, -50%); }
.hg-inicio .in-pin-p {
width: 11px; height: 11px;
border-radius: 50%;
background: var(--hg-ruta);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--hg-ruta) 22%, transparent);
}
.hg-inicio .in-pin.oficina .in-pin-p { box-sizing: border-box; width: 12px; height: 12px; border: 2.5px solid var(--hg-ruta); background: var(--hg-plano-2); }
.hg-inicio .in-pin-t {
position: absolute;
left: 15px;
white-space: nowrap;
font-size: 12.5px;
font-weight: 600;
color: var(--hg-texto);
text-shadow: 0 1px 2px var(--hg-plano-2), 0 0 4px var(--hg-plano-2);
}
.hg-inicio .in-pin.et-izq .in-pin-t { left: auto; right: 15px; }
.hg-inicio .in-pin.et-arriba .in-pin-t { left: 50%; bottom: 13px; transform: translateX(-50%); }
.hg-inicio .in-sitios li { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; padding: 7px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 13.5px; }
.hg-inicio .in-sitios a { color: var(--hg-texto); text-decoration: none; }
.hg-inicio .in-sitios a:hover { color: var(--hg-liga-hover); }
.hg-inicio .in-sitios b { margin-right: 3px; font-weight: 600; }
/* ---- Cómo trabajamos ------------------------------------------------------------------- */
.md-typeset .hg-inicio .in-nota { margin: 0 0 6px; font-size: 13.5px; color: var(--hg-texto-2); }
.hg-inicio .in-reglas li { padding: 9px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 14px; line-height: 1.45; }
.hg-inicio .in-reglas b { display: block; font-weight: 600; color: var(--hg-texto-fuerte); }
.hg-inicio .in-reglas span { color: var(--hg-texto-2); }
/* ---- ¿Qué necesitas hacer? (Markdown al final de index.md) ------------------------------ */
.hg-inicio.in-que { margin-top: 40px; }
.md-typeset .hg-inicio.in-que h2 { margin: 0 0 12px; font-size: 25px; line-height: 1.1; }
.md-typeset .hg-inicio.in-que > p { margin-top: 14px; font-size: 14px; color: var(--hg-texto-2); }
/* ---- Tamaños ------------------------------------------------------------------------------ */
@media (max-width: 1100px) {
.hg-inicio .in-tres { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.hg-inicio .in-tres > :last-child { grid-column: 1 / -1; }
}
@media (max-width: 760px) {
.hg-inicio .in-tres, .hg-inicio .in-dos { grid-template-columns: minmax(0, 1fr); }
.hg-inicio .in-tres > :last-child { grid-column: auto; }
.hg-inicio .in-pend-acc { margin-left: 0; }
}
@media (max-width: 600px) {
.hg-inicio .in-portada img { height: 104px; }
.hg-inicio .in-logo { width: 60px; margin: -34px 0 0 14px; padding: 4px; }
.md-typeset .hg-inicio .in-cab h1 { font-size: 36px; }
.md-typeset .hg-inicio .in-desc { font-size: 15.5px; }
.md-typeset .hg-inicio dl.in-props { grid-template-columns: minmax(0, 1fr); gap: 3px; }
.md-typeset .hg-inicio .in-props dt { margin-top: 10px; }
.md-typeset .hg-inicio .in-props dt:first-child { margin-top: 0; }
.hg-inicio .in-enfoque { gap: 12px; padding: 16px; }
.hg-inicio .in-enfoque-logo { display: none; }
.md-typeset .hg-inicio .in-enfoque h2 { font-size: 24px; }
.hg-inicio .in-fechas li { grid-template-columns: 74px minmax(0, 1fr) auto; }
.hg-inicio .in-ver { margin-left: 0; }
}
@media print {
.hg-inicio .in-atajos, .hg-inicio .in-pend-acc, .hg-inicio .in-editar { display: none !important; }
}

282
docs/inicio/inicio.js Normal file
View File

@ -0,0 +1,282 @@
/* Página de inicio de Drogheda (21-oct-2026).
Llena docs/index.md con:
- docs/datos/proyecto.json: fase, ruta a producción, próximas fechas, sitios (lo actualiza Claude o Leo);
- /api/hoy: el enfoque de la semana (lo escriben Leo y Adriana con «Editar»);
- /api/tareas (tablero), /api/actividad, /api/plan + /api/actas (unidades ensambladas),
/api/equipo y /api/sitio/unidades;
- /datos/documentos.json: los documentos con fecha (lo genera hooks/documentos.py al construir).
Si algo del servidor no responde, esa parte lo dice y el resto de la página sigue. */
(function () {
'use strict';
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
const DIAS = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const AREAS = ['direccion', 'operaciones', 'layout', 'pruebas', 'fabricacion', 'campo', 'ingenieria', 'software', 'produccion', 'asistente'];
const BADGE = { 'V5.5': 'v55', 'V5.6': 'v56', 'V5.7': 'v57', 'v6': 'v6' };
const PLAN_RESPALDO = { pedido_total: 570, versiones: [
{ version: 'V5.6', objetivo: 113, serie_prefijo: 'V56' }, { version: 'V5.7', objetivo: 327, serie_prefijo: 'V57' }, { version: 'v6', objetivo: 130, serie_prefijo: 'V6' }] };
const LAPIZ = 'M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16zM13.5 6.5l4 4';
const DOC = 'M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5zM14 3.5v5h5M9 13h6M9 16.5h4';
const $ = id => document.getElementById(id);
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(typeof c === 'string' ? document.createTextNode(c) : c));
return e;
};
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
const svg = (d, cls) => '<svg class="' + (cls || 'i') + '" viewBox="0 0 24 24" aria-hidden="true"><path d="' + d + '"/></svg>';
function hoyISO(delta) { const d = new Date(Date.now() + (delta || 0) * 86400000); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); }
const fechaCorta = f => { const m = String(f || '').match(/^(\d{4})-(\d{2})-(\d{2})/); return m ? (+m[3]) + ' ' + MES[+m[2] - 1] : ''; };
const plural = (n, uno, varios) => n + ' ' + (n === 1 ? uno : varios);
const esPrueba = s => String(s || '').toUpperCase().includes('PRUEBA');
async function json(url, opts) {
const r = await fetch(url, Object.assign({ cache: 'no-store', credentials: 'same-origin' }, opts || {}));
let j = null; try { j = await r.json(); } catch (e) { /* sin JSON */ }
if (!r.ok || !j || j.ok === false) throw new Error((j && j.error) || ('HTTP ' + r.status));
return j;
}
function falla(cont, que, err) {
cont.innerHTML = '';
cont.append(el('p', { class: 'vacio', text: 'No se pudo cargar ' + que + (err && err.message ? ' (' + err.message + ')' : '') + '.' }));
}
// texto del enfoque: **negritas**, `código` y [ligas](/ruta/ o https://…); lo demás se escapa
function enLinea(s) {
return esc(s)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^)\s]*)\)/g, '<a href="$2">$1</a>');
}
const equipo = () => (window.Kanban && window.Kanban.equipo) || {};
const nombreCorto = id => (window.Kanban && window.Kanban.nombreCorto) ? window.Kanban.nombreCorto(id) : ((equipo()[id] || {}).nombre || id || '').split(' ')[0];
const lista = xs => xs.length <= 1 ? (xs[0] || '') : xs.slice(0, -1).join(', ') + ' y ' + xs[xs.length - 1];
const avatares = ids => { const g = el('span', { class: 'avs' }); (ids || []).forEach(id => g.append(window.Kanban.avatar(id))); return g; };
// ---------------------------------------------------------------- arriba: fase, pedido, hito, equipo
function pintarFase(P) {
const d = $('in-fase'); d.innerHTML = '';
d.append(el('span', { class: 'in-chip', text: P.fase.nombre }), ' ', el('span', { class: 'in-gris', text: P.fase.texto }));
}
function pintarPedido(P, plan, actas) {
const d = $('in-pedido'); d.innerHTML = '';
const vs = (plan && plan.versiones && plan.versiones.length) ? plan.versiones : PLAN_RESPALDO.versiones;
const total = (plan && plan.pedido_total) || vs.reduce((n, v) => n + (v.objetivo || 0), 0);
const ens = (actas || []).filter(a => a.tipo === 'ensamble' && a.resultado !== 'FALLA' && !esPrueba(a.serie));
const hechas = v => { const pref = String(v.serie_prefijo || '').toUpperCase(); return pref ? ens.filter(a => String((a.componentes || {}).einar || '').toUpperCase().startsWith(pref + '-')).length : 0; };
const cuenta = vs.reduce((n, v) => n + hechas(v), 0);
const linea = el('div', { class: 'in-pedido-l' });
linea.append(el('b', { text: total + ' HBOX' }), ' ',
el('span', { class: 'in-gris', text: (actas ? plural(cuenta, 'ensamblada', 'ensambladas') + '; ' : '') + (P.pedido_nota || '') }));
const barra = el('div', { class: 'in-barra', role: 'img', 'aria-label': vs.map(v => v.objetivo + ' ' + v.version).join(', ') });
const leyenda = el('div', { class: 'in-barra-l' });
vs.forEach(v => {
const cls = BADGE[v.version] || 'v4';
const seg = el('span', { class: 'in-seg s-' + cls, style: 'flex:' + (v.objetivo || 1) });
const pct = v.objetivo ? Math.min(100, Math.round(100 * hechas(v) / v.objetivo)) : 0;
seg.append(el('span', { style: 'width:' + pct + '%' }));
barra.append(seg);
leyenda.append(el('span', { class: 's-' + cls, style: 'flex:' + (v.objetivo || 1), text: v.objetivo + ' ' + v.version }));
});
d.append(linea, barra, leyenda);
d.append(el('a', { class: 'in-mini', href: '/tableros/produccion/', text: 'Avance contra el plan' }));
}
function hitosVigentes(P) {
const hoy = hoyISO();
return (P.hitos || []).filter(h => !h.fecha || (h.fin || h.fecha) >= hoy)
.sort((a, b) => ((a.fecha ? 1 : 0) - (b.fecha ? 1 : 0)) || String(a.fecha).localeCompare(String(b.fecha)));
}
function pintarHito(P) {
const d = $('in-hito'); d.innerHTML = '';
const h = hitosVigentes(P)[0];
if (!h) { d.append(el('span', { class: 'vacio', text: 'Sin hitos por delante.' })); return; }
d.append(el('span', { text: h.texto }), ' ', el('span', { class: 'in-gris', text: h.cuando }));
}
function pintarEquipo(P) {
const d = $('in-equipo'); d.innerHTML = '';
const activos = Object.entries(equipo()).filter(([, m]) => m.activo !== false && m.nombre)
.sort((a, b) => ((AREAS.indexOf(a[1].area) + 1 || 99) - (AREAS.indexOf(b[1].area) + 1 || 99)) || a[1].nombre.localeCompare(b[1].nombre));
const caras = el('a', { class: 'avs in-caras', href: '/tableros/equipo/', title: 'Equipo: ' + activos.map(([, m]) => m.nombre).join(', ') });
activos.slice(0, 6).forEach(([id]) => caras.append(window.Kanban.avatar(id)));
if (activos.length > 6) caras.append(el('span', { class: 'av libre', text: '+' + (activos.length - 6) }));
d.append(caras);
if (P.gestion && P.gestion.texto) d.append(' ', el('span', { class: 'in-gris', text: P.gestion.texto }));
}
// ---------------------------------------------------------------- enfoque de la semana
const ORDEN_EDITORES = ['leo', 'adriana'];
function fechaLarga(iso) {
const f = String(iso || '').slice(0, 10); if (!f) return '';
const d = new Date(f + 'T12:00:00');
return DIAS[d.getDay()] + ' ' + d.getDate() + ' de ' + MESES[d.getMonth()];
}
function cuandoEditado(iso) {
const f = String(iso || '').slice(0, 10), h = String(iso || '').slice(11, 16);
if (f === hoyISO()) return 'hoy a las ' + h;
if (f === hoyISO(-1)) return 'ayer a las ' + h;
return 'el ' + fechaLarga(iso) + (h ? ' a las ' + h : '');
}
function pintarEnfoque(P, r) {
const c = document.querySelector('#in-enfoque .in-enfoque-c'); c.innerHTML = '';
const h = r && r.enfoque, e = h || P.enfoque_inicial || { titulo: 'Sin enfoque escrito todavía.', puntos: [] };
const editores = ((r && r.editores) || ORDEN_EDITORES).slice().sort((a, b) => ((ORDEN_EDITORES.indexOf(a) + 1 || 9) - (ORDEN_EDITORES.indexOf(b) + 1 || 9)));
const cab = el('div', { class: 'in-enfoque-cab' });
cab.append(el('span', { class: 'in-enfoque-cuando', text: 'Enfoque de la semana' + (h ? '. Escrito el ' + fechaLarga(h.editado_en) : '') }));
if (r && r.puede_editar) {
const b = el('button', { type: 'button', class: 'sec in-editar', html: svg(LAPIZ) + 'Editar' });
b.addEventListener('click', () => editarEnfoque(P, r, e));
cab.append(b);
}
c.append(cab, el('h2', { id: 'enfoque-h', text: e.titulo }));
if ((e.puntos || []).length) { const ul = el('ul'); e.puntos.forEach(p => ul.append(el('li', { html: enLinea(p) }))); c.append(ul); }
const pie = el('p', { class: 'in-enfoque-pie' });
if (h) pie.textContent = 'Lo escriben ' + lista(editores.map(nombreCorto)) + '. Editado ' + cuandoEditado(h.editado_en) + ' por ' + nombreCorto(h.editado_por) + '.';
else if (P.enfoque_inicial && P.enfoque_inicial.nota) pie.innerHTML = enLinea(P.enfoque_inicial.nota);
if (!r) pie.append(el('span', { class: 'in-gris', text: ' (El servidor no respondió: este es el texto inicial.)' }));
c.append(pie);
}
function editarEnfoque(P, r, e) {
const c = document.querySelector('#in-enfoque .in-enfoque-c'); c.innerHTML = '';
const f = el('form', { class: 'in-enfoque-ed' });
const tit = el('input', { type: 'text', maxlength: '160', id: 'enf-tit', required: '' }); tit.value = e.titulo || '';
const pts = el('textarea', { rows: '6', id: 'enf-pts' }); pts.value = (e.puntos || []).join('\n');
const msg = el('div', { class: 'msg', role: 'status' });
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' });
const guardar = el('button', { type: 'submit', text: 'Guardar' });
f.append(el('div', { class: 'in-enfoque-cab' }, el('span', { class: 'in-enfoque-cuando', text: 'Editar el enfoque de la semana' })),
el('label', { for: 'enf-tit', text: 'Frase de la semana (una línea)' }), tit,
el('label', { for: 'enf-pts', text: 'Puntos: uno por línea, hasta 8' }), pts,
el('p', { class: 'ayuda', html: 'Se puede usar <code>**negritas**</code>, <code>`código`</code> y <code>[ligas](/hardware/…/)</code>. Se guarda con tu nombre y la hora; lo anterior queda en el historial del servidor.' }),
msg, el('div', { class: 'in-enfoque-botones' }, cancelar, guardar));
c.append(f);
tit.focus();
cancelar.addEventListener('click', () => pintarEnfoque(P, r));
f.addEventListener('submit', async ev => {
ev.preventDefault();
const d = { titulo: tit.value.trim(), puntos: pts.value.split('\n').map(x => x.trim()).filter(Boolean) };
if (!d.titulo) { msg.className = 'msg err'; msg.textContent = 'Escribe la frase de la semana.'; return; }
if (d.puntos.length > 8) { msg.className = 'msg err'; msg.textContent = 'Son ' + d.puntos.length + ' puntos; deja 8 o menos.'; return; }
guardar.disabled = true;
try {
const j = await json('/api/hoy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
r = Object.assign({}, r, { enfoque: j.enfoque });
pintarEnfoque(P, r);
} catch (err) {
msg.className = 'msg err'; msg.textContent = 'No se guardó: ' + err.message;
guardar.disabled = false;
}
});
}
// ---------------------------------------------------------------- próximas fechas
function pintarFechas(P, tareas) {
const cont = $('in-hitos'); cont.innerHTML = '';
const hoy = hoyISO(), tope = hoyISO(10);
const filas = hitosVigentes(P).map(h => ({ fecha: h.fecha, cuando: h.cuando, texto: h.texto, quien: h.quien || [], hito: true }));
(tareas || []).forEach(t => {
if (t.estado === 'hecho' || !t.fecha_limite || t.fecha_limite < hoy || t.fecha_limite > tope) return;
filas.push({ fecha: t.fecha_limite, cuando: t.fecha_limite === hoy ? 'Hoy' : fechaCorta(t.fecha_limite), texto: t.titulo, quien: window.Kanban.idsResp(t), id: t.id });
});
filas.sort((a, b) => ((a.fecha ? 1 : 0) - (b.fecha ? 1 : 0)) || String(a.fecha).localeCompare(String(b.fecha)));
if (!filas.length) { cont.append(el('p', { class: 'vacio', text: 'Nada con fecha en los próximos 10 días.' })); return; }
const ol = el('ol', { class: 'in-fechas' });
filas.slice(0, 7).forEach(x => {
const li = el('li', { class: x.hito ? 'es-hito' : 'es-pendiente' });
li.append(el('span', { class: 'in-f' + (x.cuando === 'Hoy' ? ' hoy' : '') + (x.fecha ? '' : ' sin'), text: x.cuando }));
li.append(x.id ? el('a', { class: 'in-t', href: '/tableros/pendientes/?id=' + encodeURIComponent(x.id), text: x.texto }) : el('span', { class: 'in-t', text: x.texto }));
li.append(avatares(x.quien));
ol.append(li);
});
cont.append(ol);
}
// ---------------------------------------------------------------- documentos recientes
async function pintarDocs() {
const cont = $('in-docs');
try {
const j = await json('/datos/documentos.json');
cont.innerHTML = '';
const docs = (j.documentos || []).slice(0, 6);
if (!docs.length) { cont.append(el('p', { class: 'vacio', text: 'Sin documentos con fecha.' })); return; }
const ol = el('ol', { class: 'in-docs' });
docs.forEach(d => ol.append(el('li', null, el('a', { href: d.url, html: svg(DOC) + '<span>' + esc(d.titulo) + '</span>' }), el('span', { class: 'in-f', text: fechaCorta(d.fecha) }))));
cont.append(ol);
} catch (err) { falla(cont, 'la lista de documentos', err); }
}
// ---------------------------------------------------------------- dónde estamos
async function pintarMapa(P) {
const cont = $('in-mapa'), ul = $('in-sitios');
try {
const r = await fetch('/img/mexico.svg', { cache: 'force-cache' });
if (!r.ok) throw new Error('HTTP ' + r.status);
const doc = new DOMParser().parseFromString(await r.text(), 'image/svg+xml');
const mapa = doc.documentElement;
if (mapa.nodeName.toLowerCase() !== 'svg') throw new Error('mapa inválido');
mapa.removeAttribute('width'); mapa.removeAttribute('height');
mapa.setAttribute('role', 'img'); mapa.setAttribute('aria-label', 'Mapa de México con los puertos y la oficina de Querétaro');
cont.innerHTML = '';
cont.append(document.importNode(mapa, true));
} catch (err) { cont.classList.add('sin-mapa'); }
const pin = (s, oficina) => {
const p = el('span', { class: 'in-pin' + (oficina ? ' oficina' : '') + ' et-' + (s.etiqueta || 'der'), style: 'left:' + s.x + '%;top:' + s.y + '%' });
p.append(el('span', { class: 'in-pin-p', 'aria-hidden': 'true' }), el('span', { class: 'in-pin-t', text: s.nombre }));
return p;
};
(P.sitios || []).forEach(s => cont.append(pin(s)));
if (P.oficina) cont.append(pin(Object.assign({ etiqueta: 'arriba' }, P.oficina), true));
ul.innerHTML = '';
(P.sitios || []).forEach(s => {
const estado = el('span', { class: 'in-gris', text: '…' });
ul.append(el('li', null, el('a', { href: '/tableros/sitio/?id=' + encodeURIComponent(s.id) }, el('b', { text: s.nombre }), ' ' + s.ciudad), estado));
json('/api/sitio/unidades?id=' + encodeURIComponent(s.id)).then(j => {
const n = j.total_unidades || 0, inst = (j.unidades || []).filter(u => u.instalada).length;
const partes = n ? [plural(n, 'unidad con actas', 'unidades con actas'), inst ? plural(inst, 'instalada', 'instaladas') : 'ninguna instalada'] : ['Sin unidades registradas'];
if (s.nota) partes.push(s.nota);
estado.textContent = partes.join(' · ');
}).catch(() => { estado.textContent = s.nota || ''; });
});
}
// ---------------------------------------------------------------- arranque
async function iniciar() {
if (!$('inicio') || !window.Kanban) return;
const K = window.Kanban;
let P;
try { P = await json('/datos/proyecto.json'); }
catch (err) { falla($('in-ruta'), 'el estado del proyecto', err); return; }
pintarFase(P);
pintarHito(P);
pintarFechas(P, []);
window.Ruta.montar($('in-ruta'), P, { enlace: t => '/tableros/ingenieria/#' + t.clave });
// tablero de pendientes (extracto) y lo que depende de él
K.montar($('in-kb'), {
filtros: 'min', vistas: true, compacto: true, limite: { otros: 4, hecho: 3 }, limitePersona: 2,
enlaceMas: '/tableros/pendientes/', nuevoPorColumna: false, barraEn: $('in-pend-acc'),
onCargado: tareas => pintarFechas(P, tareas),
onPintado: (vis, info) => {
const ab = vis.filter(t => t.estado !== 'hecho').length, ve = vis.filter(t => t.vencida).length;
$('in-resumen').textContent = plural(ab, 'abierto', 'abiertos') + (ve ? ', ' + plural(ve, 'vencido', 'vencidos') : '') + (info.filtro.mios ? ', solo los tuyos' : '');
}
});
K.cargarEquipo().then(() => pintarEquipo(P));
// el enfoque espera al equipo para escribir los nombres («Lo escriben Leo y Adriana»)
Promise.all([json('/api/hoy').catch(() => null), K.cargarEquipo().catch(() => null)]).then(([r]) => pintarEnfoque(P, r));
Promise.all([json('/api/plan').catch(() => null), json('/api/actas').catch(() => null)])
.then(([p, a]) => pintarPedido(P, p && p.plan, a ? (a.actas || []) : null));
window.Actividad.montar($('in-actividad'), { dias: 7, limite: 6, corto: 46, mas: '/bitacora/#actividad' });
pintarDocs();
pintarMapa(P);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', iniciar); else iniciar();
})();

View File

@ -79,6 +79,12 @@
--hg-aviso: #F2C46B; --hg-aviso-bg: #3A3020;
--hg-info: #9DB8FF; --hg-info-bg: #1B2847;
--hg-pend: #8C99B5; --hg-pend-bg: #1F2A42;
--hg-halo-curso: rgba(81, 243, 218, 0.22);
--hg-halo-bloqueado: rgba(255, 148, 148, 0.22);
/* ruta a producción: lo hecho va en blanco (21-oct: «la tarjeta en blanco, no en verde») */
--hg-ruta: #F2F5FA;
--hg-ruta-linea: rgba(242, 245, 250, 0.82);
/* pistas del tablero */
--hg-p-ingenieria: #8FB0FF;
@ -140,6 +146,11 @@ html[data-tema="claro"],
--hg-aviso: #8A5A00; --hg-aviso-bg: #FFF3D6;
--hg-info: #2F55B5; --hg-info-bg: #E6EDFC;
--hg-pend: #65728D; --hg-pend-bg: #E8ECF3;
--hg-halo-curso: rgba(20, 104, 92, 0.18);
--hg-halo-bloqueado: rgba(180, 35, 24, 0.16);
--hg-ruta: #1F2A40;
--hg-ruta-linea: rgba(31, 42, 64, 0.78);
--hg-p-ingenieria: #3D63C9;
--hg-p-produccion: #1E8A5E;

156
docs/tableros/actividad.js Normal file
View File

@ -0,0 +1,156 @@
/* Actividad: lo que pasó en el tablero de pendientes y en las actas (GET /api/actividad).
No hay bitácora aparte que mantener: el servidor junta el historial de cada pendiente y las
actas recibidas (las series PRUEBA no cuentan). Lo usan la página de inicio y la Bitácora.
Uso: Actividad.montar(contenedor, { dias: 7, limite: 6, porDia: false });
Necesita tableros/kanban.js (avatares y nombres del equipo) y tableros/tablero.css. */
(function () {
const ESTADOS = { 'por-hacer': 'Por hacer', 'en-curso': 'En curso', 'bloqueado': 'Bloqueado', 'hecho': 'Hecho' };
const ACTAS = {
'prueba-v5': 'tarjeta V5', 'prueba-v4': 'tarjeta V4', 'modulo-ard': 'ArduSimple', 'modulo-4g': 'módem 4G',
'integracion': 'integración', 'ensamble': 'ensamble', 'reflasheo': 'reflasheo', 'prueba-ethernet-serie': 'Ethernet y serie',
'envio': 'envío', 'recepcion': 'recepción', 'instalacion-campo': 'instalación', 'diagnostico-campo': 'diagnóstico en campo',
'diagnostico-banco': 'diagnóstico en banco', 'dano': 'daño', 'prueba-bateria-modem': 'prueba de batería del módem'
};
const CAMPOS = { titulo: 'el título', descripcion: 'la descripción', pista: 'la pista', responsable: 'el responsable',
fecha_limite: 'la fecha', version_tarjeta: 'la tarjeta', enlaces: 'las ligas', etiquetas: 'las etiquetas',
bloqueada_por: 'de qué depende' };
const DIAS = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
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(typeof c === 'string' ? document.createTextNode(c) : c));
return e;
};
const corto = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
const dia = iso => (iso || '').slice(0, 10);
function hoyISO(delta) { const d = new Date(Date.now() + (delta || 0) * 86400000); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); }
// «Hoy, 10:52» · «Ayer, 19:30» · «lun 19, 17:10» · «3 oct, 09:30»
function cuando(iso) {
if (!iso) return '';
const f = dia(iso), h = iso.slice(11, 16);
if (f === hoyISO()) return 'Hoy' + (h ? ', ' + h : '');
if (f === hoyISO(-1)) return 'Ayer' + (h ? ', ' + h : '');
const d = new Date(f + 'T12:00:00');
const hace = (Date.now() - d.getTime()) / 86400000;
const base = hace < 6.5 ? DIAS[d.getDay()].slice(0, 3) + ' ' + d.getDate() : d.getDate() + ' ' + MES[d.getMonth()];
return base + (h ? ', ' + h : '');
}
// encabezado de día en la Bitácora: «Hoy» · «Ayer» · «Lunes 19 de octubre»
function tituloDia(f) {
if (f === hoyISO()) return 'Hoy';
if (f === hoyISO(-1)) return 'Ayer';
const d = new Date(f + 'T12:00:00');
const s = DIAS[d.getDay()] + ' ' + d.getDate() + ' de ' + MESES[d.getMonth()];
return s.charAt(0).toUpperCase() + s.slice(1);
}
// Lo que dice el historial («estado: por-hacer → en-curso; fecha_limite: … → …») en una frase.
function frasePendiente(e) {
const c = String(e.cambio || '');
const m = c.match(/^(?:creado en|creó el pendiente)\s*([a-z-]*)/i);
if (m) return { verbo: 'creó', fin: m[1] && m[1] !== 'por-hacer' && ESTADOS[m[1]] ? ' en ' + ESTADOS[m[1]] : '' };
const partes = c.split(/;\s*/).map(p => p.match(/^([a-z_]+):\s*(.*?)\s*→\s*(.*)$/)).filter(Boolean);
if (!partes.length) return { verbo: 'comentó en', nota: c };
const est = partes.find(p => p[1] === 'estado');
if (est) {
const a = (est[3] || '').split(' — ')[0].trim();
if (a === 'hecho') return { verbo: 'terminó', fin: '' };
return { verbo: 'movió', fin: ' a ' + (ESTADOS[a] || a) };
}
const campos = partes.map(p => p[1]);
if (campos.length === 1 && campos[0] === 'fecha_limite') {
const f = (partes[0][3] || '').trim();
const fm = f.match(/^(\d{4})-(\d{2})-(\d{2})/);
return { verbo: 'cambió la fecha de', fin: fm ? ' al ' + (+fm[3]) + ' ' + MES[+fm[2] - 1] : ' (sin fecha)' };
}
if (campos.length === 1 && CAMPOS[campos[0]]) return { verbo: 'cambió ' + CAMPOS[campos[0]] + ' de', fin: '' };
return { verbo: 'editó', fin: '' };
}
function avatar(id, nombreLibre) {
if (window.Kanban) return id ? window.Kanban.avatar(id) : window.Kanban.avatar(null, nombreLibre || '?');
return el('span', { class: 'av', text: String(id || nombreLibre || '?').slice(0, 2).toUpperCase() });
}
function quienEs(login, equipo) {
const id = String(login || '').toLowerCase().trim();
const eq = (window.Kanban && window.Kanban.equipo) || {};
if (id && eq[id] && window.Kanban.nombreCorto) return { id, nombre: window.Kanban.nombreCorto(id) };
if (id && (eq[id] || equipo[id])) return { id, nombre: ((eq[id] || {}).nombre || equipo[id] || id).split(' ')[0] };
return { id: null, nombre: login || 'Alguien' };
}
function evento(e, equipo, opts) {
const li = el('li', { class: 'act act-' + e.tipo });
const q = e.tipo === 'acta' ? quienEs(e.quien, equipo) : quienEs(e.quien, equipo);
if (e.tipo === 'acta' && !q.id && e.tecnico) q.nombre = e.tecnico;
li.append(q.id ? avatar(q.id) : avatar(null, q.nombre));
const t = el('div', { class: 'act-t' });
const quien = el('b', { text: q.nombre });
if (e.tipo === 'pendiente') {
const f = frasePendiente(e);
const a = el('a', { href: '/tableros/pendientes/?id=' + encodeURIComponent(e.id || ''), text: '«' + corto(e.titulo, opts.corto || 70) + '»' });
t.append(quien, ' ' + f.verbo + ' ', a);
if (f.fin) t.append(f.fin);
if (f.nota) t.append(el('span', { class: 'act-nota', text: corto(f.nota, 160) }));
} else {
const a = el('a', { href: '/tableros/unidad/?serie=' + encodeURIComponent(e.serie || ''), text: e.serie || 'sin serie' });
t.append(quien, ' registró el acta de ' + (ACTAS[e.acta] || e.acta) + ' de ', a);
if (e.resultado) t.append(' ', el('span', { class: 'act-res' + (/FALLA|BAJA|DA[ÑN]/i.test(e.resultado) ? ' mal' : ''), text: e.resultado }));
}
// en la Bitácora el día ya va en el encabezado: solo la hora
li.append(t, el('time', { class: 'act-f', datetime: e.cuando || '', text: opts.porDia ? String(e.cuando || '').slice(11, 16) : cuando(e.cuando) }));
return li;
}
async function montar(cont, opts) {
opts = opts || {};
const dias = opts.dias || 7, limite = opts.limite || 30;
cont.classList.add('act-caja');
cont.innerHTML = '';
cont.append(el('p', { class: 'vacio', text: 'Cargando…' }));
let j;
try {
const r = await fetch('/api/actividad?dias=' + dias + '&limite=' + limite, { cache: 'no-store' });
j = await r.json();
if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
} catch (err) {
cont.innerHTML = '';
cont.append(el('p', { class: 'vacio', text: 'No se pudo cargar la actividad (' + err.message + ').' }));
return null;
}
if (window.Kanban && window.Kanban.cargarEquipo) { try { await window.Kanban.cargarEquipo(); } catch (e) { /* sin equipo: iniciales del login */ } }
const equipo = j.equipo || {};
const eventos = j.eventos || [];
cont.innerHTML = '';
if (!eventos.length) {
cont.append(el('p', { class: 'vacio', text: 'Sin movimientos en los últimos ' + dias + ' días.' }));
return j;
}
if (opts.porDia) {
let actual = null, lista = null;
eventos.forEach(e => {
const f = dia(e.cuando);
if (f !== actual) {
actual = f;
cont.append(el('h3', { class: 'act-dia', text: tituloDia(f) }));
lista = el('ol', { class: 'act-lista' });
cont.append(lista);
}
lista.append(evento(e, equipo, opts));
});
} else {
const lista = el('ol', { class: 'act-lista' });
eventos.forEach(e => lista.append(evento(e, equipo, opts)));
cont.append(lista);
}
if (j.total > eventos.length && opts.mas) cont.append(el('a', { class: 'act-mas', href: opts.mas, text: (j.total - eventos.length) + ' movimientos más en la Bitácora' }));
return j;
}
window.Actividad = { montar, cuando, frasePendiente };
})();

View File

@ -10,7 +10,7 @@ Las páginas que abres para saber **dónde está el proyecto hoy**: qué pendien
| Quiero… | Tablero | Qué muestra |
|---|---|---|
| Los **pendientes** del equipo y quién los tiene | [Pendientes](pendientes.md) | Columnas *Por hacer · En curso · Bloqueado · Hecho*; pistas Ingeniería, Producción, Mecánica, Compras y Campo; responsable, fecha límite, ligas a documentos y actas; historial de cada cambio. «Solo los míos» deja solo los tuyos |
| Los **pendientes** del equipo y quién los tiene | [Pendientes](pendientes.md) | Columnas *Por hacer · En curso · Bloqueado · Hecho*; pistas Ingeniería, Producción, Mecánica, Compras y Campo; responsable, fecha límite, ligas a documentos y actas; historial de cada cambio. «Solo los míos» deja solo los tuyos; «Por persona», una fila por responsable |
| **Dónde está el hardware** (V5.5 → V5.6 → V5.7 → v6) | [Ingeniería](ingenieria.md) | Una tarjeta por versión con su estado, siguiente paso, responsables y documentos; los pendientes de la pista Ingeniería; últimas decisiones y filosofía de diseño |
| **Cuántas unidades** llevamos y cuántas faltan | [Producción](produccion.md) | Plan 113 V5.6 · 327 V5.7 · 130 v6 contra las unidades reales por etapa; embudo por paso; bloqueos; alertas de inventario |
| Qué **tarjeta, módulo o unidad** pasó cada acta | [Actas](actas.md) | Series V5-NN, ARD-NNN, RTK-NNNN y V4-NN; parejas integradas; unidades ensambladas. Pendiente = liga al acta |

View File

@ -8,19 +8,23 @@ hide:
<div class="tb tb-ingenieria">
<header class="tb-cab">
<div class="tb-cab__t"><h1>Ingeniería</h1><span class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6 — estado al 19-oct-2026</span></div>
<div class="tb-cab__t"><h1>Ingeniería</h1><span class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6</span></div>
</header>
<details class="info tb-como">
<summary>Cómo se usa</summary>
<ol>
<li>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í.</li>
<li>Arriba, la <b>ruta a producción</b> (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 <code>docs/datos/proyecto.json</code>, el mismo que usa la página de inicio: cuando cambie una decisión, pide a Leo o a Claude que lo actualice ahí.</li>
<li>En medio, los <b>pendientes de la pista Ingeniería</b>: son los mismos del <a href="/tableros/pendientes/">tablero de pendientes</a>, filtrados. Puedes moverlos desde aquí.</li>
<li>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.</li>
<li>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.</li>
</ol>
</details>
<h2 class="ing-h">Ruta a producción</h2>
<p class="sub">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.</p>
<div id="ruta" class="ing-ruta"><p class="vacio">…</p></div>
<div class="versiones" id="versiones"></div>
<section class="bloque">
@ -33,6 +37,8 @@ hide:
<h2>Últimas decisiones</h2>
<p class="sub">Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.</p>
<ul class="decisiones">
<li><b>21-oct (noche)</b> — <b>Pasada 5 revisada</b> (<code>6458c5d</code>): 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: <b>el sensado ya no es Kelvin</b> (el límite de entrada baja ≈ 3–4 %). Falta la <b>pasada 6</b>, ≈ 30 min por tarjeta; en la V5.7, además, girar C149/C150 (lazo de ≈ 56 mm²). <b>v6:</b> T25 y T28 aplicados sobre <code>9255b84</code> (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. <a href="/hardware/v56-v57-auditoria-21oct/#5-pasada-5-revisada-6458c5d-y-lo-que-falta">Pasada 5 revisada</a> · <a href="/hardware/v6-estado-21oct/">Estado de la v6</a></li>
<li><b>21-oct (tarde)</b> — <b>Auditoría de la pasada 4 de Michelle</b> (HBox_PCB <code>85e7602</code> V5.6, <code>1416dcb</code> V5.7). <b>V5.6:</b> la pasada dejó un corto nuevo (<code>/MODEM_EN</code> a GND en R106) y una pista de <code>/SYS5V</code> a 0.09 mm de una vía; el giro de U7 mejoró el nodo SW (L7 a 4 mm de U7). <b>V5.7:</b> ya sin el bloqueante de K1. <b>D-21-1, aprobada por Leo:</b> C130 de 100 nF en la entrada de U7, en las dos. En la tarde quedaron <b>T32</b> (C130 en los esquemáticos) y <b>T33</b> (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 <b>pasada 5</b> de Michelle (V5.6 ≈ 1 h, V5.7 ≈ medio día); después, revisión del cobre y Gerber. <a href="/hardware/v56-v57-auditoria-21oct/">Auditoría del 21-oct</a></li>
<li><b>19-oct (19:30)</b> — <b>V5.6: qué es crítico y qué se puede quedar</b> (archivo de trabajo de las 18:59, sobre T30). <b>Un solo crítico, nuevo:</b> 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 <b>C129</b> (10 µF 0603, C19702) junto a los pines 1/24, sin mover piezas. Regresar 2 vías de <code>/MAIN5V</code> y 2 de <code>/3V3_MODEM</code> 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. <a href="/hardware/v56-clasificacion-19oct/">Clasificación</a></li>
<li><b>19-oct (17:10)</b> — <b>Re-auditoría V5.6 / V5.7 / v6</b> sobre la pasada 3 de Michelle (HBox_PCB <code>4f4681f</code>). <b>V5.6 casi lista</b>: <code>/BAT_PLUS</code> fuera de capas internas, <code>/3V3_MODEM</code> 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). <b>V5.7 NO LISTA</b>: ya replica la V5.6, pero <code>/SYS5V</code> va 10.9 mm por In2 a 0.5 mm bajo K1 (bloqueante) y el lazo de entrada de U25 mide 52 mm². <b>v6 sin avance</b> 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. <b>Script T30</b> probado en copias en la Mac (DRC sin infracciones nuevas, 0 sin conectar). Merge del lunes (<code>40decf8</code>) arreglado por Leo (<code>240e2c3</code> + <code>merge=binary</code>) 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. <a href="/hardware/re-auditoria-19oct/">Re-auditoría</a> · <a href="/hardware/v56-v57-layout-pasada-4/">Pasada 4</a> · <a href="/hardware/v6-plan-fines-de-semana/">Plan v6 rev. 1.1</a></li>
<li><b>16-oct (17:30)</b> — <b>Auditoría de producción de la v6: NO LISTA.</b> Ruteo completo (<code>2612891</code>, 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 <code>--set v6</code> y <b>T28</b> (probados con DRC). <b>Plan de fines de semana</b>: 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). <a href="/hardware/v6-auditoria-produccion-16oct/">Auditoría v6</a> · <a href="/hardware/v6-plan-fines-de-semana/">Plan</a></li>
@ -54,7 +60,7 @@ hide:
</ul>
</section>
<section class="bloque">
<section class="bloque" id="filosofia">
<h2>Filosofía de diseño</h2>
<p class="sub">Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en <a href="/hardware/">Tarjetas y hardware</a>.</p>
<ul class="filosofia">
@ -67,49 +73,52 @@ hide:
</section>
</div>
<script src="/tableros/ruta.js"></script>
<script>
(function () {
const VERSIONES = [
{ v: 'V5.5', cls: 'v55', badge: 'badge-v55', estado: 'En campo — 15 pzas (lote 22-ago), fw 0.6.x',
siguiente: 'Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a V5.6. Inspeccionar U6 con lupa en cada tarjeta. Medir en una unidad el límite de entrada del cargador (≈ 1 A, auditoría 16-oct §4.3).',
quien: 'Jesús (pruebas) · Alan y Daniel (campo)',
docs: [['Manual V5', '/manuales/manual-tarjeta-V5/'], ['Auditoría 16-oct §4', '/hardware/auditoria-produccion-16oct/'], ['Lecciones V5.5 → v6', '/hardware/v6-revision-lecciones-v55/'], ['Acta de tarjeta V5', '/formularios/v5.html']] },
{ v: 'V5.6', cls: 'v56', badge: 'badge-v56', estado: 'Casi lista: T30 aplicado (7650b96); falta la pasada 4, parte A, con un crítico nuevo: C129 en VSYS de U6 (19-oct, tarde)',
siguiente: 'Pasada 4, parte A: primero C129 junto a VSYS de U6 (crítico); regresar 4 vías perdidas; quitar la rama de Dbk2; exclusiones del DRC → Claude re-audita y genera Gerber/BOM/CPL → pedido del piloto de 5 (≈ 21–22-oct, con los 5 de V5.7) → banco con firmware 0.7.0 (incluye medir VSYS, el riel del módem y el GNSS) → las 108 restantes (con SD-15B). Antes del pedido: decisión D-19-1 (puertos).',
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido, firmware) · Jesús (banco)',
docs: [['Qué es crítico (19-oct, tarde)', '/hardware/v56-clasificacion-19oct/'], ['Pasada 4 — V5.6 y V5.7', '/hardware/v56-v57-layout-pasada-4/'], ['Re-auditoría 19-oct', '/hardware/re-auditoria-19oct/'], ['Auditoría 16-oct (requisitos)', '/hardware/auditoria-produccion-16oct/'], ['ECO V5.6', '/hardware/v56-eco/'], ['Hoja de datos', '/hardware/v56-datasheet/'], ['Pinout', '/hardware/v56-pinout/']] },
{ v: 'V5.7', cls: 'v57', badge: 'badge-v57', estado: 'Re-auditada 19-oct — NO LISTA: pasada 3 hecha (f3f35f4), ya replica la V5.6; bloqueante /SYS5V por In2 bajo K1; lazo de entrada de U25 de 52 mm²',
siguiente: 'Lun 19: T30 (Leo). Mar 20 – mié 21: Michelle pasada 4, parte B (≈ medio día: /SYS5V fuera de In2, C149/C150 y L22 junto a U25, lo mismo que V5.6) → re-auditoría → 5 prototipos con el piloto de V5.6 (ECO §6: 9/12/24/32 V, polaridad, temperatura) → pedido 327. Entrada 9–32 V.',
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido)',
docs: [['Pasada 4 — parte B', '/hardware/v56-v57-layout-pasada-4/#parte-b-v57-hbox-watchdog-v57-medio-dia'], ['Re-auditoría 19-oct §4', '/hardware/re-auditoria-19oct/#4-v57-punto-por-punto'], ['ECO V5.7 — fuente a bordo', '/hardware/v57-eco-fuente/']] },
{ v: 'v6', cls: 'v6', badge: 'badge-v6', estado: 'Re-auditada 19-oct: NO LISTA, sin cambio — solo 15 vías de tierra nuevas (00a9af4); siguen los 8 bloqueantes del 16-oct',
siguiente: 'Lun 19 – mar 20: pre-órdenes JLC (THVD1410DR, TPS7A2033PDBVR, J14). Sáb 24 8:00: Leo corre T25 --set v6 + T28. Sáb 24: Michelle bloques A y B (potencia) + MH4/MH6. Dom 25: bloques C, D y E (RF del módem, U20, GNSS, U12). Sáb 31 – dom 1-nov: pares, puertos, In1, T29 (vías GND), serigrafía, DRC final. Lun 2-nov: piloto de 20. Firmware v6 los fines de semana 7–8 y 14–15 nov; banco 21–22 nov (G2). No habrá v6.1.',
quien: 'Michelle (layout) · Leo · Claude · Adriana (pre-órdenes)',
docs: [['Plan de fines de semana (rev. 1.1)', '/hardware/v6-plan-fines-de-semana/'], ['Auditoría v6 (16-oct)', '/hardware/v6-auditoria-produccion-16oct/'], ['Re-auditoría 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']] },
];
// Ruta y tarjetas por versión: docs/datos/proyecto.json (el mismo archivo que usa la página de inicio).
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
const cont = document.getElementById('versiones');
const contadores = {};
VERSIONES.forEach(x => {
const c = el('div', { class: 'ver ' + x.cls });
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.v }), el('span', { class: x.badge, text: x.v })));
c.append(el('div', { class: 'est', text: x.estado }));
c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' + x.siguiente }));
const qd = el('div', { class: 'quien', html: Kanban.icono('equipo') }); qd.append(el('span', { text: x.quien })); c.append(qd);
const d = el('div', { class: 'docs' }); x.docs.forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.v] = cnt; c.append(cnt);
cont.append(c);
});
Kanban.montar(document.getElementById('kb'), {
pista: 'ingenieria', filtros: true,
onCargado: tareas => {
VERSIONES.forEach(x => {
const mios = tareas.filter(t => t.version_tarjeta === x.v && t.estado !== 'hecho');
let TAREAS = null;
function contar() {
if (!TAREAS) return;
Object.keys(contadores).forEach(v => {
const mios = TAREAS.filter(t => t.version_tarjeta === v && t.estado !== 'hecho');
const bloq = mios.filter(t => t.estado === 'bloqueado').length, venc = mios.filter(t => t.vencida).length;
const a = el('a', { href: '/tableros/pendientes/?pista=ingenieria', text: mios.length + ' pendiente(s) abierto(s)' + (bloq ? ' · ' + bloq + ' bloqueado(s)' : '') + (venc ? ' · ' + venc + ' vencido(s)' : '') });
contadores[x.v].innerHTML = ''; contadores[x.v].append(a);
const n = (k, a, b) => k + ' ' + (k === 1 ? a : b);
const a = el('a', { href: '/tableros/pendientes/?pista=ingenieria', text: n(mios.length, 'pendiente abierto', 'pendientes abiertos') + (bloq ? ' · ' + n(bloq, 'bloqueado', 'bloqueados') : '') + (venc ? ' · ' + n(venc, 'vencido', 'vencidos') : '') });
contadores[v].innerHTML = ''; contadores[v].append(a);
});
}
fetch('/datos/proyecto.json', { cache: 'no-store' }).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(P => {
const m = String(P.actualizado || '').match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) document.getElementById('meta').textContent = 'V5.5 · V5.6 · V5.7 · v6, al día el ' + (+m[3]) + ' ' + MES[+m[2] - 1];
Ruta.montar(document.getElementById('ruta'), P, { enlace: t => '#' + t.clave });
(P.tarjetas || []).forEach(x => {
const c = el('div', { class: 'ver ' + x.clave, id: x.clave });
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.version }), el('span', { class: BADGE[x.version] || 'badge-v4', text: x.version })));
c.append(el('div', { class: 'est', text: x.detalle || x.estado || '' }));
if (x.siguiente) c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' }, document.createTextNode(x.siguiente)));
if (x.quien) { const qd = el('div', { class: 'quien', html: Kanban.icono('equipo') }); qd.append(el('span', { text: x.quien })); c.append(qd); }
const d = el('div', { class: 'docs' }); (x.docs || []).forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.version] = cnt; c.append(cnt);
cont.append(c);
});
contar();
if (location.hash) { const t = document.getElementById(location.hash.slice(1)); if (t) t.scrollIntoView(); }
}).catch(e => {
document.getElementById('ruta').innerHTML = '';
document.getElementById('ruta').append(el('div', { class: 'err', text: 'No se pudo leer el estado de las tarjetas (' + e.message + ').' }));
});
Kanban.montar(document.getElementById('kb'), {
pista: 'ingenieria', filtros: true, vistas: true,
onCargado: tareas => { TAREAS = tareas; contar(); }
});
})();
</script>

View File

@ -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) => '<svg class="i' + (cls ? ' ' + cls : '') + '" viewBox="0 0 24 24" aria-hidden="true"><path d="' + (ICONOS[n] || '') + '"/></svg>';
// Í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] + '<span>' + nombre + '</span><span class="n">0</span>' }));
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') + '<span>Espera: ' + esc(bl.map(b => corto(b.titulo.split(':')[0], 42)).join(' · ')) + '</span>' }));
// 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') + '<span>' + esc(x.titulo || x.url) + '</span>' }); 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] + '<span>' + (k === 'hecho' && filtro.hechas === 'recientes' ? 'Hecho, 14 días' : n) + '</span>' })));
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; } };
})();

View File

@ -22,6 +22,7 @@ hide:
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.</li>
<li>Al terminar, muévelo a <b>Hecho</b>. No lo borres: solo el administrador borra, y el historial sirve de bitácora.</li>
<li>Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.</li>
<li><b>Por persona</b> 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 (<code>?vista=persona</code>, <code>?mios=1</code>) se puede compartir tal cual.</li>
</ol>
</details>
@ -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;

96
docs/tableros/ruta.js Normal file
View File

@ -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 };
})();

View File

@ -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; }

52
hooks/documentos.py Normal file
View File

@ -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))

View File

@ -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

17
overrides/inicio.html Normal file
View File

@ -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() }}
<link rel="stylesheet" href="{{ 'inicio/inicio.css' | url }}">
<link rel="preload" href="{{ 'img/tarjetas/portada-v56.webp' | url }}" as="image" type="image/webp">
<script src="{{ 'tableros/ruta.js' | url }}"></script>
<script src="{{ 'tableros/actividad.js' | url }}"></script>
<script src="{{ 'inicio/inicio.js' | url }}" defer></script>
{% endblock %}

View File

@ -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() }}
<link rel="stylesheet" href="{{ 'tableros/tablero.css' | url }}">
<style>:root{--hg-ancho-contenido:none}</style>
<style>:root{--hg-ancho-contenido:{{ page.meta.ancho | default('none') }}}</style>
<script src="{{ 'tableros/kanban.js' | url }}"></script>
{% endblock %}

View File

@ -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

View File

@ -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 <commit>: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).

View File

@ -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()})

View File

@ -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('<html><body style="margin:0">' + txt + '</body></html>')
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())

View File

@ -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'<svg xmlns="http://www.w3.org/2000/svg" width="{OW}" height="{OH}" viewBox="0 0 {OW} {OH}">',
f'<rect width="{OW}" height="{OH}" fill="{T["fondo"]}"/>']
def grupo(op, partes):
if partes:
o.append(f'<g opacity="{op}" fill="#fff" stroke="#fff">' + ''.join(partes) + '</g>')
def lineas(capas, solo=None, sin=None):
r = []
for x1, y1, x2, y2, w, l, n in d['segments']:
if l not in capas or (solo is not None and n not in solo) or (sin is not None and n in sin):
continue
r.append(f'<line x1="{X(x1):.1f}" y1="{Y(y1):.1f}" x2="{X(x2):.1f}" y2="{Y(y2):.1f}" '
f'stroke-width="{max(w * s, T["min_w"]):.2f}" stroke-linecap="round" fill="none"/>')
return r
def pads(solo=None, sin=None):
r = []
for ax, ay, w, h, prot, shape, typ, n, ref, layer, lays in d['pads']:
if not any(l in ('F.Cu', '*.Cu') for l in lays):
continue
if (solo is not None and n not in solo) or (sin is not None and n in sin):
continue
cx, cy, ww, hh = X(ax), Y(ay), w * s, h * s
if shape == 'circle':
r.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{ww / 2:.1f}" stroke="none"/>')
else:
rr = min(ww, hh) / 2 if shape == 'oval' else (min(ww, hh) * 0.18 if shape == 'roundrect' else 0)
r.append(f'<rect x="{cx - ww / 2:.1f}" y="{cy - hh / 2:.1f}" width="{ww:.1f}" height="{hh:.1f}" rx="{rr:.1f}" '
f'stroke="none" transform="rotate({-prot:.0f} {cx:.1f} {cy:.1f})"/>')
return r
def vias(solo=None, sin=None):
return [f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="{sz * s / 2:.1f}" stroke="none"/>'
for x, y, sz, n in d['vias'] if not ((solo is not None and n not in solo) or (sin is not None and n in sin))]
zonas = []
for lay, net, pts in d['zones']:
if lay in ('F.Cu', 'B.Cu') and pts:
zonas.append('<path d="M' + ' L'.join(f'{X(x):.1f},{Y(y):.1f}' for x, y in pts) + 'Z" '
f'stroke-width="{0.12 * s:.2f}" fill-opacity="0.6"/>')
grupo(T['z'], zonas)
grupo(T['b'], lineas({'B.Cu'}, sin=hl))
grupo(T['i'], lineas({'In1.Cu', 'In2.Cu'}, sin=hl))
grupo(T['f'], lineas({'F.Cu'}, sin=hl))
grupo(T['pad'], pads(sin=hl))
grupo(T['via'], vias(sin=hl))
if hl:
grupo(T['ni'], lineas({'B.Cu', 'In1.Cu', 'In2.Cu'}, solo=hl))
grupo(T['nf'], lineas({'F.Cu'}, solo=hl))
grupo(T['npad'], pads(solo=hl) + vias(solo=hl))
# barrenos: del color del fondo, encima de todo
hoyos = [f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="{sz * s / 4.2:.1f}"/>' for x, y, sz, n in d['vias']]
for ax, ay, w, h, prot, shape, typ, n, ref, layer, lays in d['pads']:
if typ == 'thru_hole':
hoyos.append(f'<circle cx="{X(ax):.1f}" cy="{Y(ay):.1f}" r="{min(w, h) * s * 0.28:.1f}"/>')
o.append(f'<g fill="{T["fondo"]}">' + ''.join(hoyos) + '</g>')
# contorno de la tarjeta (solo en miniaturas)
if tema == 'mini':
bx, by, bw, bh = contorno(d)
o.append(f'<rect x="{X(bx):.1f}" y="{Y(by):.1f}" width="{bw * s:.1f}" height="{bh * s:.1f}" rx="{1.2 * s:.1f}" '
f'fill="none" stroke="#fff" stroke-opacity="0.35" stroke-width="{max(0.35 * s, 1):.2f}"/>')
o.append('</svg>')
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'))