diff --git a/DESPLIEGUE.md b/DESPLIEGUE.md index 33737dd..7c39994 100644 --- a/DESPLIEGUE.md +++ b/DESPLIEGUE.md @@ -15,6 +15,7 @@ Servidor: `git.helgen.online` · dominio público: `https://drogheda.helgen.onli | **Sitios** | `/srv/projects/drogheda/sitios` | las escribe el backend — **13-oct** | | **Plan de producción** | `/srv/projects/drogheda/plan-produccion.json` | lo escribe el backend (admin) — **13-oct** | | **Equipo (miembros, bitácora de accesos)** | `/srv/projects/drogheda/equipo` | las escribe el backend — **14-oct** | +| **Enfoque de la semana (página de inicio)** | `/srv/projects/drogheda/enfoque.json` | lo escriben Leo y Adriana desde el inicio — **21-oct** | | Usuarios (HTTP Basic) | `/etc/nginx/hbox-docs.htpasswd` | `htpasswd` | **Lo importante:** `deploy.sh` publica **solo el sitio**. Si cambió `backend/app.py` —como en esta @@ -260,6 +261,38 @@ pendiente sin responsable debe rechazarse; `backend/tests-1014.sh` reproduce tod --- +## A4 · 21-oct: página de inicio, enfoque de la semana y actividad + +El inicio deja de ser una página escrita a mano: lee `docs/datos/proyecto.json` (fase, ruta a +producción, fechas, sitios), el tablero y dos rutas nuevas del backend: + +- `GET /api/hoy` · `PUT /api/hoy`: el enfoque de la semana. Lo escriben `HOY_EDITORES` (por omisión + `leo,adriana`) y los administradores; se guarda en `enfoque.json` junto a `/actas`, con sus 20 + versiones anteriores. +- `GET /api/actividad?dias=7&limite=30`: lo que pasó, del historial de los pendientes y de las actas + recibidas (sin las series PRUEBA). La usan el inicio y la Bitácora. + +No hay carpetas ni variables nuevas que fijar (las dos vienen comentadas en el `.service`). Como +cambió `backend/app.py`, después del deploy hay que reiniciar el servicio: + +```bash +# Mac +cd ~/Documents/Claude/Projects/Drogheda/hbox-docs && ./scripts/deploy.sh +ssh leo@git.helgen.online 'sudo systemctl restart actas-hbox' + +# verificar (en el servidor) +curl -s -H 'X-Remote-User: leo' http://127.0.0.1:8081/api/hoy +curl -s -H 'X-Remote-User: leo' 'http://127.0.0.1:8081/api/actividad?dias=7&limite=3' | head -c 400 +``` + +| Síntoma | Causa | Arreglo | +|---|---|---| +| En el inicio, el enfoque dice «El servidor no respondió» y la actividad, «HTTP 404» | backend viejo | el `restart` de arriba | +| «El enfoque de la semana lo escriben adriana y leo» al guardar | ese usuario no está en `HOY_EDITORES` | así debe ser; para agregar a alguien, `Environment=HOY_EDITORES=leo,adriana,…` en el `.service` | +| `PermissionError` al guardar el enfoque | `/srv/projects/drogheda` no es de `leo` | `chown` del paso 2 de A2 | + +--- + ## B · Verificación después de desplegar ```bash diff --git a/backend/actas-hbox.service b/backend/actas-hbox.service index 3a68294..ad66220 100644 --- a/backend/actas-hbox.service +++ b/backend/actas-hbox.service @@ -20,6 +20,9 @@ Environment=ACTAS_ADMINS=leo #Environment=PLAN_FILE=/srv/projects/drogheda/plan-produccion.json #Environment=EQUIPO_DIR=/srv/projects/drogheda/equipo #Environment=HTPASSWD_LOCK=/srv/projects/drogheda/.htpasswd.lock +# Página de inicio (21-oct): quién escribe el enfoque de la semana y dónde se guarda. +#Environment=HOY_EDITORES=leo,adriana +#Environment=HOY_FILE=/srv/projects/drogheda/enfoque.json Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd Environment=PASS_MIN=8 Environment=PORT=8081 diff --git a/backend/app.py b/backend/app.py index 96b02ec..abe1bd0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -37,6 +37,11 @@ autenticado crea y modifica, solo un administrador borra: Equipo y accesos (14-oct): los miembros del equipo NO son lo mismo que los usuarios con login. GET /api/equipo · POST /api/equipo (admin) · PATCH /api/equipo/miembro?id= (admin) POST /api/usuarios (admin: crea login en el htpasswd + miembro) · DELETE /api/usuario?u= (admin: quita el login) +Inicio (21-oct): + GET /api/hoy · PUT /api/hoy (solo HOY_EDITORES, por omisión leo y adriana, y los administradores): + el enfoque de la semana que se ve en la página de inicio; guarda las 20 versiones anteriores. + GET /api/actividad?dias=7&limite=30 → lo que pasó: cambios de pendientes (de su historial) y actas + recibidas, del más nuevo al más viejo. Las series PRUEBA no cuentan. """ import os, re, json, csv, datetime, uuid, hashlib, unicodedata, fcntl from flask import Flask, request, jsonify, abort, send_file @@ -1945,5 +1950,97 @@ def sitio_unidades(): total_actas=len(actas), danos=[u for u in lista if u["marca"]], admin=is_admin()) +# ------------------------------------------------------------------ inicio: enfoque de la semana y actividad (21-oct) +# El enfoque es un texto corto que escriben Leo y Adriana (HOY_EDITORES) y se ve arriba en la página de +# inicio. La actividad junta lo que ya guardan los pendientes (historial) y las actas (_recibido): no +# hay bitácora aparte que mantener. +HOY_FILE = os.environ.get("HOY_FILE", os.path.join(DATOS_BASE, "enfoque.json")) +HOY_EDITORES = {u.strip().lower() for u in os.environ.get("HOY_EDITORES", "leo,adriana").split(",") if u.strip()} + + +def _puede_enfoque(): + u = current_user().lower() + return bool(u) and (u in HOY_EDITORES or u in ADMINS) + + +@app.get("/api/hoy") +def hoy_leer(): + h = _leer_json(HOY_FILE) + if not isinstance(h, dict) or not h.get("titulo"): + h = None + else: + h = {k: h.get(k) for k in ("titulo", "puntos", "editado_por", "editado_en")} + return jsonify(ok=True, enfoque=h, puede_editar=_puede_enfoque(), editores=sorted(HOY_EDITORES), + user=current_user()) + + +@app.put("/api/hoy") +def hoy_escribir(): + u = quien() + if not u: + return _sin_usuario() + if not _puede_enfoque(): + return jsonify(ok=False, error="El enfoque de la semana lo escriben " + " y ".join(sorted(HOY_EDITORES)) + + ". Pídeselo a uno de ellos."), 403 + d = _json_body() + if d is None: + return jsonify(ok=False, error="JSON inválido"), 400 + titulo = _texto(d.get("titulo"), 160) + if not titulo: + return jsonify(ok=False, error="Falta la frase de la semana."), 400 + puntos = d.get("puntos") + if isinstance(puntos, str): + puntos = puntos.split("\n") + puntos = [_texto(p, 400) for p in (puntos if isinstance(puntos, list) else []) if _texto(p, 400)][:8] + base = _leer_json(HOY_FILE) or {} + anteriores = list(base.get("historial") or []) + if base.get("titulo"): + anteriores.append({k: base.get(k) for k in ("titulo", "puntos", "editado_por", "editado_en")}) + h = {"titulo": titulo, "puntos": puntos, "editado_por": u, "editado_en": ahora(), "historial": anteriores[-20:]} + _escribir_json(HOY_FILE, h) + return jsonify(ok=True, enfoque={k: h[k] for k in ("titulo", "puntos", "editado_por", "editado_en")}) + + +@app.get("/api/actividad") +def actividad(): + dias = max(1, min(_entero(request.args.get("dias"), 7), 60)) + limite = max(1, min(_entero(request.args.get("limite"), 30), 200)) + desde = (datetime.datetime.now() - datetime.timedelta(days=dias)).replace(microsecond=0).isoformat() + eventos = [] + for t in _listar_json(TAREAS_DIR): + for h in (t.get("historial") or []): + cuando = str(h.get("cuando") or "") + if cuando >= desde: + eventos.append({"tipo": "pendiente", "cuando": cuando, "quien": h.get("quien") or "", + "cambio": _texto(h.get("cambio"), 300), "id": t.get("id"), + "titulo": t.get("titulo"), "pista": t.get("pista"), "estado": t.get("estado")}) + if os.path.isdir(BASE): + for tipo in sorted(os.listdir(BASE)): + carpeta = os.path.join(BASE, tipo) + if tipo.startswith((".", "_")) or not os.path.isdir(carpeta): + continue + for fn in os.listdir(carpeta): + ruta = os.path.join(carpeta, fn) + if not fn.endswith(".json"): + continue + try: # filtro barato antes de abrir el archivo + if datetime.datetime.fromtimestamp(os.path.getmtime(ruta)).isoformat() < desde: + continue + except OSError: + continue + d = _leer_json(ruta) + if not isinstance(d, dict) or d.get("_prueba"): + continue + cuando = str(d.get("_recibido") or "") + if cuando < desde: + continue + eventos.append({"tipo": "acta", "cuando": cuando, "quien": d.get("_usuario") or "", + "tecnico": _texto(d.get("tecnico"), 80), "acta": tipo, "serie": d.get("serie"), + "resultado": d.get("resultado") or d.get("clasificacion") or ""}) + eventos.sort(key=lambda e: e["cuando"], reverse=True) + equipo = {k: m.get("nombre") for k, m in _miembros().items()} + return jsonify(ok=True, desde=desde, total=len(eventos), eventos=eventos[:limite], equipo=equipo) + + if __name__ == "__main__": app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081"))) diff --git a/docs/bitacora.md b/docs/bitacora.md index cc3ff6a..e05c88a 100644 --- a/docs/bitacora.md +++ b/docs/bitacora.md @@ -1,16 +1,33 @@ --- title: "Bitácora" -description: Los documentos del proyecto por fecha, del más nuevo al más viejo. +description: Lo que pasó en el tablero y en las actas, y los documentos del proyecto por fecha. +template: tablero.html +ancho: 52rem --- +
+ # Bitácora -Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra solo lo vigente; aquí queda todo, incluido lo superado. Lo que pasa cada día en el tablero (quién movió qué) se verá aquí cuando el servidor lo publique; por ahora vive en el historial de cada [pendiente](tableros/pendientes.md). +Arriba, lo que pasó en el tablero de pendientes y en las actas en los últimos 14 días: sale solo del historial de cada [pendiente](tableros/pendientes.md) y de las actas recibidas. Abajo, los documentos del proyecto por fecha; el menú lateral muestra solo lo vigente y aquí queda todo, incluido lo superado. + +## Actividad { #actividad } + +

…

+ +## Documentos por fecha { #documentos-por-fecha } !!! info "Cómo se agrega un documento" - Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado». + Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado». Si el documento trae `date:` en su encabezado, también sale solo en «Documentos recientes» de la página de inicio. -## Octubre de 2026 +### Octubre de 2026 + +**21 oct (noche)** +: [Estado de la v6 y T25/T28 aplicados](hardware/v6-estado-21oct.md). T28 cerró 2 de los 8 bloqueantes; D34 quedó lejos del pin SW de U25 (lazo de ≈ 115 mm²) y MH8 cae encima de D34. Falta el push de Leo. +: [Pasada 5 revisada](hardware/v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta): el sensado de RS1 dejó de ser Kelvin en las dos tarjetas; falta la pasada 6, ≈ 30 min por tarjeta. + +**21 oct** +: [Auditoría de la pasada 4 de Michelle: V5.6 y V5.7](hardware/v56-v57-auditoria-21oct.md). El corto de R106 y lo que no pide mover piezas ya quedó con T32 y T33 (C130 aprobado por Leo); a Michelle le tocó la pasada 5. **19 oct (tarde)** : [V5.6: qué es crítico y qué se puede quedar](hardware/v56-clasificacion-19oct.md). Un solo crítico, nuevo: C129 en VSYS de U6. L7 y lo heredado de la V5.5 se quedan para el piloto y se miden en el banco. @@ -43,7 +60,7 @@ Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra **1 oct** : [Hoja de datos de la V5.6](hardware/v56-datasheet.md). -## Septiembre de 2026 +### Septiembre de 2026 **29 sep** : [Pinout de la V5.6](hardware/v56-pinout.md) y [reglas de layout, del esquemático a la PCB](hardware/v56-layout-instrucciones.md). @@ -63,3 +80,10 @@ Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra : [Auditoría pre-fabricación de la v6](hardware/v6-auditoria-prefab.md) ([en inglés](hardware/v6-prefab-audit.md)) (superada por la del 28 sep). Lo anterior a septiembre está en la [historia del proyecto](historia/index.md). + +
+ + + diff --git a/docs/datos/proyecto.json b/docs/datos/proyecto.json new file mode 100644 index 0000000..14e810c --- /dev/null +++ b/docs/datos/proyecto.json @@ -0,0 +1,74 @@ +{ + "_como": "Datos fijos de la página de inicio y del tablero de Ingeniería (Ruta a producción y una tarjeta por versión). Los actualiza Claude o Leo cuando una tarjeta cambia de etapa o se mueve un hito; después, deploy. Estados de cada paso: hecho, ahora, bloqueado, falta. Cada paso es [estado, texto, debajo]. Tono de la tarjeta: hecho, ahora, aviso, bloqueado. Hitos: fecha AAAA-MM-DD (null si aún no hay fecha), fin opcional y «cuando», el texto que se ve. El enfoque de la semana NO va aquí: lo escriben Leo y Adriana en la página de inicio (/api/hoy); enfoque_inicial solo se muestra mientras nadie lo ha escrito.", + "actualizado": "2026-10-21", + "fase": { "nombre": "Pre-producción", "texto": "V5.6 y V5.7 en la pasada 6; la v6 en layout" }, + "pedido_nota": "primero los pilotos: 5 V5.6 y 5 V5.7, luego 20 v6", + "gestion": { "id": "adriana", "texto": "Adriana lleva la gestión del proyecto" }, + "etapas": ["Esquemático", "Layout", "Auditoría", "Gerber", "Piloto", "Banco", "Pedido"], + "tarjetas": [ + { + "version": "V5.5", "clave": "v55", "estado": "En campo", "tono": "hecho", + "nuevo": "15 piezas; ya no se fabrica", + "imagen": "/img/tarjetas/v55.webp", "alt": "Cobre de la EinarBoard V5.5", + "pasos": [["hecho"], ["hecho"], ["hecho", "EMC 22 ago"], ["hecho"], ["hecho"], ["hecho", "0.6.0 en campo"], ["hecho", "Ago", "15 piezas"]], + "detalle": "En campo: 15 piezas (lote del 22-ago), firmware 0.6.x.", + "siguiente": "Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a la V5.6. Revisar U6 con lupa en cada tarjeta. Jesús mide en una unidad el límite de entrada del cargador con TP1 (≈ 1 A, auditoría del 16-oct, §4.5).", + "quien": "Jesús (pruebas) · Alán y Daniel (campo)", + "docs": [["Manual V5", "/manuales/manual-tarjeta-V5/"], ["Auditoría del 16-oct, §4", "/hardware/auditoria-produccion-16oct/"], ["Lecciones de la V5.5", "/hardware/v6-revision-lecciones-v55/"], ["Acta de tarjeta V5", "/formularios/v5.html"]] + }, + { + "version": "V5.6", "clave": "v56", "estado": "Casi lista", "tono": "ahora", + "nuevo": "Lo nuevo: control del módem y acelerómetro. Pedido de 113", + "imagen": "/img/tarjetas/v56.webp", "alt": "Cobre de la EinarBoard V5.6; lo nuevo, en blanco", + "pasos": [["hecho", "28 sep"], ["ahora", "Pasada 6", "Michelle, ≈ 30 min"], ["falta", "Revisión del cobre", "Claude"], ["falta", "Gerber y BOM", "Leo y Claude"], ["falta", "5 piezas", "con las 5 de la V5.7"], ["falta", "Firmware 0.7.0", "Jesús; por escribir"], ["falta", "113"]], + "detalle": "Casi lista (21-oct, noche): la pasada 5 (6458c5d) dejó bien C129 y C130, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: el sensado ya no es Kelvin. Falta la pasada 6, ≈ 30 min.", + "siguiente": "Pasada 6 (Michelle, ≈ 30 min): la vía de entrada de RS1 a 0.2 mm o más del hilo de sensado, con una 2.ª vía unida en F.Cu y en B.Cu; 2.ª vía de la entrada de U7; exclusiones de MH7, MH9 y MH10; el tramo suelto de /RS485_B → revisión del cobre (Claude, sobre todo el sensado de RS1) → Gerber, BOM y CPL → pedido del piloto de 5, junto con los 5 de la V5.7 → banco con el firmware 0.7.0 → las 108 restantes, con SD-15B.", + "quien": "Michelle (layout) · Claude (revisión) · Leo (scripts, pedido, firmware) · Jesús (banco)", + "docs": [["Lo que falta: pasada 6 (21-oct, noche)", "/hardware/v56-v57-auditoria-21oct/#5-pasada-5-revisada-6458c5d-y-lo-que-falta"], ["Auditoría de la pasada 4 (21-oct)", "/hardware/v56-v57-auditoria-21oct/"], ["Qué es crítico (19-oct)", "/hardware/v56-clasificacion-19oct/"], ["Pasada 4", "/hardware/v56-v57-layout-pasada-4/"], ["Re-auditoría del 19-oct", "/hardware/re-auditoria-19oct/"], ["ECO V5.6", "/hardware/v56-eco/"], ["Hoja de datos", "/hardware/v56-datasheet/"], ["Pinout", "/hardware/v56-pinout/"]] + }, + { + "version": "V5.7", "clave": "v57", "estado": "No lista", "tono": "aviso", + "nuevo": "Lo nuevo: fuente de 9 a 32 V a bordo. Pedido de 327", + "imagen": "/img/tarjetas/v57.webp", "alt": "Cobre de la EinarBoard V5.7; la fuente a bordo, en blanco", + "pasos": [["hecho", "13 oct"], ["ahora", "Pasada 6", "Michelle, ≈ 30 min"], ["falta", "Revisión del cobre", "Claude"], ["falta", "Con la V5.6", "Leo y Claude"], ["falta", "5 prototipos", "con las 5 de la V5.6"], ["falta", "9 a 32 V", "Jesús: polaridad y calor"], ["falta", "327"]], + "detalle": "No lista (21-oct, noche), pero ya sin el bloqueante de K1. La pasada 5 dejó bien C130 y las 2 vías de /VEH5V; quedan RS1 (igual que en la V5.6), C149/C150 sin girar (lazo de ≈ 56 mm², meta < 30) y el GND de C129.", + "siguiente": "Pasada 6 (Michelle, ≈ 30 min): RS1 como en la V5.6, 2.ª vía de la entrada de U7, 1 o 2 vías al GND de C129, C149/C150 girados hacia D34 y el EP de U25 (lazo de menos de 30 mm²), exclusiones y el tramo suelto de /24V → revisión del cobre (Claude) → 5 prototipos con el piloto de la V5.6 (ECO §6: 9, 12, 24 y 32 V, polaridad y temperatura) → pedido de 327. Entrada de 9 a 32 V.", + "quien": "Michelle (layout) · Claude (revisión) · Leo (pedido) · Jesús (prototipos)", + "docs": [["Lo que falta: pasada 6 (21-oct, noche)", "/hardware/v56-v57-auditoria-21oct/#5-pasada-5-revisada-6458c5d-y-lo-que-falta"], ["Auditoría del 21-oct, §3.2", "/hardware/v56-v57-auditoria-21oct/#32-v57-hbox-watchdog-v57"], ["Re-auditoría del 19-oct, §4", "/hardware/re-auditoria-19oct/#4-v57-punto-por-punto"], ["ECO V5.7: fuente a bordo", "/hardware/v57-eco-fuente/"]] + }, + { + "version": "v6", "clave": "v6", "estado": "No lista", "tono": "bloqueado", + "nuevo": "Lo nuevo: GNSS y 4G en la tarjeta. Pedido de 130", + "imagen": "/img/tarjetas/v6.webp", "alt": "Cobre de la EinarBoard v6; el GNSS y el 4G, en blanco", + "pasos": [["hecho", "28 sep"], ["bloqueado", "6 bloqueantes", "T28 cerró 2; D34 lejos de U25"], ["falta", "25 oct y 1 nov", "Claude"], ["falta", "1 nov", "Claude (G1)"], ["falta", "20, el 2 nov", "Aldair y Leo"], ["falta", "21–22 nov", "Jesús y Leo (G2)"], ["falta", "130", "después de G2"]], + "detalle": "No lista (21-oct): T25 y T28 ya están aplicados (falta el push de Leo) y T28 cerró 2 de los 8 bloqueantes. Michelle adelantó la fuente U25, pero D34 quedó a 13 mm del pin SW (lazo caliente de ≈ 115 mm², antes ≈ 47) y MH8 cae encima de D34.", + "siguiente": "Leo: commit y push de T25 y T28; Michelle hace git pull --ff-only antes de abrir la v6. Sáb 24: Michelle regresa D34 junto a U25.8 (moviendo R53 y R54 junto a U6), libera MH8, MH4 y MH6, y sigue con los bloques A y B. Dom 25: bloques C, D y E (módem, GNSS y U12); en la noche, re-auditoría de Claude. Sáb 31: bloques F, G y H1 (pares, puertos serie e In1). Dom 1-nov: H2 y DRC final; en la tarde, re-auditoría final y Gerber (G1). Lun 2-nov: pedido del piloto de 20. Firmware los fines de semana del 7–8 y del 14–15 nov; banco del piloto el 21–22 nov (G2). No habrá v6.1.", + "quien": "Michelle (layout) · Leo (scripts y pedido) · Claude (re-auditoría y Gerber) · Aldair (pedido) · Jesús (banco)", + "docs": [["Estado de la v6 (21-oct)", "/hardware/v6-estado-21oct/"], ["Plan de fines de semana (rev. 1.1)", "/hardware/v6-plan-fines-de-semana/"], ["Auditoría de la v6 (16-oct)", "/hardware/v6-auditoria-produccion-16oct/"], ["Re-auditoría del 19-oct, §8", "/hardware/re-auditoria-19oct/#8-v6"], ["Plan de corrección (28-sep)", "/hardware/v6-plan-correccion/"], ["Manual v6", "/manuales/manual-tarjeta-v6/"], ["Plan de cobre", "/hardware/EinarBoard-v6_Plan-de-Cobre_ES.html"]] + } + ], + "hitos": [ + { "fecha": null, "cuando": "Por fijar", "texto": "Pedido del piloto: 5 V5.6 y 5 V5.7, después de la pasada 6", "quien": ["leo"] }, + { "fecha": "2026-10-24", "fin": "2026-10-25", "cuando": "24 y 25 oct", "texto": "v6, primer fin de semana de layout", "quien": ["michelle", "leo"] }, + { "fecha": "2026-10-31", "fin": "2026-11-01", "cuando": "31 oct y 1 nov", "texto": "v6, segundo fin de semana y Gerber (G1)", "quien": ["michelle", "leo", "claude"] }, + { "fecha": "2026-11-02", "cuando": "2 nov", "texto": "Pedido del piloto de 20 v6", "quien": ["aldair", "leo"] }, + { "fecha": "2026-11-07", "fin": "2026-11-15", "cuando": "7 al 15 nov", "texto": "Firmware de la v6, dos fines de semana", "quien": ["leo", "claude"] }, + { "fecha": "2026-11-21", "fin": "2026-11-22", "cuando": "21 y 22 nov", "texto": "Banco del piloto de la v6 (G2)", "quien": ["jesus", "leo"] } + ], + "enfoque_inicial": { + "titulo": "Esta semana: la pasada 6 y el piloto de V5.6 y V5.7.", + "puntos": [ + "La pasada 5 dejó bien C129 y C130 en las dos tarjetas. Falta la pasada 6 (≈ 30 min por tarjeta): sacar la vía de RS1 de encima del hilo de sensado, la 2.ª vía de RS1 y de U7 y, en la V5.7, girar C149/C150.", + "Después, Claude revisa el cobre (sobre todo el sensado de RS1), Gerber y un solo pedido de 5 y 5.", + "v6: T25 y T28 ya están aplicados; falta el push de Leo. El sábado 24 se empieza por D34, que quedó lejos de U25, y por MH8." + ], + "nota": "Texto inicial tomado de la [auditoría del 21-oct](/hardware/v56-v57-auditoria-21oct/) y del [estado de la v6](/hardware/v6-estado-21oct/). Leo y Adriana lo cambian con «Editar»." + }, + "sitios": [ + { "id": "icave", "nombre": "ICAVE", "ciudad": "Veracruz", "x": 68.27, "y": 73.55, "etiqueta": "der", "nota": "se compara con el CalAmp" }, + { "id": "lzc", "nombre": "LZC", "ciudad": "Lázaro Cárdenas", "x": 49.6, "y": 80.34, "etiqueta": "der" }, + { "id": "eit", "nombre": "EIT", "ciudad": "Ensenada", "x": 9.34, "y": 7.92, "etiqueta": "der" }, + { "id": "timsa", "nombre": "TIMSA", "ciudad": "Manzanillo", "x": 43.06, "y": 74.67, "etiqueta": "izq" } + ], + "oficina": { "nombre": "Helgen", "ciudad": "Querétaro", "x": 55.08, "y": 67.1 } +} diff --git a/docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png b/docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png new file mode 100644 index 0000000..83ce75b Binary files /dev/null and b/docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png differ diff --git a/docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png b/docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png new file mode 100644 index 0000000..c83a5bc Binary files /dev/null and b/docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png differ diff --git a/docs/hardware/index.md b/docs/hardware/index.md index 13ec0e7..7e7d56d 100644 --- a/docs/hardware/index.md +++ b/docs/hardware/index.md @@ -6,19 +6,19 @@ title: "Tarjetas y hardware" Estado de cada versión de la EinarBoard al **21-oct-2026**, con sus documentos vigentes, los históricos y el siguiente paso. ¿No sabes qué tarjeta tienes? [Manuales por tarjeta](../manuales/index.md) te la identifica por serigrafía, conectores y LEDs. Estado vivo (tareas, decisiones, responsables): [Tablero de ingeniería](../tableros/ingenieria.md). -!!! warning "21-oct: pasada 4 auditada y, en la tarde, T32 + T33 — falta la pasada 5 de Michelle" - - **V5.6 — todavía no se pide.** C129 quedó colocado y unido a U6, salió la rama de Dbk2, y U7 girado acortó el nodo SW de 13 a 10 mm. El corto `/MODEM_EN`–GND que dejó la pasada 4 en R106 y la separación de 0.09 mm bajo U7 **ya los quitó T33**, junto con las vías sencillas de VSYS y de `/3V3_MODEM`. **C130** (100 nF en la entrada de U7, D-21-1) ya está en el esquemático (T32). Falta (≈ 1 h): F8, C130 junto a U7.2, el GND de C129 y la entrada de RS1 ([pasada 5](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5)). - - **V5.7 — ya sin el bloqueante de K1.** `/SYS5V` salió de In2 y L22 quedó junto a D34; T33 reforzó 4 cambios de capa del 5 V y quitó las vías sueltas. Falta acercar C149/C150 a U25 (lazo de 52 mm²), C130 y la vía de (168.0, 110.6): ≈ medio día ([§3.2](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57)). - - **v6 — sin cambio** desde el 19-oct. Plan de fines de semana: 24–25 oct y 31 oct–1 nov. +!!! warning "21-oct, noche: pasada 5 de V5.6/V5.7 revisada y T25 + T28 aplicados en la v6" + - **V5.6 — todavía no se pide.** La pasada 5 (`6458c5d`) colocó C130 junto a U7.2 y la vía del GND de C129, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: **el sensado ya no es Kelvin**. Falta (≈ 30 min): esa vía y una 2.ª de RS1 bien unida, la 2.ª vía de U7.2 y las exclusiones del DRC ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). + - **V5.7 — ya sin el bloqueante de K1.** Lo mismo en RS1 y en U7.2, más C149/C150 sin girar (lazo de ≈ 56 mm²) y el GND de C129 ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). + - **v6 — T25 y T28 aplicados** (falta el push). Michelle adelantó la fuente U25, pero D34 quedó lejos del pin SW (lazo de ≈ 115 mm²) y MH8 cae encima de D34: [**▶ estado de la v6 al 21-oct**](v6-estado-21oct.md). Detalle y lista para Michelle: [**▶ Auditoría del 21-oct**](v56-v57-auditoria-21oct.md). Antes: [re-auditoría del 19-oct](re-auditoria-19oct.md) · [qué es crítico en la V5.6](v56-clasificacion-19oct.md) · [plan v6 (rev. 1.1)](v6-plan-fines-de-semana.md). | Tarjeta | Estado | Pedido | Proyecto KiCad | Firmware | |---|---|---|---|---| | V5.5 | En campo (15 pzas, 22-ago). No se fabrica más. | — | `hbox-watchdog-v5` (tag `v5.5-as-fabbed`) | `hbox-v5-bringup_0.6.0` CAMPO · 0.6.3 banco | -| V5.6 | Pasada 4 auditada el 21-oct (`85e7602`) y T32 + T33 en la tarde: **no lista**. El corto de R106 ya salió; faltan C130 junto a U7.2, el GND de C129 y la entrada de RS1: ≈ 1 h ([pasada 5](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5)). C129 ya está, con T31 (`3daff19`). | 113 (piloto de 5 primero) | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), no existe | -| V5.7 | Pasada 4 auditada el 21-oct (`1416dcb`) y T32 + T33 en la tarde: **NO LISTA**, pero ya sin el bloqueante de K1. Faltan C149/C150 junto a U25, C130 y una vía del 5 V: ≈ medio día ([§3.2](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57)). Entrada 9–32 V. | 327 (5 prototipos primero) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) | -| v6 | Re-auditada el 19-oct: **NO LISTA**, sin cambio (`00a9af4`: solo 15 vías de tierra); siguen los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md) (rev. 1.1). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe | +| V5.6 | Pasada 5 revisada el 21-oct en la noche (`6458c5d`): **no lista**. C129 y C130 ya están; la vía de entrada de RS1 quedó encima del hilo de sensado (ya no es Kelvin). Falta ≈ 30 min ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). | 113 (piloto de 5 primero) | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), no existe | +| V5.7 | Pasada 5 revisada el 21-oct en la noche (`6458c5d`): **NO LISTA**, pero ya sin el bloqueante de K1. Igual que la V5.6 en RS1, más C149/C150 sin girar (lazo de ≈ 56 mm²) y el GND de C129 ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). Entrada 9–32 V. | 327 (5 prototipos primero) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) | +| v6 | Revisada el 21-oct (`9255b84` + T25/T28): **NO LISTA**. T28 cerró 2 de los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md) y T25 regresó los barrenos; Michelle adelantó la fuente U25, pero D34 quedó lejos del pin SW y MH8 cae encima ([estado del 21-oct](v6-estado-21oct.md)). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe | | V4 | En prueba y campo. Sin producción nueva. | — | `hbox-watchdog-v4` | `hbox-tarjetaV4_0.6.0` CAMPO | | V3 | 3 unidades en campo. Registro congelado. | — | `hbox-watchdog-v3-PROD` (no editar) | `hbox-tarjetaV3_4.0.0` | @@ -52,7 +52,8 @@ Estado de cada versión de la EinarBoard al **21-oct-2026**, con sus documentos - **Ya lo hizo T30** (`7650b96`, 19-oct): vías en paralelo en los cambios de capa de potencia (18 vías), las 4 vías «micro» a vías normales, JP/TP/MOD1 fuera del BOM y del CPL, cajetín Rev 5.6 y stackup JLC04161H-7628. - **Pasada 4 entregada el 21-oct (`85e7602`):** C129 colocado (T31 + F8), rama de Dbk2 fuera, U7 girado (L7.1 a 4.0 mm de U7.8), vías de `/MAIN5V` hacia P11 de vuelta. - **T32 + T33 (21-oct, tarde):** C130 en el esquemático; fuera el corto de R106 y la separación de 0.09 mm; 2.ª vía de VSYS; 3 vías en cada cambio de capa de `/3V3_MODEM` y de `/SYS5V` hacia U12; vías sueltas fuera. DRC: solo los 3 errores de MOD1, 0 sin conectar. -- **Falta (pasada 5, ≈ 1 h):** F8 y C130 junto a U7.2 con la 2.ª vía de la entrada de U7, vías al GND de C129 y la 2.ª vía de RS1. Más las exclusiones del DRC: [lista](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5). +- **Pasada 5 (`6458c5d`, 21-oct, noche):** C130 junto a U7.2 con su vía a GND y la vía al GND de C129. **Nuevo:** la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6 (ya no es Kelvin) y la «2.ª vía» de RS1 no está unida en B.Cu. +- **Falta (pasada 6, ≈ 30 min):** esa vía fuera del sensado y una 2.ª vía de RS1 unida en F.Cu y B.Cu, la 2.ª vía de la entrada de U7, las exclusiones del DRC y un tramo suelto de `/RS485_B`: [lista](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta). L7 y lo heredado de la V5.5 se pueden quedar para el piloto, y se miden en el banco: [**▶ qué es crítico y qué se puede quedar**](v56-clasificacion-19oct.md). @@ -78,7 +79,8 @@ Lleva Mean Well SD-15B-05 externa (igual que V5.5). - **Lo hace T30:** lo mismo que en la V5.6, más 11 vías de tierra en la entrada y la salida de U25, y borra 12 cobres sueltos. - **Pasada 4, auditada el 21-oct (`1416dcb`):** hechos `/SYS5V` fuera de In2, L22 junto a D34, U7 girado (L7.1 a 0.4 mm), C129 y la serigrafía de Cbst2. - **T32 + T33 (21-oct, tarde):** C130 en el esquemático, 2.ª vía de VSYS y otros 3 cambios de capa del 5 V reforzados, vías sueltas fuera. DRC: solo los 3 errores de MOD1, 0 sin conectar. -- **Falta (≈ medio día):** C149/C150 junto a U25, C130 junto a U7.2, la vía de (168.0, 110.6), el GND de C129 y la entrada de RS1: [lista](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57). +- **Pasada 5 (`6458c5d`, 21-oct, noche):** C130 junto a U7.2 con su vía a GND, 2 vías en (168.0, 110.6) y C149/C150 a 1.0 mm de U25.2. **Nuevo:** igual que en la V5.6, la vía de entrada de RS1 quedó encima del hilo de sensado. +- **Falta (pasada 6):** RS1 como en la V5.6, C149/C150 girados (lazo de ≈ 56 mm², meta < 30), el GND de C129, la 2.ª vía de U7.2, las exclusiones y un tramo suelto de `/24V`: [lista](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta). | | Documentos | |---|---| @@ -104,11 +106,15 @@ Lleva Mean Well SD-15B-05 externa (igual que V5.5). - 96 vías de tierra (meta ≥ 400; eran 81 el 16-oct). - Stackup genérico → T28. -El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a probar sobre `00a9af4` el 19-oct: aplican igual (con los 14 errores esperados de MH4, que se liberan a mano). El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**. +El sensado de RS1 ya está bien (hay que conservarlo). + +**21-oct (`9255b84`, Michelle):** adelantó el bloque A (L22 a 2.2 mm del pin SW de U25, 6 vías en su EP) y llevó `/VEH5V` de L22 a RS1 todo por F.Cu. Pero D34 quedó a 13 mm del pin SW: el lazo caliente pasó de ≈ 47 a ≈ 115 mm². **T25 y T28 ya están aplicados** (falta el push): cierran los bloqueantes 1 y 8 y regresan los barrenos; el DRC da 22 errores en MH4 y MH8, que Michelle libera. Detalle: [**▶ estado de la v6 al 21-oct**](v6-estado-21oct.md). + +El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**. | | Documentos | |---|---| -| Vigentes | [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.1: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) | +| Vigentes | [**▶ Estado al 21-oct (T25 + T28)**](v6-estado-21oct.md) · [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.1: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) | | Detalle (28-sep) | [Auditoría completa](v6-auditoria-completa.md) ([inglés](v6-full-audit.md)) · [Pasada 1 — cambios al esquemático](v6-cambios-esquematico.md) · [Pasada 2 — puertos (inglés)](v6-puertos-sin-nueva-version.md) · [Pasada 3 — revisión final (inglés)](v6-revision-final.md) | | Históricos | [Próximos pasos para ordenar (29-sep)](v6-proximos-pasos.md) — superado por el plan de fines de semana · [Auditoría V5.6 · V5.7 · v6 del 16-oct (mañana), §8](auditoria-produccion-16oct.md#8-v6-que-falta) · [Revisión contra lecciones V5.5](v6-revision-lecciones-v55.md) — superada por la auditoría completa · [Auditoría pre-fab 25-sep](v6-auditoria-prefab.md) ([inglés](v6-prefab-audit.md)) — superada por la del 28-sep | | Cantidad | La auditoría de septiembre hablaba de **5 prototipos**; el plan del 13-oct son **130 unidades y todos los reemplazos futuros**. Recomendado: piloto de 20 que pase el banco con el firmware 0.7.x antes de las 130. | @@ -116,8 +122,8 @@ El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a **Siguiente paso** ([plan de fines de semana, rev. 1.1](v6-plan-fines-de-semana.md); recorrido una semana el 19-oct): - **Lun 19 o mar 20:** pre-órdenes en JLC (THVD1410DR, TPS7A2033PDBVR, J14). No se recorren. -- **Sáb 24, 8:00:** Leo corre T25 `--set v6` y T28. -- **Sáb 24:** Michelle trabaja la potencia y libera MH4/MH6. +- **21-oct:** T25 y T28 ya aplicados; falta el push de Leo. +- **Sáb 24:** Michelle regresa D34 junto a U25, libera MH4, MH6 y MH8, y sigue con la potencia ([estado del 21-oct, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir)). - **Dom 25:** RF del módem, U20, GNSS y U12. - **Sáb 31 y dom 1-nov:** pares, puertos, In1, vías de tierra (T29), serigrafía y DRC final. P7/P8 se quitan del esquemático (decisión del 9-oct). - **Lun 2-nov:** pedido del piloto de 20. diff --git a/docs/hardware/v56-v57-auditoria-21oct.md b/docs/hardware/v56-v57-auditoria-21oct.md index 62957a8..f58f7d5 100644 --- a/docs/hardware/v56-v57-auditoria-21oct.md +++ b/docs/hardware/v56-v57-auditoria-21oct.md @@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v5.6, v5.7, layout, auditor project: Project Drogheda author: Helgen Technologies (revisión: Claude; pedido: Leo) date: 2026-10-21 -version: 1.1 -status: ACTIVO — audita HBox_PCB 85e7602 (V5.6) y 1416dcb (V5.7) · 21-oct tarde: T32 (C130) y T33 (arreglos por script) aplicados · lo que queda para Michelle en la sección 3 +version: 1.2 +status: ACTIVO — audita HBox_PCB 85e7602 (V5.6) y 1416dcb (V5.7) · 21-oct tarde: T32 (C130) y T33 (arreglos por script) aplicados · 21-oct noche: pasada 5 (6458c5d) revisada, lo que falta en la sección 5 audience: Leo, Michelle (layout) --- @@ -17,6 +17,8 @@ audience: Leo, Michelle (layout) **Actualización (21-oct, tarde):** el corto, la separación, la 2.ª vía de VSYS y casi todas las vías de potencia ya quedaron con **T33**, y C130 ya está en los dos esquemáticos con **T32**. A Michelle le queda lo que pide mover pistas: C130 junto a U7.2, el GND de C129, la entrada de RS1 y, en la V5.7, C149/C150 ([sección 3](#3-para-michelle-pasada-5-despues-de-t32-y-t33)). +**Actualización (21-oct, noche):** Michelle ya hizo la pasada 5 (`6458c5d`). C130 quedó bien en las dos, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: **el sensado ya no es Kelvin**. Lo que falta está en la [sección 5](#5-pasada-5-revisada-6458c5d-y-lo-que-falta). + | Tarjeta | Commit | DRC (errores): commit → con T33 | Sin conectar | Paridad | Veredicto (con T32 y T33) | |---|---|---|---|---|---| | **V5.6** (`hbox-watchdog-v5`) | `85e7602` | **5 nuevos** (un corto y una separación) + los 3 de MOD1 → **solo los 3 de MOD1** | 0 | solo PF* (+ C130 hasta el F8) | **No lista:** el corto y la separación ya salieron. Faltan C130 junto a U7.2, el GND de C129 y la entrada de RS1 (≈ 1 h). Luego Gerber. | @@ -140,6 +142,48 @@ Avísale a Leo con los dos hashes: Claude revisa el cobre antes de pedir Gerber. - Si no cabe junto a U7.2, el punto 3 queda en 🟡 y se mide en el piloto: zumbido en U7.2 y caída del riel del módem en J7.1. - **Giro de U7: aceptarlo.** Mejora el nodo SW de 13 a 6–10 mm; lo que empeora, se arregla con los puntos 3 y D-21-1. +## 5. Pasada 5 revisada (`6458c5d`) y lo que falta + +![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. diff --git a/docs/hardware/v6-auditoria-produccion-16oct.md b/docs/hardware/v6-auditoria-produccion-16oct.md index 917d24b..b7c60da 100644 --- a/docs/hardware/v6-auditoria-produccion-16oct.md +++ b/docs/hardware/v6-auditoria-produccion-16oct.md @@ -18,6 +18,9 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana !!! note "19-oct: sin cambios en los bloqueantes" El commit del sábado 17 (`00a9af4`) solo agregó 15 vías de tierra (81 → 96) y mejoró la cobertura de In1 bajo la RF LTE. Todo lo de esta auditoría sigue vigente; T25 y T28 se volvieron a probar sobre `00a9af4` y aplican igual. El plan de fines de semana se recorrió una semana: [**rev. 1.1**](v6-plan-fines-de-semana.md) (24–25 oct y 31 oct–1 nov). Detalle: [re-auditoría del 19-oct, §8](re-auditoria-19oct.md#8-v6). +!!! note "21-oct: T25 y T28 aplicados; avance del bloque A" + T25 y T28 ya se aplicaron sobre `9255b84` (falta el push de Leo): quedan cerrados los bloqueantes 1 y 8, y los barrenos ya están en la tarjeta. Michelle adelantó el bloque A, pero D34 quedó lejos del pin SW de U25 y MH8 cae encima de D34. Detalle: [estado de la v6 al 21-oct](v6-estado-21oct.md). + **El plan para cerrarla** está en un documento aparte: [**▶ Plan de fines de semana — v6 lista para producción**](v6-plan-fines-de-semana.md). !!! danger "Veredicto: la v6 todavía no se puede mandar a fabricar" diff --git a/docs/hardware/v6-estado-21oct.md b/docs/hardware/v6-estado-21oct.md new file mode 100644 index 0000000..cfd8228 --- /dev/null +++ b/docs/hardware/v6-estado-21oct.md @@ -0,0 +1,102 @@ +--- +title: "Estado de la v6 al 21-oct y scripts T25/T28 aplicados" +tags: [project-drogheda, hardware, hbox, einarboard, v6, auditoria, t25, t28, barrenos, u25, layout, michelle] +project: Project Drogheda +author: Helgen Technologies (revisión: Claude; pedido: Leo) +date: 2026-10-21 +version: 1.0 +status: ACTIVO — HBox_PCB 9255b84 (Michelle, 21-oct 18:14) con T25 y T28 aplicados por Claude · falta commit y push de Leo +audience: Leo, Michelle (layout) +--- + +# Estado de la v6 al 21-oct · T25 y T28 aplicados + +**Pregunta (Leo, 21-oct):** ¿T28 ya quedó o hay que correrlo? ¿Cómo va la v6? + +**Respuesta corta:** + +- **T28 nunca se había corrido, y T25 tampoco.** Se probaron el 16 y el 19-oct para correrlos el sábado 24 a las 8:00 ([plan, §2](v6-plan-fines-de-semana.md#2-sabado-24-800-leo-scripts-t25-y-t28-15-min)). Hoy los probé en una copia del commit de Michelle (`9255b84`) y los apliqué a los archivos de HBox_PCB. **Falta que Leo haga commit y push.** +- **La v6 todavía no se puede pedir.** Michelle adelantó el bloque A (fuente U25) y parte del B (`/VEH5V`). Hay avance real, pero **D34 quedó lejos del pin SW de U25**: el lazo caliente creció de ≈ 47 a ≈ 115 mm². Además, **MH8 cae encima de D34** y de 2 vías de GND. No se veía porque los barrenos no estaban en la tarjeta. +- **DRC de hoy, con T25 y T28:** 22 errores, todos en MH4 y MH8; 0 sin conectar; la paridad solo marca los 19 PWR_FLAG. + +![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. diff --git a/docs/hardware/v6-plan-fines-de-semana.md b/docs/hardware/v6-plan-fines-de-semana.md index ca1d028..ee9b460 100644 --- a/docs/hardware/v6-plan-fines-de-semana.md +++ b/docs/hardware/v6-plan-fines-de-semana.md @@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v6, plan, layout, firmware, project: Project Drogheda author: Helgen Technologies (plan: Claude; revisión: Leo) date: 2026-10-16 -version: 1.1 -status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00 +version: 1.2 +status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00 · 21-oct: T25 y T28 ya aplicados (falta el push) audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana (PM) --- @@ -16,6 +16,9 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana !!! note "Rev. 1.1 (19-oct): el plan se recorre una semana" El fin de semana del 17–18 se fue en la pasada 3 de V5.6/V5.7; a la v6 solo entraron 15 vías de tierra (`00a9af4`), así que los 8 bloqueantes de la [auditoría del 16-oct](v6-auditoria-produccion-16oct.md) siguen igual ([re-auditoría del 19-oct, §8](re-auditoria-19oct.md#8-v6)). Mismo trabajo, una semana después: **24–25 oct** y **31 oct–1 nov**; pedido del piloto el **lun 2-nov**; G2 ≈ **22-nov**. T25 y T28 se volvieron a probar sobre `00a9af4` y aplican igual. Lo único que no se recorre son las **pre-órdenes en JLC**: se piden ya. +!!! warning "21-oct: T25 y T28 ya están aplicados y Michelle adelantó el bloque A" + El paso del §2 (sábado 24, 8:00) se hizo el 21-oct sobre `9255b84`; falta el commit y push de Leo. Michelle ya movió la fuente U25 (bloque A), pero D34 quedó lejos del pin SW y MH8 cae encima de D34. Lo primero del sábado: [estado de la v6 al 21-oct, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir). + **Dos metas:** | Meta | Qué significa | Fecha objetivo | @@ -39,7 +42,7 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana | Día | Quién | Qué | Listo cuando | |---|---|---|---| -| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2) | push hecho; Michelle avisada | +| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2). **Ya se corrieron el 21-oct: falta el push** | push hecho; Michelle avisada | | **Sáb 24-oct** | Michelle | **Bloques A + B:** el camino de la energía (U25, RS1, Q7, U6, L6) + liberar MH4 y MH6 (§3) | criterios del §3.5 | | **Dom 25-oct** | Michelle | **Bloque C:** módem (RF LTE, U20, VBAT). **Bloque D:** GNSS. **Bloque E:** U12 (§4) | criterios del §4.4 | | **Dom 25-oct, noche** | Claude | Re-auditoría del commit del domingo; lista corta para el lunes | lista enviada | @@ -59,6 +62,8 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana ## 2. Sábado 24, 8:00 — Leo: scripts T25 y T28 (15 min) +**21-oct:** ya se corrieron sobre `9255b84` ([estado del 21-oct](v6-estado-21oct.md)). El DRC da 22 errores, en MH4 y en MH8, y no solo los 14 de MH4: D34 quedó dentro de MH8. Solo falta el commit y push de abajo. + Con KiCad **cerrado** y todo subido. Los dos scripts se probaron juntos en copias del commit `2612891` (16-oct) y del `00a9af4` (el del sábado 17, probado el 19-oct): el DRC solo muestra los errores esperados en MH4. !!! warning "Antes: árbol limpio y nadie más con la v6 abierta" diff --git a/docs/img/mexico.svg b/docs/img/mexico.svg new file mode 100644 index 0000000..bb7d2a0 --- /dev/null +++ b/docs/img/mexico.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/img/tarjetas/portada-v56.webp b/docs/img/tarjetas/portada-v56.webp new file mode 100644 index 0000000..fa12b44 Binary files /dev/null and b/docs/img/tarjetas/portada-v56.webp differ diff --git a/docs/img/tarjetas/v55.webp b/docs/img/tarjetas/v55.webp new file mode 100644 index 0000000..bf11356 Binary files /dev/null and b/docs/img/tarjetas/v55.webp differ diff --git a/docs/img/tarjetas/v56.webp b/docs/img/tarjetas/v56.webp new file mode 100644 index 0000000..c29bd10 Binary files /dev/null and b/docs/img/tarjetas/v56.webp differ diff --git a/docs/img/tarjetas/v57.webp b/docs/img/tarjetas/v57.webp new file mode 100644 index 0000000..1057c87 Binary files /dev/null and b/docs/img/tarjetas/v57.webp differ diff --git a/docs/img/tarjetas/v6.webp b/docs/img/tarjetas/v6.webp new file mode 100644 index 0000000..e4405ca Binary files /dev/null and b/docs/img/tarjetas/v6.webp differ diff --git a/docs/index.md b/docs/index.md index 1dcce79..0889e2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,18 +1,116 @@ --- -title: "HBOX — Documentación técnica" +title: "Inicio" +description: "Drogheda: rastreadores HBOX RTK para el equipo de patio de Hutchison Ports. Dónde va cada tarjeta, el enfoque de la semana, los pendientes y los atajos." +template: inicio.html +ancho: 64rem hide: - toc --- -# HBOX — Documentación técnica + -Sitio interno de Helgen para producir, probar, instalar y diagnosticar el rastreador **HBOX RTK** (tarjeta EinarBoard). Todo trabajo deja un **acta**; toda unidad tiene un **expediente**. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md). +
-
-La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento. +
+ Cobre de la EinarBoard V5.6; lo nuevo de la revisión, en blanco
-## ¿Qué necesitas hacer? +
+ +

Drogheda

+

Rastreadores HBOX RTK para el equipo de patio de Hutchison Ports. Aquí se diseña, se produce, se prueba y se instala.

+
+ + + +
+ +
+
Enfoque de la semana
+

…

+
+
+ +
+
+

Ruta a producción

+ Línea continua: hecho. Punteada: lo que falta. El anillo marca el paso en el que va cada tarjeta. En cada imagen, lo nuevo de la revisión va en blanco. + Tablero de Ingeniería +
+

…

+
+ +
+
+

Pendientes

+ +
+
+
+ +
+
+

Próximas fechas

+

…

+
+
+

Actividad

Bitácora
+
+
+
+

Documentos recientes

Todos
+

…

+
+
+ +
+
+

Dónde estamos

Sitios
+
+
    +
    +
    +

    Cómo trabajamos

    Con detalle
    +

    Antes de proponer un cambio, revisa que no choque con ninguna de estas reglas.

    +
      +
    • Primero la seguridad en campo. La unidad vive a más de 45 °C y en batería; todo se prueba en banco antes de llevarlo a campo.
    • +
    • Una etapa probada vale más que una nueva. Los zócalos se quedan; la fuente de la V5.7 viene del bloque ya auditado de la v6.
    • +
    • Redundancia solo con causa. Por eso salió P12 y entró el acelerómetro U26.
    • +
    • Las reglas de fabricación no se negocian. Pasta de U6 al 50 % o menos, nada de potencia por capas internas, lupa en U6 antes de energizar.
    • +
    • Cambios con prueba y rastro. Scripts verificados contra la netlist, una persona por tarjeta a la vez y no habrá v6.1.
    • +
    +
    +
    + +
    + +
    + +## ¿Qué necesitas hacer? { #que-necesitas-hacer } | Quiero… | Abre | Acta | |---|---|---| @@ -21,7 +119,7 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J | **Diagnosticar** una unidad que falla en la máquina | [Diagnóstico en campo](diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](formularios/diagnostico.html) | | **Diagnosticar** una unidad en la mesa | [Diagnóstico en banco](procedimientos/diagnostico-banco.md) | [Banco](formularios/banco.html) | | **Instalar en sitio** (ICAVE, LZC, EIT, TIMSA) | [Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md) | [Instalación en campo](formularios/instalacion.html) | -| **Ver los tableros** (pendientes, ingeniería, producción, actas) | [Tableros](tableros/index.md) | — | +| **Ver los tableros** (ingeniería, producción, actas, inventario) | [Tableros](tableros/index.md) | — | | **Inventario** (existencias, entradas y salidas) | [Inventario](tableros/inventario.md) | — | | **Pedir una tarjeta o saber qué versión va** | [Tarjetas y hardware](hardware/index.md) | — | | **Descargar firmware** (por tarjeta, con SHA-256) | [Descargas](descargas/index.md) | [Reflasheo](formularios/reflasheo.html) | @@ -31,31 +129,6 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J | Saber qué tarjeta tengo en la mano | [Manuales por tarjeta](manuales/index.md) | — | | Entender cómo llegamos aquí | [Historia del proyecto](historia/index.md) | — | -## Tarjetas — estado al 19-oct-2026 +La lupa de arriba busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md). Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech -| Tarjeta | Estado | Pedido | Firmware | Dónde leer | -|---|---|---|---|---| -| V5.5 | **En campo.** 15 pzas fabricadas el 22-ago; en ensamble, taller y puertos. No se fabrica más. Hallazgo del 16-oct: el cargador queda limitado a ≈ 1 A de entrada (medir en banco). | — | 0.6.0 CAMPO · 0.6.3 en prueba de banco | [Manual V5](manuales/manual-tarjeta-V5.md) | -| V5.6 | **Casi lista.** T30 aplicado; falta la pasada 4, con un crítico nuevo: **C129 en VSYS de U6** (un 0603, sin mover piezas). Después Gerber/BOM/CPL y el piloto de 5. Falta también el firmware 0.7.0. Con Mean Well SD-15B externa. | **113** (piloto de 5 primero) | 0.7.0 (`board_v56.h`), no existe | [Qué es crítico](hardware/v56-clasificacion-19oct.md) · [Re-auditoría 19-oct](hardware/re-auditoria-19oct.md) · [Pasada 4](hardware/v56-v57-layout-pasada-4.md) | -| V5.7 | **NO LISTA.** Fuente a bordo (entrada 9–32 V). Pasada 3 hecha: ya replica la V5.6. Bloqueante: `/SYS5V` por In2 bajo K1; falta compactar la fuente U25. T30 + ≈ medio día de la pasada 4; después los 5 prototipos. | **327** (5 prototipos primero) | el mismo 0.7.0 | [Re-auditoría 19-oct, §4](hardware/re-auditoria-19oct.md#4-v57-punto-por-punto) | -| v6 | **NO LISTA**, sin cambio desde el 16-oct (solo 15 vías de tierra nuevas): potencia por capas internas, RF del módem por In1, sin barrenos. Plan recorrido una semana: fines de semana 24–25 oct y 31 oct–1 nov; piloto de 20 el lun 2-nov. | **130** + reemplazos (piloto de 20) | 0.7.x (`board_v6.h`), no existe | [Auditoría v6](hardware/v6-auditoria-produccion-16oct.md) · [Plan de fines de semana (rev. 1.1)](hardware/v6-plan-fines-de-semana.md) | -| V4 | En prueba y campo (Blackpill + Ethernet/serie). Sin producción nueva. | — | 0.6.0 CAMPO · 0.6.3 en prueba | [Manual V4](manuales/manual-tarjeta-V4.md) | -| V3 | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) | -| firmware | El binario se elige **por tarjeta**, nunca por número de versión. 0.6.3 en banco desde el 1-oct, no para campo. | | | [Descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) · [notas de versión](descargas/notas-de-version.md) | - -Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el plan: [Tablero de producción](tableros/produccion.md). - -## Hoy — 19 de octubre de 2026 - -- **V5.6 (tarde): qué es crítico y qué se puede quedar** ([clasificación](hardware/v56-clasificacion-19oct.md)). Hay un solo crítico, y es nuevo: U6 no tiene capacitor en sus pines VSYS (viene desde la V5.5). Se agrega C129 (10 µF 0603) sin mover piezas. Además hay que regresar 4 vías que se perdieron en el guardado de las 18:59. L7 y lo heredado de la V5.5 se pueden quedar para el piloto, y se miden en el banco. -- **Re-auditoría de las tres tarjetas** sobre la pasada 3 de Michelle: [Re-auditoría del 19-oct](hardware/re-auditoria-19oct.md). **V5.6 casi lista** (T30 + ≈ 1 h); **V5.7** con un bloqueante (`/SYS5V` por In2) y la fuente U25 por compactar; **v6** sin avance en sus 8 bloqueantes. -- **Script T30** listo y probado en copias con el DRC de KiCad: vías en paralelo, vías «micro», BOM/CPL, cajetín y stackup de V5.6 y V5.7. Lo corre Leo hoy: [pasada 4, §0](hardware/v56-v57-layout-pasada-4.md#0-antes-de-empezar-leo-10-min). -- **[Pasada 4](hardware/v56-v57-layout-pasada-4.md)** (Michelle): parte A (V5.6, ≈ 1 h) el martes 20; parte B (V5.7, ≈ medio día) el 20–21. Después, un solo pedido: 5 V5.6 + 5 V5.7 (≈ 21–22-oct). -- **Merge del lunes resuelto** (`240e2c3`; `.gitattributes` con `merge=binary`) y verificado objeto por objeto. Para que no se repita: el auto-sync de las 18:00 no debe subir archivos de KiCad, y una persona por tarjeta a la vez ([§2](hardware/re-auditoria-19oct.md#2-el-merge-del-lunes-que-paso-y-como-evitarlo)). -- **v6:** [plan de fines de semana, rev. 1.1](hardware/v6-plan-fines-de-semana.md): 24–25 oct y 31 oct–1 nov, piloto de 20 el lun 2-nov, G2 ≈ 22-nov. Las **pre-órdenes en JLC** (THVD1410DR, TPS7A2033PDBVR, J14) no esperan: hoy o mañana. -- **Decisiones de Leo (D-19-1…4):** puertos de V5.6/V5.7 (si van, entran al piloto), Dbk2, auto-sync y clase HV24: [§7](hardware/re-auditoria-19oct.md#7-decisiones-para-leo). -- **V5.7 sin Mean Well acepta 9–32 V** (máquinas de 12 y 24 V). Recomendado: TVS bidireccional para que la polaridad invertida no abra el fusible. -- **Jesús:** medir en una V5.5 el límite del cargador con TP1 ([§4.5 de la auditoría del 16-oct](hardware/auditoria-produccion-16oct.md#45-prueba-definitiva-con-tp1-clprog-cualquier-tarjeta-cualquier-corriente)): (TP1 ÷ 32) ÷ voltaje en RS1 ≈ 3.1 confirma el defecto. -- **Plan de 570 unidades** (113 / 327 / 130). Pendiente: la caja plástica del HBOX, donde el cable Ethernet no cabe. Tareas y responsables en [Pendientes](tableros/pendientes.md). - -Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech +
    diff --git a/docs/inicio/inicio.css b/docs/inicio/inicio.css new file mode 100644 index 0000000..c1cb8b5 --- /dev/null +++ b/docs/inicio/inicio.css @@ -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; } +} diff --git a/docs/inicio/inicio.js b/docs/inicio/inicio.js new file mode 100644 index 0000000..b50710e --- /dev/null +++ b/docs/inicio/inicio.js @@ -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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + const svg = (d, cls) => ''; + 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, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^)\s]*)\)/g, '$1'); + } + + 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 **negritas**, `código` y [ligas](/hardware/…/). 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) + '' + esc(d.titulo) + '' }), 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(); +})(); diff --git a/docs/stylesheets/helgen.css b/docs/stylesheets/helgen.css index 1cde448..b5b1377 100644 --- a/docs/stylesheets/helgen.css +++ b/docs/stylesheets/helgen.css @@ -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; diff --git a/docs/tableros/actividad.js b/docs/tableros/actividad.js new file mode 100644 index 0000000..103256a --- /dev/null +++ b/docs/tableros/actividad.js @@ -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 }; +})(); diff --git a/docs/tableros/index.md b/docs/tableros/index.md index e62439c..af9a5fb 100644 --- a/docs/tableros/index.md +++ b/docs/tableros/index.md @@ -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 | diff --git a/docs/tableros/ingenieria.md b/docs/tableros/ingenieria.md index 2982565..54109ea 100644 --- a/docs/tableros/ingenieria.md +++ b/docs/tableros/ingenieria.md @@ -8,19 +8,23 @@ hide:
    -

    Ingeniería

    V5.5 · V5.6 · V5.7 · v6 — estado al 19-oct-2026
    +

    Ingeniería

    V5.5 · V5.6 · V5.7 · v6
    Cómo se usa
      -
    1. 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í.
    2. +
    3. Arriba, la ruta a producción (los siete pasos de cada versión) y una tarjeta por versión de EinarBoard: estado, siguiente paso, responsables y sus documentos. Es texto fijo de docs/datos/proyecto.json, el mismo que usa la página de inicio: cuando cambie una decisión, pide a Leo o a Claude que lo actualice ahí.
    4. En medio, los pendientes de la pista Ingeniería: son los mismos del tablero de pendientes, filtrados. Puedes moverlos desde aquí.
    5. 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.
    6. Para una tarjeta nueva o un cambio de pedido, primero el pendiente, luego el trabajo: así queda el rastro de quién y cuándo.
    +

    Ruta a producción

    +

    Línea continua: hecho. Punteada: lo que falta. El anillo marca el paso en el que va cada tarjeta. En cada imagen, lo nuevo de la revisión va en blanco.

    +

    …

    +
    @@ -33,6 +37,8 @@ hide:

    Últimas decisiones

    Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.

      +
    • 21-oct (noche) — Pasada 5 revisada (6458c5d): C129 y C130 quedaron bien en las dos, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: el sensado ya no es Kelvin (el límite de entrada baja ≈ 3–4 %). Falta la pasada 6, ≈ 30 min por tarjeta; en la V5.7, además, girar C149/C150 (lazo de ≈ 56 mm²). v6: T25 y T28 aplicados sobre 9255b84 (falta el push de Leo); T28 cerró 2 de los 8 bloqueantes, pero D34 quedó a 13 mm del pin SW de U25 (lazo caliente de ≈ 115 mm², antes ≈ 47) y MH8 cae encima de D34. Pasada 5 revisada · Estado de la v6
    • +
    • 21-oct (tarde) — Auditoría de la pasada 4 de Michelle (HBox_PCB 85e7602 V5.6, 1416dcb V5.7). V5.6: la pasada dejó un corto nuevo (/MODEM_EN a GND en R106) y una pista de /SYS5V a 0.09 mm de una vía; el giro de U7 mejoró el nodo SW (L7 a 4 mm de U7). V5.7: ya sin el bloqueante de K1. D-21-1, aprobada por Leo: C130 de 100 nF en la entrada de U7, en las dos. En la tarde quedaron T32 (C130 en los esquemáticos) y T33 (el corto, la separación, la 2.ª vía de VSYS y las vías de potencia): DRC solo con los 3 errores de MOD1. Falta la pasada 5 de Michelle (V5.6 ≈ 1 h, V5.7 ≈ medio día); después, revisión del cobre y Gerber. Auditoría del 21-oct
    • 19-oct (19:30) — V5.6: qué es crítico y qué se puede quedar (archivo de trabajo de las 18:59, sobre T30). Un solo crítico, nuevo: U6 (LTC4040) no tiene capacitor en sus pines VSYS; el más cercano, C34, está a 11.5 mm (viene desde la V5.5). La hoja de datos lo pone por encima de todo en el layout. Se agrega C129 (10 µF 0603, C19702) junto a los pines 1/24, sin mover piezas. Regresar 2 vías de /MAIN5V y 2 de /3V3_MODEM que se perdieron al guardar. L7 lejos de U7 y lo heredado de la V5.5 (L6, L8, Dbk1) se quedan para el piloto, y se miden en el banco. Clasificación
    • 19-oct (17:10) — Re-auditoría V5.6 / V5.7 / v6 sobre la pasada 3 de Michelle (HBox_PCB 4f4681f). V5.6 casi lista: /BAT_PLUS fuera de capas internas, /3V3_MODEM a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto; faltan T30 + ≈ 1 h (L7/Dbk2, salida VSYS de U6, 3 vías, exclusiones del DRC). V5.7 NO LISTA: ya replica la V5.6, pero /SYS5V va 10.9 mm por In2 a 0.5 mm bajo K1 (bloqueante) y el lazo de entrada de U25 mide 52 mm². v6 sin avance en sus 8 bloqueantes (solo 15 vías GND): plan recorrido una semana, piloto de 20 el lun 2-nov; pre-órdenes de JLC ya. Script T30 probado en copias en la Mac (DRC sin infracciones nuevas, 0 sin conectar). Merge del lunes (40decf8) arreglado por Leo (240e2c3 + merge=binary) y verificado objeto por objeto. Decisiones para Leo: D-19-1 puertos (si van, entran al piloto), D-19-2 Dbk2, D-19-3 sacar KiCad del auto-sync de las 18:00, D-19-4 clase HV24. Re-auditoría · Pasada 4 · Plan v6 rev. 1.1
    • 16-oct (17:30) — Auditoría de producción de la v6: NO LISTA. Ruteo completo (2612891, DRC 0 errores), pero: U25 con el código del AP64350, sin barrenos, 5 V por In1/In2 a 0.5 mm, cargador con pistas de 0.5 mm y 1 vía en el EP, RF del módem por In1, 81 vías de tierra, stackup genérico. Scripts listos: T25 --set v6 y T28 (probados con DRC). Plan de fines de semana: layout el 17–18 y el 24–25 oct, pedido del piloto de 20 el 26-oct, firmware y banco hasta ≈ 15-nov. P7/P8 se quitan (decisión del 9-oct). Auditoría v6 · Plan
    • @@ -54,7 +60,7 @@ hide:
    -
    +

    Filosofía de diseño

    Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en Tarjetas y hardware.

      @@ -67,49 +73,52 @@ hide:
    + diff --git a/docs/tableros/kanban.js b/docs/tableros/kanban.js index 5ddd153..791ff6b 100644 --- a/docs/tableros/kanban.js +++ b/docs/tableros/kanban.js @@ -5,11 +5,20 @@ Uso: const kb = Kanban.montar(document.getElementById('kb'), { pista: 'ingenieria', // opcional: fija la pista (oculta su filtro y la precarga en "Nuevo") filtros: true, // barra de filtros (responsable / versión / buscar / pista / solo los míos) - compacto: false, // tarjetas más pequeñas (tableros que solo muestran un extracto) + compacto: false, // tarjetas sin ligas ni etiquetas (la página de inicio) botonNuevoEn: elemento, // opcional: dónde va el botón «Nuevo pendiente» (p. ej. el encabezado) - onCargado: (tareas, meta) => {} // callback después de cada carga + onCargado: (tareas, meta) => {}, // callback después de cada carga + // agregados el 21-oct (página de inicio y vista por persona): + vistas: true, // botones «Tablero · Por persona» (?vista=persona en la dirección) + limite: 4, // máximo de pendientes por columna; el resto, «N más» → enlaceMas + limitePersona: 3, // máximo por celda en la vista por persona + enlaceMas: '/tableros/pendientes/', // a dónde llevan los «N más» (sin él, se despliegan ahí) + nuevoPorColumna: true, // el «+ Nuevo» al pie de cada columna + barraEn: elemento, // los botones de la barra van dentro de este elemento (filtros: 'min') + onPintado: (visibles, info) => {} // después de cada pintado: lo que se ve con los filtros }); kb.recargar(); + filtros: true = barra completa · 'min' = solo vistas, «Solo los míos» y «Nuevo» · false = sin barra. Servidor: GET /api/tareas · POST /api/tareas · PATCH /api/tarea?id= · POST /api/tareas/orden · DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial. @@ -51,7 +60,8 @@ flecha: 'M5 12h14M13 6l6 6-6 6', unidad: 'M5 7.5 12 4l7 3.5v9L12 20l-7-3.5zM5 7.5l7 3.5 7-3.5M12 11v9', actualizar: 'M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4', - imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z' + imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z', + columnas: 'M5 4.5h14a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 18V6A1.5 1.5 0 0 1 5 4.5zM9.3 4.5v15M14.7 4.5v15' }; const icono = (n, cls) => ''; // Íconos de estado de las columnas (con relleno; colores de los tokens). @@ -66,14 +76,16 @@ const norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim().replace(/[^a-z0-9_\-.]/g, ''); let EQUIPO = {}; let equipoProm = null; + // junta los datos de cada persona sin borrar lo que ya se sabía (p. ej. el rol) + function fundir(mapa) { Object.keys(mapa || {}).forEach(id => { EQUIPO[id] = Object.assign({}, EQUIPO[id] || {}, mapa[id]); }); return EQUIPO; } function cargarEquipo() { if (!equipoProm) equipoProm = fetch('/api/equipo?todos=1', { cache: 'no-store' }).then(r => r.json()).then(j => { - const m = {}; (j.miembros || []).forEach(x => m[x.id] = { nombre: x.nombre, activo: x.activo !== false, area: x.area, tiene_login: x.tiene_login }); + const m = {}; (j.miembros || []).forEach(x => m[x.id] = { nombre: x.nombre, activo: x.activo !== false, area: x.area, rol: x.rol || '', tiene_login: x.tiene_login }); return m; - }).catch(() => ({})).then(m => { if (!Object.keys(m).length) RESPONSABLES.forEach(id => m[id] = { nombre: id, activo: true }); EQUIPO = Object.assign(EQUIPO, m); return EQUIPO; }); + }).catch(() => ({})).then(m => { if (!Object.keys(m).length && !Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => m[id] = { nombre: id, activo: true }); return fundir(m); }); return equipoProm; } - function usarEquipo(mapa) { if (mapa && Object.keys(mapa).length) EQUIPO = Object.assign(EQUIPO, mapa); if (!Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => EQUIPO[id] = { nombre: id, activo: true }); return EQUIPO; } + function usarEquipo(mapa) { if (mapa && Object.keys(mapa).length) fundir(mapa); if (!Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => EQUIPO[id] = { nombre: id, activo: true }); return EQUIPO; } const partesResp = t => String((t && t.responsable) || '').split(/[,;\/]+/).map(x => x.trim()).filter(Boolean); // nombres a mostrar: cada parte del campo responsable → nombre del miembro, o el texto tal cual function nombresResp(t, eq) { @@ -86,6 +98,12 @@ return partesResp(t).map(p => { const a = norm(p.replace(/\s+/g, '')), b = norm(p); return eq[a] ? a : (eq[b] ? b : null); }).filter(Boolean); } const libresResp = (t, eq) => { eq = eq || EQUIPO; return partesResp(t).filter(p => !eq[norm(p.replace(/\s+/g, ''))] && !eq[norm(p)]); }; + // nombre corto para frases («Leo movió…»): el primer nombre, con los apodos de la casa + const APODOS = { leopoldo: 'Leo' }; + function nombreCorto(id) { + const n = String((EQUIPO[id] || {}).nombre || id || '').trim().split(/\s+/)[0]; + return APODOS[norm(n)] || (EQUIPO[id] ? n : n.charAt(0).toUpperCase() + n.slice(1)); + } // ---- avatares: iniciales sobre el color de la persona function iniciales(nombre) { @@ -140,8 +158,28 @@ let tareas = [], admin = false, user = '', porId = {}; const q0 = new URLSearchParams(location.search); const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1' }; + const vistasOn = !!opts.vistas; + let vista = vistasOn && (q0.get('vista') === 'persona' || (!q0.get('vista') && opts.vista === 'persona')) ? 'persona' : 'tablero'; + const desplegadas = new Set(); // celdas de la vista por persona que ya muestran todo root.classList.add('kanban-root'); const contenedor = root.closest('.tb') || root; + // los filtros que vale la pena compartir viajan en la dirección (?mios=1, ?vista=persona) + function ponURL(k, v) { + if (opts.url === false) return; + const u = new URL(location.href); + if (v) u.searchParams.set(k, v); else u.searchParams.delete(k); + history.replaceState(null, '', u.pathname + u.search + u.hash); + } + // dirección del tablero completo con lo que se está viendo (para los «N más» de la página de inicio) + function enlaceCompleto(extra) { + const u = new URLSearchParams(); + if (filtro.mios) u.set('mios', '1'); + if (vista === 'persona') u.set('vista', 'persona'); + if (opts.pista) u.set('pista', opts.pista); + Object.entries(extra || {}).forEach(([k, v]) => v && u.set(k, v)); + const q = u.toString(); + return (opts.enlaceMas || '/tableros/pendientes/') + (q ? '?' + q : ''); + } // ---------- barra de filtros ---------- const barra = el('div', { class: 'kb-barra' }); @@ -151,16 +189,29 @@ const selVer = el('select', { title: 'Versión de tarjeta', 'aria-label': 'Versión de tarjeta' }); selVer.append(el('option', { value: '', text: 'Tarjeta: todas' })); VERSIONES.filter(v => v !== '—').forEach(v => selVer.append(el('option', { value: v, text: v }))); const selHechas = el('select', { title: 'Hechos', 'aria-label': 'Hechos' }); - [['recientes', 'Hechos: últimos 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t }))); + [['recientes', 'Hechos: 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t }))); const buscar = el('input', { type: 'search', placeholder: 'Buscar en pendientes', 'aria-label': 'Buscar en pendientes' }); const btnMios = el('button', { type: 'button', class: 'kb-solo', 'aria-pressed': String(filtro.mios), html: icono('persona') + 'Solo los míos' }); const btnNuevo = el('button', { type: 'button', class: 'kb-nuevo', html: icono('mas') + 'Nuevo pendiente' }); - if (opts.filtros !== false) { + const selVista = el('div', { class: 'kb-vistas', role: 'group', 'aria-label': 'Vista' }); + [['tablero', 'Tablero', 'columnas'], ['persona', 'Por persona', 'equipo']].forEach(([k, n, ic]) => { + const b = el('button', { type: 'button', 'data-vista': k, 'aria-pressed': String(vista === k), html: icono(ic) + n }); + b.addEventListener('click', () => { if (vista === k) return; vista = k; ponURL('vista', k === 'persona' ? 'persona' : null); pintar(); }); + selVista.append(b); + }); + const modo = opts.filtros === undefined ? true : opts.filtros; + if (modo) { + if (vistasOn) barra.append(selVista); barra.append(btnMios); - if (!opts.pista) barra.append(selPista); - barra.append(selResp, selVer, selHechas, buscar); + if (modo !== 'min') { if (!opts.pista) barra.append(selPista); barra.append(selResp, selVer, selHechas, buscar); } if (opts.botonNuevoEn) opts.botonNuevoEn.append(btnNuevo); else barra.append(btnNuevo); - root.append(barra); + if (opts.barraEn) { + // la página ya puso ahí sus propios elementos (p. ej. una liga): las vistas y «Solo los míos» + // van antes, «Nuevo pendiente» al final + const nodos = [...barra.childNodes]; + nodos.filter(n => n !== btnNuevo).reverse().forEach(n => opts.barraEn.prepend(n)); + if (nodos.includes(btnNuevo)) opts.barraEn.append(btnNuevo); + } else root.append(barra); } const aviso = el('div', { class: 'msg', role: 'status' }); root.append(aviso); function mensaje(txt, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = txt; clearTimeout(aviso._t); aviso._t = setTimeout(() => { aviso.className = 'msg'; }, bien ? 3500 : 8000); } @@ -172,7 +223,7 @@ ESTADOS.forEach(([k, nombre]) => { const c = el('div', { class: 'col e-' + k, 'data-estado': k }); c.append(el('h3', { html: ESTADO_SVG[k] + '' + nombre + '0' })); - if (k !== 'hecho') { + if (k !== 'hecho' && opts.nuevoPorColumna !== false) { const mas = el('button', { type: 'button', class: 'kb-mas', html: icono('mas') + 'Nuevo' }); mas.addEventListener('click', () => abrir(null, k)); c.append(mas); @@ -191,6 +242,8 @@ cols[k] = c; kb.append(c); }); root.append(kb); + const per = el('div', { class: 'kb-personas', hidden: '' }); + root.append(per); // ---------- filtrado y pintado ---------- function visibles() { @@ -235,13 +288,13 @@ const bl = bloqueosAbiertos(t); if (bl.length) c.append(el('div', { class: 'nota', html: icono('aviso') + 'Espera: ' + esc(bl.map(b => corto(b.titulo.split(':')[0], 42)).join(' · ')) + '' })); // ligas - if ((t.enlaces || []).length) { + if (!opts.compacto && (t.enlaces || []).length) { const e = el('div', { class: 'enl' }); t.enlaces.slice(0, 2).forEach(x => { const a = el('a', { href: x.url, html: icono('liga') + '' + esc(x.titulo || x.url) + '' }); a.addEventListener('click', ev => ev.stopPropagation()); e.append(a); }); if (t.enlaces.length > 2) e.append(el('span', { class: 'vacio', text: '+' + (t.enlaces.length - 2) + ' más' })); c.append(e); } - if ((t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); } + if (!opts.compacto && (t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); } // responsables y fecha const pie = el('div', { class: 'pie' }); pie.append(t.responsable ? avatares(t) : el('span', { class: 'vacio', text: 'Sin responsable' })); @@ -261,12 +314,21 @@ function pintar() { porId = {}; tareas.forEach(t => porId[t.id] = t); const vis = visibles(); - ESTADOS.forEach(([k]) => { - const c = cols[k]; c.querySelectorAll('.card').forEach(x => x.remove()); + selVista.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vista === vista))); + kb.hidden = vista === 'persona'; + per.hidden = vista !== 'persona'; + if (vista === 'persona') pintarPersonas(vis); + else ESTADOS.forEach(([k]) => { + const c = cols[k]; c.querySelectorAll('.card, .kb-mas-n').forEach(x => x.remove()); const lst = vis.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1)); + // con tope (página de inicio), en Hecho van primero los más recientes + if (opts.limite && k === 'hecho') lst.sort((a, b) => String(b.modificado_en || '').localeCompare(String(a.modificado_en || ''))); c.querySelector('.n').textContent = lst.length; const mas = c.querySelector('.kb-mas'); - lst.forEach(t => mas ? c.insertBefore(tarjeta(t), mas) : c.append(tarjeta(t))); + const pon = x => mas ? c.insertBefore(x, mas) : c.append(x); + const tope = !opts.limite ? Infinity : (typeof opts.limite === 'object' ? (opts.limite[k] || opts.limite.otros || 4) : opts.limite); + lst.slice(0, tope).forEach(t => pon(tarjeta(t))); + if (lst.length > tope) pon(el('a', { class: 'kb-mas-n', href: enlaceCompleto(), text: (lst.length - tope) + ' más' })); }); // responsables presentes en los datos (más los conocidos) selResp.querySelectorAll('option:not(:first-child)').forEach(o => o.remove()); @@ -278,6 +340,86 @@ btnMios.setAttribute('aria-pressed', String(filtro.mios)); btnMios.disabled = !user; btnMios.title = user ? 'Solo los pendientes de ' + ((EQUIPO[norm(user)] || {}).nombre || user) : 'No se sabe quién eres (falta X-Remote-User)'; + if (opts.onPintado) opts.onPintado(vis, { vista, filtro, user, admin }); + } + + // ---------- vista por persona: una fila por responsable, una columna por estado ---------- + function filtrosActivos() { return !!((filtro.pista && !opts.pista) || filtro.responsable || filtro.version || filtro.buscar.trim() || filtro.mios); } + function itemPersona(t) { + const b = el('button', { type: 'button', class: 'kb-p-item' + (t.vencida ? ' vencida' : '') + (t.estado === 'hecho' ? ' e-hecho' : ''), title: t.titulo }); + b.append(el('span', { class: 'kb-p-tit', text: t.titulo })); + const f = fechaDe(t); + if (f) b.append(el('span', { class: 'fecha ' + f.cls, text: f.texto })); + b.addEventListener('click', () => abrir(t)); + return b; + } + function pintarPersonas(vis) { + per.innerHTML = ''; + const grupos = new Map(); + const agrega = (clave, t, libre) => { if (!grupos.has(clave)) grupos.set(clave, { clave, libre, tareas: [] }); grupos.get(clave).tareas.push(t); }; + vis.forEach(t => { + const ids = idsResp(t), libres = libresResp(t); + ids.forEach(id => agrega(id, t)); + libres.forEach(x => agrega('libre:' + norm(x), t, x)); + if (!ids.length && !libres.length) agrega('_nadie', t, 'Sin responsable'); + }); + const abiertos = g => g.tareas.filter(t => t.estado !== 'hecho').length; + const nombre = g => g.libre || (EQUIPO[g.clave] || {}).nombre || g.clave; + const filas = [...grupos.values()].sort((a, b) => (abiertos(b) - abiertos(a)) || nombre(a).localeCompare(nombre(b))); + + const cab = el('div', { class: 'kb-p-fila kb-p-cab', 'aria-hidden': 'true' }); + cab.append(el('span', { text: 'Persona' })); + ESTADOS.forEach(([k, n]) => cab.append(el('span', { html: ESTADO_SVG[k] + '' + (k === 'hecho' && filtro.hechas === 'recientes' ? 'Hecho, 14 días' : n) + '' }))); + per.append(cab); + if (!filas.length) per.append(el('p', { class: 'vacio', text: 'Ningún pendiente con estos filtros.' })); + + const N = opts.limitePersona || 3; + filas.forEach(g => { + const fila = el('div', { class: 'kb-p-fila' }); + const quien = el('div', { class: 'kb-p-quien' }); + quien.append(g.libre ? avatar(null, g.libre) : avatar(g.clave)); + const m = EQUIPO[g.clave] || {}; + const ab = abiertos(g), bl = g.tareas.filter(t => t.estado === 'bloqueado').length, ve = g.tareas.filter(t => t.vencida).length; + const carga = ab ? ab + (ab === 1 ? ' abierto' : ' abiertos') + (bl ? ', ' + bl + (bl === 1 ? ' bloqueado' : ' bloqueados') : '') + (ve ? ', ' + ve + (ve === 1 ? ' vencido' : ' vencidos') : '') : 'Nada abierto'; + const txt = el('span', { class: 'kb-p-nom' }); + txt.append(el('b', { text: nombre(g) })); + if (m.rol) txt.append(el('span', { class: 'kb-p-rol', text: m.rol })); + txt.append(el('span', { class: 'kb-p-carga' + (ve ? ' mal' : (bl ? ' aviso' : '')), text: carga })); + quien.append(txt); + fila.append(quien); + ESTADOS.forEach(([k, n]) => { + const celda = el('div', { class: 'kb-p-celda e-' + k, 'data-col': n }); + let lst = g.tareas.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1)); + if (k === 'hecho') lst = lst.sort((a, b) => String(b.modificado_en || '').localeCompare(String(a.modificado_en || ''))); + const clave = g.clave + '|' + k; + const todo = desplegadas.has(clave); + (todo ? lst : lst.slice(0, N)).forEach(t => celda.append(itemPersona(t))); + if (!todo && lst.length > N) { + if (opts.enlaceMas) celda.append(el('a', { class: 'kb-mas-n', href: enlaceCompleto(g.libre ? {} : { responsable: g.clave }), text: (lst.length - N) + ' más' })); + else { + const b = el('button', { type: 'button', class: 'kb-mas-n', text: (lst.length - N) + ' más' }); + b.addEventListener('click', () => { desplegadas.add(clave); pintar(); }); + celda.append(b); + } + } + if (!lst.length) celda.classList.add('vacia'); + fila.append(celda); + }); + per.append(fila); + }); + + // quienes no tienen nada abierto (solo sin filtros: con filtros sería engañoso) + if (!filtrosActivos()) { + const con = new Set(filas.filter(g => abiertos(g)).map(g => g.clave)); + const libres = Object.entries(EQUIPO).filter(([id, m]) => m.activo !== false && !con.has(id) && m.nombre) + .sort((a, b) => a[1].nombre.localeCompare(b[1].nombre)); + if (libres.length) { + const f = el('div', { class: 'kb-p-libres' }); + f.append(el('span', { class: 'kb-p-libres-t', text: 'Sin pendientes abiertos' })); + libres.forEach(([id]) => f.append(el('span', { class: 'persona' }, avatar(id), document.createTextNode(nombreCorto(id))))); + per.append(f); + } + } } async function recargar() { @@ -416,19 +558,16 @@ selVer.addEventListener('change', () => { filtro.version = selVer.value; pintar(); }); selHechas.addEventListener('change', () => { filtro.hechas = selHechas.value; pintar(); }); buscar.addEventListener('input', () => { filtro.buscar = buscar.value; pintar(); }); - btnMios.addEventListener('click', () => { - filtro.mios = !filtro.mios; pintar(); - const u = new URL(location.href); if (filtro.mios) u.searchParams.set('mios', '1'); else u.searchParams.delete('mios'); - history.replaceState(null, '', u.pathname + u.search + u.hash); - }); + btnMios.addEventListener('click', () => { filtro.mios = !filtro.mios; ponURL('mios', filtro.mios ? '1' : null); pintar(); }); btnNuevo.addEventListener('click', () => abrir(null)); if (q0.get('pista') && !opts.pista) { selPista.value = q0.get('pista'); filtro.pista = q0.get('pista'); } if (q0.get('responsable')) { filtro.responsable = q0.get('responsable').toLowerCase(); } if (q0.get('id')) setTimeout(() => { const t = porId[q0.get('id')]; if (t) abrir(t); }, 600); + if (vistasOn) cargarEquipo().then(() => { if (vista === 'persona' && tareas.length) pintar(); }); recargar(); - return { recargar, abrir, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; } }; + return { recargar, abrir, pintar, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; }, get vista() { return vista; } }; } - window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, avatar, avatares, icono, fechaDe, get equipo() { return EQUIPO; } }; + window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, nombreCorto, avatar, avatares, icono, fechaDe, fechaCorta, get equipo() { return EQUIPO; } }; })(); diff --git a/docs/tableros/pendientes.md b/docs/tableros/pendientes.md index 21182ec..ccb4b01 100644 --- a/docs/tableros/pendientes.md +++ b/docs/tableros/pendientes.md @@ -22,6 +22,7 @@ hide:
  • Columna Bloqueado: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.
  • Al terminar, muévelo a Hecho. No lo borres: solo el administrador borra, y el historial sirve de bitácora.
  • Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.
  • +
  • Por persona muestra una fila por responsable con sus pendientes en cada estado: sirve para ver quién está cargado y quién está libre. La dirección (?vista=persona, ?mios=1) se puede compartir tal cual.
  • @@ -32,6 +33,7 @@ hide: (function () { Kanban.montar(document.getElementById('kb'), { filtros: true, + vistas: true, botonNuevoEn: document.getElementById('acciones-kb'), onCargado: (tareas, meta) => { const abiertas = tareas.filter(t => t.estado !== 'hecho').length; diff --git a/docs/tableros/ruta.js b/docs/tableros/ruta.js new file mode 100644 index 0000000..c48deff --- /dev/null +++ b/docs/tableros/ruta.js @@ -0,0 +1,96 @@ +/* Ruta a producción: una fila por versión de EinarBoard y sus siete pasos + (esquemático → layout → auditoría → Gerber → piloto → banco → pedido). + Los datos salen de /datos/proyecto.json («tarjetas» y «etapas»); este archivo solo dibuja. + Lo usan la página de inicio y el tablero de Ingeniería. + + Uso: Ruta.montar(contenedor, datos, { enlace: t => '/tableros/ingenieria/#' + t.clave }); + + Cómo se lee: línea continua = hecho; punteada = lo que falta; el anillo de color marca el paso + en el que va cada tarjeta (cian = en curso, coral = detenida). En el teléfono los textos de cada + paso se esconden y queda una línea con el paso actual y el que sigue. + Estilos: tableros/tablero.css (sección «Ruta a producción»). */ +(function () { + const TONO = { hecho: 'Lista', ahora: 'En curso', aviso: 'Con pendientes', bloqueado: 'Detenida' }; + const ESTADO_PASO = { hecho: 'hecho', ahora: 'en curso', bloqueado: 'detenido', falta: 'falta' }; + const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' }; + + const el = (t, a, ...k) => { + const e = document.createElement(t); + if (a) for (const p in a) { if (p === 'text') e.textContent = a[p]; else if (p === 'html') e.innerHTML = a[p]; else e.setAttribute(p, a[p]); } + k.forEach(c => c != null && e.append(c)); + return e; + }; + + // «Layout · Pasada 5 · Michelle, ≈ 1 h» + const describe = (etapa, p) => [etapa, p[1], p[2]].filter(Boolean).join(' · '); + + function resumen(t, etapas) { + const pasos = t.pasos || []; + const i = pasos.findIndex(p => p[0] === 'ahora' || p[0] === 'bloqueado'); + if (i < 0) { + if (pasos.length && pasos.every(p => p[0] === 'hecho')) return 'Todos los pasos hechos.'; + const j = pasos.findIndex(p => p[0] === 'falta'); + return j < 0 ? '' : 'Sigue: ' + describe(etapas[j], pasos[j]); + } + let s = (pasos[i][0] === 'bloqueado' ? 'Detenida en ' : 'Ahora: ') + describe(etapas[i], pasos[i]); + const j = pasos.findIndex((p, k) => k > i && p[0] === 'falta'); + if (j > 0) s += '. Sigue: ' + [etapas[j], pasos[j][1]].filter(Boolean).join(' · '); + return s + '.'; + } + + function montar(cont, datos, opts) { + opts = opts || {}; + const etapas = datos.etapas || []; + const tarjetas = datos.tarjetas || []; + cont.innerHTML = ''; + const caja = el('div', { class: 'ruta', style: '--ruta-pasos:' + etapas.length }); + + const cab = el('div', { class: 'ruta-fila ruta-cab', 'aria-hidden': 'true' }); + const nombres = el('div', { class: 'ruta-pasos-cab' }); + etapas.forEach(e => nombres.append(el('span', { text: e }))); + cab.append(el('span', { class: 'ruta-cab-t', text: 'Tarjeta' }), nombres); + caja.append(cab); + + tarjetas.forEach(t => { + const fila = el('div', { class: 'ruta-fila tono-' + (t.tono || 'falta'), role: 'group', 'aria-label': 'EinarBoard ' + t.version + ': ' + (t.estado || '') }); + if (opts.anclas) fila.id = t.clave; + + const href = opts.enlace ? opts.enlace(t) : (t.liga || '#'); + const info = el('a', { class: 'ruta-tarjeta', href }); + if (t.imagen) info.append(el('img', { src: t.imagen, alt: t.alt || ('EinarBoard ' + t.version), width: '420', height: '300', loading: 'lazy', decoding: 'async' })); + const txt = el('span', { class: 'ruta-info' }); + txt.append(el('span', { class: 'ruta-ver' }, + el('span', { class: BADGE[t.version] || 'badge-v4', text: t.version }), + el('span', { class: 'ruta-estado', text: t.estado || TONO[t.tono] || '' }))); + if (t.nuevo) txt.append(el('span', { class: 'ruta-nuevo', text: t.nuevo })); + info.append(txt); + fila.append(info); + + const pasos = t.pasos || []; + const lista = el('ol', { class: 'ruta-pasos' }); + pasos.forEach((p, i) => { + const est = p[0] || 'falta'; + const llega = est === 'hecho' || est === 'ahora' || est === 'bloqueado'; + const sale = est === 'hecho' && pasos[i + 1] && pasos[i + 1][0] !== 'falta'; + const cls = ['ruta-paso', 'p-' + est]; + if (i === 0) cls.push('primero'); + if (i === pasos.length - 1) cls.push('ultimo'); + if (llega) cls.push('entra'); + if (sale || (est === 'hecho' && i === pasos.length - 1)) cls.push('sale'); + const li = el('li', { class: cls.join(' '), title: describe(etapas[i], p) + ' (' + ESTADO_PASO[est] + ')' }); + li.append(el('span', { class: 'ruta-punto', 'aria-hidden': 'true' })); + li.append(el('span', { class: 'vh', text: (etapas[i] || '') + ', ' + ESTADO_PASO[est] + '. ' })); + li.append(el('span', { class: 'ruta-txt', text: p[1] || '' })); + if (p[2]) li.append(el('span', { class: 'ruta-sub', text: p[2] })); + lista.append(li); + }); + fila.append(lista); + fila.append(el('p', { class: 'ruta-resumen', text: resumen(t, etapas) })); + caja.append(fila); + }); + cont.append(caja); + return caja; + } + + window.Ruta = { montar, resumen }; +})(); diff --git a/docs/tableros/tablero.css b/docs/tableros/tablero.css index 4f64d7e..13394ff 100644 --- a/docs/tableros/tablero.css +++ b/docs/tableros/tablero.css @@ -245,7 +245,7 @@ /* ---- Tablero de pendientes ---------------------------------------------- */ .tb .kb-barra { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 14px; } .tb .kb-barra select, .tb .kb-barra input { padding: 7px 10px; font-size: 13.5px; } -.tb .kb-barra input[type="search"] { flex: 0 1 210px; min-width: 140px; } +.tb .kb-barra input[type="search"] { flex: 1 1 150px; min-width: 130px; max-width: 280px; } .tb .kb-barra .cnt { color: var(--hg-texto-3); font-size: 13px; } .tb .kb-barra .kb-nuevo { margin-left: auto; } .tb .kb-solo { @@ -338,6 +338,7 @@ .tb .card .mover { display: flex; gap: 6px; align-items: center; } .tb .card .mover select { padding: 5px 8px; font-size: 13px; flex: 1; } @media (min-width: 981px) { .tb .card .mover { display: none; } .tb .kb.movil .card .mover { display: flex; } } +.tb .kb.compacto .card .mover { display: none; } /* el extracto del inicio: se mueve desde el detalle o el tablero completo */ /* ---- Diálogo (detalle y alta) -------------------------------------------- */ .tb dialog { @@ -524,6 +525,147 @@ .tb .visita .cl { font-weight: 700; } .tb .marca-unidad { border-radius: 10px; padding: 10px 14px; margin: 0 0 14px; font-size: 14.5px; } +/* ---- Vistas del tablero y «N más» (21-oct) -------------------------------- */ +.tb .kb-vistas { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--hg-linea-2); border-radius: 10px; background: var(--hg-plano-2); } +.tb .kb-vistas button { background: transparent; border: 0; color: var(--hg-texto-2); font-size: 13.5px; font-weight: 500; padding: 5px 11px; border-radius: 7px; } +.tb .kb-vistas button[aria-pressed="true"] { background: var(--hg-elevado); color: var(--hg-texto-fuerte); font-weight: 600; } +.tb .kb-vistas button:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); } +.tb .kb-vistas .i { width: 15px; height: 15px; } +.tb .kb-mas-n { align-self: flex-start; padding: 4px; font-size: 13px; color: var(--hg-texto-2); text-decoration: none; } +.tb a.kb-mas-n:hover { color: var(--hg-liga-hover); text-decoration: underline; } +.tb button.kb-mas-n { background: none; border: 0; font-weight: 500; } +.tb button.kb-mas-n:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); } +.tb [hidden] { display: none !important; } + +/* ---- Vista por persona: una fila por responsable --------------------------- */ +.tb .kb-personas { border: 1px solid var(--hg-linea); border-radius: 12px; background: var(--hg-plano-2); } +.tb .kb-p-fila { display: grid; grid-template-columns: minmax(200px, 1.15fr) repeat(4, minmax(0, 1fr)); gap: 0 14px; align-items: start; padding: 12px 16px; border-top: 1px solid var(--hg-linea); } +.tb .kb-p-fila:first-child { border-top: 0; } +.tb .kb-p-cab { padding-top: 11px; padding-bottom: 11px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); } +.tb .kb-p-cab > span { display: inline-flex; align-items: center; gap: 7px; } +.tb .kb-p-cab svg { width: 15px; height: 15px; flex: none; } +.tb .kb-p-quien { display: flex; gap: 10px; align-items: flex-start; min-width: 0; } +.tb .kb-p-quien .av { width: 30px; height: 30px; font-size: 11px; } +.tb .kb-p-nom { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; } +.tb .kb-p-nom b { font-weight: 600; color: var(--hg-texto-fuerte); } +.tb .kb-p-rol { font-size: 12.5px; color: var(--hg-texto-3); } +.tb .kb-p-carga { margin-top: 2px; font-size: 12.5px; color: var(--hg-texto-2); } +.tb .kb-p-carga.mal { color: var(--hg-bloqueado); } +.tb .kb-p-carga.aviso { color: var(--hg-aviso); } +.tb .kb-p-celda { display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.tb button.kb-p-item { + display: flex; flex-direction: column; align-items: stretch; gap: 3px; + width: 100%; padding: 8px 10px; border-radius: 8px; + border: 1px solid var(--hg-linea); background: var(--hg-plano); color: var(--hg-texto); + font-size: 13.5px; font-weight: 500; line-height: 1.35; text-align: left; white-space: normal; +} +.tb button.kb-p-item:hover:not(:disabled) { filter: none; border-color: var(--hg-linea-2); color: var(--hg-texto-fuerte); } +.tb button.kb-p-item.vencida { border-color: color-mix(in srgb, var(--hg-bloqueado) 45%, var(--hg-linea)); } +.tb button.kb-p-item.e-hecho { color: var(--hg-texto-2); } +.tb .kb-p-tit { overflow-wrap: anywhere; } +.tb .kb-p-item .fecha { font-size: 12px; font-weight: 400; color: var(--hg-texto-3); } +.tb .kb-p-item .fecha.hoy { color: var(--hg-acento); font-weight: 600; } +.tb .kb-p-item .fecha.mal { color: var(--hg-bloqueado); font-weight: 600; } +.tb .kb-p-libres { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 12px 16px; border-top: 1px solid var(--hg-linea); font-size: 13px; color: var(--hg-texto-2); } +.tb .kb-p-libres-t { color: var(--hg-texto-3); margin-right: 2px; } +@media (max-width: 900px) { + .tb .kb-p-cab { display: none; } + .tb .kb-p-cab + .kb-p-fila { border-top: 0; } + .tb .kb-p-fila { grid-template-columns: 1fr 1fr; gap: 10px 12px; } + .tb .kb-p-quien { grid-column: 1 / -1; } + .tb .kb-p-celda::before { content: attr(data-col); font-size: 12px; font-weight: 600; color: var(--hg-texto-3); } + .tb .kb-p-celda.vacia { display: none; } +} +@media (max-width: 520px) { .tb .kb-p-fila { grid-template-columns: 1fr; } } + +/* ---- Ruta a producción (inicio e Ingeniería) ------------------------------- + Línea continua = hecho (blanco en oscuro, tinta en claro); punteada = lo que falta; + anillo cian = el paso en curso; coral = detenido. */ +.tb .ruta { border: 1px solid var(--hg-linea); border-radius: 14px; background: var(--hg-plano-2); padding: 2px 18px; } +.tb .ruta-fila { display: grid; grid-template-columns: minmax(230px, 1.4fr) minmax(0, 5fr); gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--hg-linea); } +.tb .ruta-fila:first-child { border-top: 0; } +.tb .ruta-cab { padding: 12px 0 8px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); align-items: end; } +.tb .ruta-cab-t { color: var(--hg-texto-3); font-weight: 500; } +/* (Material pone display: flow-root a .md-typeset ol:not([hidden]); por eso el selector largo) */ +.tb .ruta-pasos-cab, +.md-typeset .tb ol.ruta-pasos:not([hidden]) { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(var(--ruta-pasos, 7), minmax(0, 1fr)); } +.tb .ruta-pasos-cab span { text-align: center; padding: 0 4px; } +.md-typeset .tb ol.ruta-pasos { list-style: none; margin: 0; padding: 0; } +.md-typeset .tb .ruta-pasos li { margin: 0; } +.tb .ruta-paso { position: relative; display: flex; flex-direction: column; align-items: center; min-width: 0; padding: 0 4px; text-align: center; } +.tb .ruta-paso::before, +.tb .ruta-paso::after { content: ""; position: absolute; top: 11px; width: 50%; border-top: 1px dashed var(--hg-linea-2); } +.tb .ruta-paso::before { left: 0; } +.tb .ruta-paso::after { right: 0; } +.tb .ruta-paso.entra::before, +.tb .ruta-paso.sale::after { top: 10px; border-top: 3px solid var(--hg-ruta-linea); } +.tb .ruta-paso.primero::before, +.tb .ruta-paso.ultimo::after { display: none; } +.tb .ruta-punto { position: relative; z-index: 1; box-sizing: border-box; width: 16px; height: 16px; margin: 3px 0 8px; border-radius: 50%; border: 2px solid var(--hg-linea-2); background: var(--hg-plano-2); } +.tb .p-hecho .ruta-punto { border: 3.5px solid var(--hg-ruta); } +.tb .p-ahora .ruta-punto, +.tb .p-bloqueado .ruta-punto { width: 22px; height: 22px; margin: 0 0 5px; border: 4px solid var(--hg-acento); box-shadow: 0 0 0 5px var(--hg-halo-curso); } +.tb .p-bloqueado .ruta-punto { border-color: var(--hg-bloqueado); box-shadow: 0 0 0 5px var(--hg-halo-bloqueado); } +.tb .ruta-txt { font-size: 13px; line-height: 1.3; color: var(--hg-texto-2); overflow-wrap: anywhere; } +.tb .p-hecho .ruta-txt { color: var(--hg-texto-suave); } +.tb .p-ahora .ruta-txt { color: var(--hg-acento); font-weight: 700; } +.tb .p-bloqueado .ruta-txt { color: var(--hg-bloqueado); font-weight: 700; } +.tb .ruta-sub { margin-top: 2px; font-size: 12px; line-height: 1.3; color: var(--hg-texto-3); overflow-wrap: anywhere; } +.tb a.ruta-tarjeta { display: flex; gap: 12px; align-items: center; min-width: 0; color: inherit; text-decoration: none; } +.tb a.ruta-tarjeta img { flex: none; width: 100px; height: auto; aspect-ratio: 7 / 5; object-fit: cover; border-radius: 7px; border: 1px solid var(--hg-linea); background: #0E1524; filter: brightness(1.2) contrast(1.05); } +.tb a.ruta-tarjeta:hover img { border-color: var(--hg-linea-2); } +.tb a.ruta-tarjeta:hover .ruta-nuevo { color: var(--hg-texto-suave); } +.tb .ruta-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; } +.tb .ruta-ver { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } +.tb .ruta-estado { font-size: 14px; font-weight: 700; color: var(--hg-texto); } +.tb .tono-hecho .ruta-estado { color: var(--hg-texto-fuerte); } +.tb .tono-ahora .ruta-estado { color: var(--hg-acento); } +.tb .tono-aviso .ruta-estado { color: var(--hg-aviso); } +.tb .tono-bloqueado .ruta-estado { color: var(--hg-bloqueado); } +.tb .ruta-nuevo { font-size: 13px; line-height: 1.35; color: var(--hg-texto-2); } +.md-typeset .tb p.ruta-resumen { display: none; } +@media (max-width: 1000px) { + .tb .ruta-fila { grid-template-columns: 1fr; gap: 12px; } + .tb .ruta-cab { display: none; } + .tb .ruta-cab + .ruta-fila { border-top: 0; } +} +@media (max-width: 600px) { + .tb .ruta { padding: 2px 14px; } + .tb .ruta-txt, .tb .ruta-sub { display: none; } + .tb .ruta-punto { margin-bottom: 0; } + .md-typeset .tb p.ruta-resumen { display: block; margin: 0; font-size: 13.5px; line-height: 1.4; color: var(--hg-texto-suave); } +} + +/* ---- Actividad (inicio y Bitácora) ------------------------------------------ */ +.md-typeset .tb ol.act-lista { list-style: none; margin: 0; padding: 0; } +.md-typeset .tb .act-lista li { margin: 0; } +.tb .act { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1px 10px; padding: 9px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 14px; line-height: 1.4; } +.tb .act:first-child { border-top: 0; } +.tb .act .av { grid-row: span 2; width: 26px; height: 26px; margin-top: 1px; font-size: 10px; } +.tb .act-t { color: var(--hg-texto-suave); overflow-wrap: anywhere; } +.tb .act-t b { font-weight: 600; color: var(--hg-texto-fuerte); } +.tb .act-t a { color: var(--hg-texto); text-decoration: none; } +.tb .act-t a:hover { color: var(--hg-liga-hover); text-decoration: underline; } +.tb .act-nota { display: block; margin-top: 4px; padding: 6px 9px; border-radius: 8px; background: var(--hg-plano); font-size: 13px; color: var(--hg-texto-2); } +.tb .act-res { padding: 1px 7px; border-radius: 5px; font-size: 12px; font-weight: 600; background: var(--hg-hecho-bg); color: var(--hg-hecho); white-space: nowrap; } +.tb .act-res.mal { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); } +.tb .act-f { grid-column: 2; font-size: 12.5px; color: var(--hg-texto-3); } +.md-typeset .tb h3.act-dia { margin: 18px 0 2px; font-family: var(--hg-fuente); font-size: 13px; font-weight: 600; color: var(--hg-texto-3); } +.md-typeset .tb h3.act-dia:first-child { margin-top: 0; } +.tb .act-mas { display: inline-block; margin-top: 8px; font-size: 13px; } + +/* ---- Ingeniería: la ruta arriba; Bitácora: página de lectura ------------------- */ +.md-typeset .tb h2.ing-h { margin: 4px 0 4px; } +.tb .ing-ruta { margin-bottom: 16px; } +.tb .versiones .ver { scroll-margin-top: 4.5rem; } +.md-typeset .tb-bitacora h2 { margin: 30px 0 10px; } +.md-typeset .tb-bitacora h3 { margin: 24px 0 8px; font-family: var(--hg-fuente-titulo); font-size: 18px; font-weight: 600; } +.tb-bitacora #bit-actividad { padding: 8px 16px 12px; border: 1px solid var(--hg-linea); border-radius: 12px; background: var(--hg-plano-2); } +.md-typeset .tb-bitacora dd { margin-bottom: 6px; } + +/* texto solo para lectores de pantalla */ +.tb .vh { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } + @media print { .tb .tb-cab__acc, .tb .buscar, .tb button, .tb .kb-barra, .tb details.tb-como { display: none !important; } .tb section.bloque, .tb .kpi, .tb .card, .tb .cel, .tb .cadena .paso { background: #fff !important; border-color: #999 !important; color: #000 !important; } diff --git a/hooks/documentos.py b/hooks/documentos.py new file mode 100644 index 0000000..159535e --- /dev/null +++ b/hooks/documentos.py @@ -0,0 +1,52 @@ +"""Documentos con fecha para la página de inicio («Documentos recientes»), 21-oct-2026. + +Cada página .md con `date:` en su encabezado (AAAA-MM-DD) entra en `datos/documentos.json` +del sitio construido, del más nuevo al más viejo. Lo lee docs/inicio/inicio.js. +Las páginas que en el menú están bajo «Historia» (lo superado) no entran. +No toca ningún archivo de docs/: el JSON solo existe en site/. +""" +import datetime +import json +import logging +import os +import re + +log = logging.getLogger("mkdocs.hooks.documentos") +_DOCS = [] +_FECHA = re.compile(r"^\d{4}-\d{2}-\d{2}$") +# fecha al final del título: «… (21-oct-2026)», «… · 21-oct-2026», «… — 16 oct» +_COLA = re.compile(r"\s*[(·—–-]\s*\d{1,2}[-\s](?:ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)[a-z]*\.?(?:[-\s]\d{4})?\s*\)?\s*$", re.I) +# o un paréntesis final con fecha: «… (19-oct-2026, tarde)», «… (24-oct → 22-nov-2026)» +_PAREN = re.compile(r"\s*\([^()]*\d{1,2}[-\s](?:ene|feb|mar|abr|may|jun|jul|ago|sep|oct|nov|dic)[^()]*\)\s*$", re.I) + + +def on_pre_build(config, **kwargs): + _DOCS.clear() + + +def _fecha(valor): + if isinstance(valor, (datetime.date, datetime.datetime)): + return valor.isoformat()[:10] + texto = str(valor or "")[:10] + return texto if _FECHA.match(texto) else None + + +def on_page_markdown(markdown, page, config, files, **kwargs): + fecha = _fecha(page.meta.get("date")) + if not fecha: + return markdown + if any(getattr(a, "title", "") == "Historia" for a in (page.ancestors or [])): + return markdown + titulo = str(page.meta.get("title") or page.title or "").strip() + titulo = _COLA.sub("", _PAREN.sub("", titulo)).strip() or titulo + _DOCS.append({"titulo": titulo, "fecha": fecha, "url": "/" + page.url}) + return markdown + + +def on_post_build(config, **kwargs): + docs = sorted(_DOCS, key=lambda d: (d["fecha"], d["titulo"]), reverse=True) + destino = os.path.join(config["site_dir"], "datos", "documentos.json") + os.makedirs(os.path.dirname(destino), exist_ok=True) + with open(destino, "w", encoding="utf-8") as f: + json.dump({"total": len(docs), "documentos": docs[:60]}, f, ensure_ascii=False, indent=1) + log.info("documentos.json: %d documentos con fecha", len(docs)) diff --git a/mkdocs.yml b/mkdocs.yml index 99fc7f5..81c5a53 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ extra_javascript: hooks: - hooks/iconos.py # íconos del menú lateral (también para páginas que llegan por sync-docs.sh) - hooks/frontmatter.py # encabezados YAML con «: » sin comillas: se leen igual (no toca los archivos) + - hooks/documentos.py # «Documentos recientes» del inicio: site/datos/documentos.json (páginas con date:) plugins: - search: @@ -150,6 +151,7 @@ nav: - hardware/index.md - Manuales por tarjeta: manuales/index.md - Auditorías y pasadas: + - 21 oct · Estado v6 (T25 + T28): hardware/v6-estado-21oct.md - 21 oct · Auditoría pasada 4: hardware/v56-v57-auditoria-21oct.md - 19 oct · Críticos V5.6: hardware/v56-clasificacion-19oct.md - 19 oct · Re-auditoría: hardware/re-auditoria-19oct.md diff --git a/overrides/inicio.html b/overrides/inicio.html new file mode 100644 index 0000000..5247498 --- /dev/null +++ b/overrides/inicio.html @@ -0,0 +1,17 @@ +{#- + Página de inicio (docs/index.md con `template: inicio.html`, 21-oct-2026). + Extiende la plantilla de los tableros (tablero.css y kanban.js, porque el inicio lleva el + tablero de pendientes) y agrega lo suyo: inicio/inicio.css, la ruta a producción + (tableros/ruta.js), la actividad (tableros/actividad.js) e inicio/inicio.js, que llena la página + con docs/datos/proyecto.json y con el servidor (/api/...). +-#} +{% extends "tablero.html" %} + +{% block extrahead %} + {{ super() }} + + + + + +{% endblock %} diff --git a/overrides/tablero.html b/overrides/tablero.html index 71b5aa9..445071d 100644 --- a/overrides/tablero.html +++ b/overrides/tablero.html @@ -2,7 +2,8 @@ Plantilla de los tableros (Pendientes, Ingeniería, Producción, Actas, Inventario, Sitios, Equipo, Expediente y Visitas de campo). Se elige con `template: tablero.html` en el front matter de la página. - - Usa todo el ancho (sin la columna de texto de los documentos). + - Usa todo el ancho (sin la columna de texto de los documentos). Una página puede pedir + otro ancho en su front matter, p. ej. `ancho: 52rem` (la Bitácora) o `ancho: 64rem` (inicio). - Carga tableros/tablero.css (estilos del tablero, sobre los tokens de helgen.css) y tableros/kanban.js (el tablero de pendientes) antes del contenido, para que el script de cada página pueda llamar a Kanban.montar(...). @@ -12,6 +13,6 @@ {% block extrahead %} {{ super() }} - + {% endblock %} diff --git a/scripts/sync-docs.sh b/scripts/sync-docs.sh index 55150b4..a437a35 100755 --- a/scripts/sync-docs.sh +++ b/scripts/sync-docs.sh @@ -130,6 +130,10 @@ declare -a MAP=( "03-Architecture-Hardware/2026-10-21_Drogheda_Auditoria-Pasada-4_V5.6-V5.7_ES_v1.0.md|docs/hardware/v56-v57-auditoria-21oct.md" "03-Architecture-Hardware/img/2026-10-21_Drogheda_Auditoria-Pasada-4-V56-V57_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_Auditoria-Pasada-4-V56-V57_v1.0.png" "03-Architecture-Hardware/img/2026-10-21_Drogheda_T33-Arreglos-y-Pendientes_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_T33-Arreglos-y-Pendientes_v1.0.png" + # 2026-10-21 (noche): estado de la v6 con T25 + T28 aplicados y revisión de la pasada 5 de V5.6/V5.7 + "03-Architecture-Hardware/2026-10-21_Drogheda_Estado-v6-T25-T28_ES_v1.0.md|docs/hardware/v6-estado-21oct.md" + "03-Architecture-Hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png" + "03-Architecture-Hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png" ) n=0 diff --git a/scripts/tarjetas/README.md b/scripts/tarjetas/README.md new file mode 100644 index 0000000..0e756d2 --- /dev/null +++ b/scripts/tarjetas/README.md @@ -0,0 +1,22 @@ +# Imágenes de las tarjetas (portada y ruta a producción) + +Las imágenes de `docs/img/tarjetas/` (la franja de la portada del inicio y una miniatura por versión) se +dibujan con el cobre real de cada `.kicad_pcb`, en blanco sobre el fondo del sitio. Lo nuevo de cada +revisión (las redes que no estaban en la versión anterior) va en blanco sólido; el resto, tenue. +Decisión de Leo del 21-oct-2026: «la tarjeta en blanco, no en verde». + +Las del 21-oct salieron del cobre del 20-oct. Para rehacerlas (en la fase 3 esto lo hará `sync-docs.sh`): + +```bash +cd hbox-docs/scripts/tarjetas +K=../../../03-Architecture-Hardware/kicad +python3 leer_pcb.py $K/hbox-watchdog-v5.kicad_pcb /tmp/tarjetas/pcb_v56.json +python3 leer_pcb.py $K/hbox-watchdog-v57.kicad_pcb /tmp/tarjetas/pcb_v57.json +python3 leer_pcb.py $K/hbox-watchdog-v6.kicad_pcb /tmp/tarjetas/pcb_v6.json +# la V5.5 sale del commit con el que se pidió (22-ago): git show :hbox-watchdog-v5.kicad_pcb > v55.kicad_pcb +python3 leer_pcb.py v55.kicad_pcb /tmp/tarjetas/pcb_v55.json +python3 tarjetas_blanco.py /tmp/tarjetas /tmp/tarjetas/svg +python3 rasterizar.py ../../docs/img/tarjetas /tmp/tarjetas/svg/*.svg # necesita Pillow y Playwright (Chromium) +``` + +Solo con KiCad cerrado y sobre archivos ya subidos (no a media pasada de layout). diff --git a/scripts/tarjetas/leer_pcb.py b/scripts/tarjetas/leer_pcb.py new file mode 100644 index 0000000..b08536c --- /dev/null +++ b/scripts/tarjetas/leer_pcb.py @@ -0,0 +1,63 @@ +"""Lee un .kicad_pcb (KiCad 8/9/10) sin KiCad y guarda en JSON lo que hace falta para dibujarlo: +pistas, arcos, vías, pads (con su red y su capa), contorno y zonas rellenadas. +Uso: python3 leer_pcb.py tarjeta.kicad_pcb pcb_v56.json""" +import re, json, math, sys +src = open(sys.argv[1], encoding='utf-8').read() +tok_re = re.compile(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()"]+') +def parse(s): + stack=[[]] + for m in tok_re.finditer(s): + t=m.group(0) + if t=='(': + stack.append([]) + elif t==')': + x=stack.pop(); stack[-1].append(x) + else: + if t.startswith('"'): t=t[1:-1] + stack[-1].append(t) + return stack[0][0] +root=parse(src) +def find(node, key): + return [c for c in node if isinstance(c,list) and c and c[0]==key] +def one(node,key): + r=find(node,key); return r[0] if r else None +out={'segments':[], 'vias':[], 'pads':[], 'edges':[], 'zones':[], 'arcs':[]} +for seg in find(root,'segment'): + s=one(seg,'start'); e=one(seg,'end'); w=one(seg,'width'); l=one(seg,'layer'); n=one(seg,'net') + out['segments'].append([float(s[1]),float(s[2]),float(e[1]),float(e[2]),float(w[1]),l[1],n[1] if n else '']) +for a in find(root,'arc'): + s=one(a,'start'); m=one(a,'mid'); e=one(a,'end'); w=one(a,'width'); l=one(a,'layer'); n=one(a,'net') + out['arcs'].append([float(s[1]),float(s[2]),float(m[1]),float(m[2]),float(e[1]),float(e[2]),float(w[1]),l[1],n[1] if n else '']) +for v in find(root,'via'): + at=one(v,'at'); sz=one(v,'size'); n=one(v,'net') + out['vias'].append([float(at[1]),float(at[2]),float(sz[1]),n[1] if n else '']) +for fp in find(root,'footprint'): + at=one(fp,'at'); fx,fy=float(at[1]),float(at[2]); frot=float(at[3]) if len(at)>3 else 0.0 + layer=one(fp,'layer')[1] + ref='' + for p in find(fp,'property'): + if len(p)>2 and p[1]=='Reference': ref=p[2] + for pad in find(fp,'pad'): + pat=one(pad,'at'); px,py=float(pat[1]),float(pat[2]); prot=float(pat[3]) if len(pat)>3 else 0.0 + size=one(pad,'size'); w,h=float(size[1]),float(size[2]) + shape=pad[3] if len(pad)>3 else 'rect' + typ=pad[2] if len(pad)>2 else 'smd' + n=one(pad,'net'); net=n[1] if n and len(n)>1 else '' + # KiCad: pad position rotated by footprint rotation (clockwise in screen coords since y down) + r=math.radians(-frot) + ax=fx+px*math.cos(r)-py*math.sin(r); ay=fy+px*math.sin(r)+py*math.cos(r) + lay=one(pad,'layers'); lays=lay[1:] if lay else [] + out['pads'].append([round(ax,3),round(ay,3),w,h,prot,shape,typ,net,ref,layer,lays]) +for g in root: + if isinstance(g,list) and g and g[0] in ('gr_line','gr_rect','gr_arc','gr_circle','gr_poly'): + l=one(g,'layer') + if l and l[1]=='Edge.Cuts': + out['edges'].append(g) +for z in find(root,'zone'): + n=one(z,'net'); net=n[1] if n and len(n)>1 else '' + for fpoly in find(z,'filled_polygon'): + l=one(fpoly,'layer'); pts=one(fpoly,'pts') + xy=[(float(p[1]),float(p[2])) for p in pts[1:] if isinstance(p,list) and p[0]=='xy'] + out['zones'].append([l[1] if l else '', net, xy]) +json.dump(out, open(sys.argv[2] if len(sys.argv) > 2 else 'pcb.json', 'w')) +print({k: len(v) for k, v in out.items()}) diff --git a/scripts/tarjetas/rasterizar.py b/scripts/tarjetas/rasterizar.py new file mode 100644 index 0000000..9adad5b --- /dev/null +++ b/scripts/tarjetas/rasterizar.py @@ -0,0 +1,26 @@ +"""SVG → WebP con Chromium (Playwright) y Pillow, para que el texto y los trazos salgan iguales que en el navegador. +Uso: python3 rasterizar.py SALIDA_DIR a.svg [b.svg …]""" +import asyncio, io, os, re, sys +from PIL import Image +from playwright.async_api import async_playwright + +OUT = sys.argv[1] + + +async def main(): + async with async_playwright() as p: + b = await p.chromium.launch() + for f in sys.argv[2:]: + txt = open(f).read() + w, h = map(int, re.search(r'width="(\d+)" height="(\d+)"', txt).groups()) + pg = await b.new_page(viewport={'width': w, 'height': h}) + await pg.set_content('' + txt + '') + png = await pg.screenshot(clip={'x': 0, 'y': 0, 'width': w, 'height': h}) + await pg.close() + im = Image.open(io.BytesIO(png)).convert('RGB') + dst = os.path.join(OUT, os.path.basename(f)[:-4] + '.webp') + im.save(dst, 'WEBP', quality=84, method=6) + print(dst, im.size, os.path.getsize(dst), 'bytes') + await b.close() + +asyncio.run(main()) diff --git a/scripts/tarjetas/tarjetas_blanco.py b/scripts/tarjetas/tarjetas_blanco.py new file mode 100644 index 0000000..796ce67 --- /dev/null +++ b/scripts/tarjetas/tarjetas_blanco.py @@ -0,0 +1,131 @@ +"""Cobre de las EinarBoard en blanco sobre noche (21-oct): portada (V5.6) y miniaturas por versión. +Lo nuevo de cada revisión va en blanco sólido; el resto del cobre en blanco tenue. +Uso: python3 tarjetas_blanco.py DIR_JSON DIR_SALIDA (pcb_v55/v56/v57/v6.json, de leer_pcb.py) +Genera SVG; la conversión a WebP la hace rasterizar.py.""" +import json, os, re, sys + +SRC, OUT = sys.argv[1], sys.argv[2] +D = {v: json.load(open(os.path.join(SRC, f'pcb_{v}.json'))) for v in ('v55', 'v56', 'v57', 'v6')} + + +def nets(d): + return {p[7] for p in d['pads'] if p[7]} | {s[6] for s in d['segments'] if s[6]} + + +def ok(n): + return n and n != 'GND' and not n.startswith('unconnected') + + +NUEVO = {'v55': set(), + 'v56': {n for n in nets(D['v56']) - nets(D['v55']) if ok(n)}, + 'v57': {n for n in nets(D['v57']) - nets(D['v56']) if ok(n)}, + 'v6': {n for n in nets(D['v6']) - nets(D['v57']) + if ok(n) and re.search(r'GNSS|MODEM|4G|ANT|RF_|NETLIGHT|1V8|F9P|LTE', n)}} + + +def contorno(d): + xs, ys = [], [] + for e in d['edges']: + for k in e[1:]: + if isinstance(k, list) and k and k[0] in ('start', 'end'): + xs.append(float(k[1])); ys.append(float(k[2])) + return min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys) + + +# opacidades: (zonas, B.Cu, capas internas, F.Cu, pads, vías) y lo nuevo (F.Cu, internas, pads) +TEMAS = { + 'mini': dict(fondo='#111A2C', z=0.035, b=0.12, i=0.08, f=0.28, pad=0.38, via=0.32, nf=1.0, ni=0.55, npad=0.95, min_w=0.55), + 'portada': dict(fondo='#0E1524', z=0.03, b=0.10, i=0.07, f=0.22, pad=0.30, via=0.26, nf=0.95, ni=0.5, npad=0.9, min_w=0.8), +} + + +def svg(d, hl, crop, size, tema): + T = TEMAS[tema] + x0, y0, W, H = crop + OW, OH = size + s = max(OW / W, OH / H) # llena el recuadro (la portada recorta arriba y abajo) + ox, oy = (OW - W * s) / 2, (OH - H * s) / 2 + X = lambda x: ox + (x - x0) * s + Y = lambda y: oy + (y - y0) * s + o = [f'', + f''] + + def grupo(op, partes): + if partes: + o.append(f'' + ''.join(partes) + '') + + 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'') + 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'') + else: + rr = min(ww, hh) / 2 if shape == 'oval' else (min(ww, hh) * 0.18 if shape == 'roundrect' else 0) + r.append(f'') + return r + + def vias(solo=None, sin=None): + return [f'' + 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('') + 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'' 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'') + o.append(f'' + ''.join(hoyos) + '') + # contorno de la tarjeta (solo en miniaturas) + if tema == 'mini': + bx, by, bw, bh = contorno(d) + o.append(f'') + o.append('') + 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'))