Sitio: página de inicio con enfoque de la semana, ruta a producción y actividad (fase 2a)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")))
|
||||
|
||||
@ -1,16 +1,33 @@
|
||||
---
|
||||
title: "Bitácora"
|
||||
description: Los documentos del proyecto por fecha, del más nuevo al más viejo.
|
||||
description: Lo que pasó en el tablero y en las actas, y los documentos del proyecto por fecha.
|
||||
template: tablero.html
|
||||
ancho: 52rem
|
||||
---
|
||||
|
||||
<div class="tb tb-bitacora" markdown>
|
||||
|
||||
# Bitácora
|
||||
|
||||
Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra solo lo vigente; aquí queda todo, incluido lo superado. Lo que pasa cada día en el tablero (quién movió qué) se verá aquí cuando el servidor lo publique; por ahora vive en el historial de cada [pendiente](tableros/pendientes.md).
|
||||
Arriba, lo que pasó en el tablero de pendientes y en las actas en los últimos 14 días: sale solo del historial de cada [pendiente](tableros/pendientes.md) y de las actas recibidas. Abajo, los documentos del proyecto por fecha; el menú lateral muestra solo lo vigente y aquí queda todo, incluido lo superado.
|
||||
|
||||
## Actividad { #actividad }
|
||||
|
||||
<div id="bit-actividad"><p class="vacio">…</p></div>
|
||||
|
||||
## Documentos por fecha { #documentos-por-fecha }
|
||||
|
||||
!!! info "Cómo se agrega un documento"
|
||||
Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado».
|
||||
Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado». Si el documento trae `date:` en su encabezado, también sale solo en «Documentos recientes» de la página de inicio.
|
||||
|
||||
## Octubre de 2026
|
||||
### Octubre de 2026
|
||||
|
||||
**21 oct (noche)**
|
||||
: [Estado de la v6 y T25/T28 aplicados](hardware/v6-estado-21oct.md). T28 cerró 2 de los 8 bloqueantes; D34 quedó lejos del pin SW de U25 (lazo de ≈ 115 mm²) y MH8 cae encima de D34. Falta el push de Leo.
|
||||
: [Pasada 5 revisada](hardware/v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta): el sensado de RS1 dejó de ser Kelvin en las dos tarjetas; falta la pasada 6, ≈ 30 min por tarjeta.
|
||||
|
||||
**21 oct**
|
||||
: [Auditoría de la pasada 4 de Michelle: V5.6 y V5.7](hardware/v56-v57-auditoria-21oct.md). El corto de R106 y lo que no pide mover piezas ya quedó con T32 y T33 (C130 aprobado por Leo); a Michelle le tocó la pasada 5.
|
||||
|
||||
**19 oct (tarde)**
|
||||
: [V5.6: qué es crítico y qué se puede quedar](hardware/v56-clasificacion-19oct.md). Un solo crítico, nuevo: C129 en VSYS de U6. L7 y lo heredado de la V5.5 se quedan para el piloto y se miden en el banco.
|
||||
@ -43,7 +60,7 @@ Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra
|
||||
**1 oct**
|
||||
: [Hoja de datos de la V5.6](hardware/v56-datasheet.md).
|
||||
|
||||
## Septiembre de 2026
|
||||
### Septiembre de 2026
|
||||
|
||||
**29 sep**
|
||||
: [Pinout de la V5.6](hardware/v56-pinout.md) y [reglas de layout, del esquemático a la PCB](hardware/v56-layout-instrucciones.md).
|
||||
@ -63,3 +80,10 @@ Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra
|
||||
: [Auditoría pre-fabricación de la v6](hardware/v6-auditoria-prefab.md) ([en inglés](hardware/v6-prefab-audit.md)) (superada por la del 28 sep).
|
||||
|
||||
Lo anterior a septiembre está en la [historia del proyecto](historia/index.md).
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/tableros/actividad.js"></script>
|
||||
<script>
|
||||
Actividad.montar(document.getElementById('bit-actividad'), { dias: 14, limite: 80, porDia: true });
|
||||
</script>
|
||||
|
||||
74
docs/datos/proyecto.json
Normal file
@ -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 }
|
||||
}
|
||||
BIN
docs/hardware/img/2026-10-21_Drogheda_Pasada-5-V56-V57_v1.0.png
Normal file
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 550 KiB |
@ -6,19 +6,19 @@ title: "Tarjetas y hardware"
|
||||
|
||||
Estado de cada versión de la EinarBoard al **21-oct-2026**, con sus documentos vigentes, los históricos y el siguiente paso. ¿No sabes qué tarjeta tienes? [Manuales por tarjeta](../manuales/index.md) te la identifica por serigrafía, conectores y LEDs. Estado vivo (tareas, decisiones, responsables): [Tablero de ingeniería](../tableros/ingenieria.md).
|
||||
|
||||
!!! warning "21-oct: pasada 4 auditada y, en la tarde, T32 + T33 — falta la pasada 5 de Michelle"
|
||||
- **V5.6 — todavía no se pide.** C129 quedó colocado y unido a U6, salió la rama de Dbk2, y U7 girado acortó el nodo SW de 13 a 10 mm. El corto `/MODEM_EN`–GND que dejó la pasada 4 en R106 y la separación de 0.09 mm bajo U7 **ya los quitó T33**, junto con las vías sencillas de VSYS y de `/3V3_MODEM`. **C130** (100 nF en la entrada de U7, D-21-1) ya está en el esquemático (T32). Falta (≈ 1 h): F8, C130 junto a U7.2, el GND de C129 y la entrada de RS1 ([pasada 5](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5)).
|
||||
- **V5.7 — ya sin el bloqueante de K1.** `/SYS5V` salió de In2 y L22 quedó junto a D34; T33 reforzó 4 cambios de capa del 5 V y quitó las vías sueltas. Falta acercar C149/C150 a U25 (lazo de 52 mm²), C130 y la vía de (168.0, 110.6): ≈ medio día ([§3.2](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57)).
|
||||
- **v6 — sin cambio** desde el 19-oct. Plan de fines de semana: 24–25 oct y 31 oct–1 nov.
|
||||
!!! warning "21-oct, noche: pasada 5 de V5.6/V5.7 revisada y T25 + T28 aplicados en la v6"
|
||||
- **V5.6 — todavía no se pide.** La pasada 5 (`6458c5d`) colocó C130 junto a U7.2 y la vía del GND de C129, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: **el sensado ya no es Kelvin**. Falta (≈ 30 min): esa vía y una 2.ª de RS1 bien unida, la 2.ª vía de U7.2 y las exclusiones del DRC ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)).
|
||||
- **V5.7 — ya sin el bloqueante de K1.** Lo mismo en RS1 y en U7.2, más C149/C150 sin girar (lazo de ≈ 56 mm²) y el GND de C129 ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)).
|
||||
- **v6 — T25 y T28 aplicados** (falta el push). Michelle adelantó la fuente U25, pero D34 quedó lejos del pin SW (lazo de ≈ 115 mm²) y MH8 cae encima de D34: [**▶ estado de la v6 al 21-oct**](v6-estado-21oct.md).
|
||||
|
||||
Detalle y lista para Michelle: [**▶ Auditoría del 21-oct**](v56-v57-auditoria-21oct.md). Antes: [re-auditoría del 19-oct](re-auditoria-19oct.md) · [qué es crítico en la V5.6](v56-clasificacion-19oct.md) · [plan v6 (rev. 1.1)](v6-plan-fines-de-semana.md).
|
||||
|
||||
| Tarjeta | Estado | Pedido | Proyecto KiCad | Firmware |
|
||||
|---|---|---|---|---|
|
||||
| <span class="badge-v55">V5.5</span> | En campo (15 pzas, 22-ago). No se fabrica más. | — | `hbox-watchdog-v5` (tag `v5.5-as-fabbed`) | `hbox-v5-bringup_0.6.0` CAMPO · 0.6.3 banco |
|
||||
| <span class="badge-v56">V5.6</span> | Pasada 4 auditada el 21-oct (`85e7602`) y T32 + T33 en la tarde: **no lista**. El corto de R106 ya salió; faltan C130 junto a U7.2, el GND de C129 y la entrada de RS1: ≈ 1 h ([pasada 5](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5)). C129 ya está, con T31 (`3daff19`). | 113 (piloto de 5 primero) | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), no existe |
|
||||
| <span class="badge-v57">V5.7</span> | Pasada 4 auditada el 21-oct (`1416dcb`) y T32 + T33 en la tarde: **NO LISTA**, pero ya sin el bloqueante de K1. Faltan C149/C150 junto a U25, C130 y una vía del 5 V: ≈ medio día ([§3.2](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57)). Entrada 9–32 V. | 327 (5 prototipos primero) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) |
|
||||
| <span class="badge-v6">v6</span> | Re-auditada el 19-oct: **NO LISTA**, sin cambio (`00a9af4`: solo 15 vías de tierra); siguen los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md) (rev. 1.1). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe |
|
||||
| <span class="badge-v56">V5.6</span> | Pasada 5 revisada el 21-oct en la noche (`6458c5d`): **no lista**. C129 y C130 ya están; la vía de entrada de RS1 quedó encima del hilo de sensado (ya no es Kelvin). Falta ≈ 30 min ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). | 113 (piloto de 5 primero) | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), no existe |
|
||||
| <span class="badge-v57">V5.7</span> | Pasada 5 revisada el 21-oct en la noche (`6458c5d`): **NO LISTA**, pero ya sin el bloqueante de K1. Igual que la V5.6 en RS1, más C149/C150 sin girar (lazo de ≈ 56 mm²) y el GND de C129 ([pasada 6](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta)). Entrada 9–32 V. | 327 (5 prototipos primero) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) |
|
||||
| <span class="badge-v6">v6</span> | Revisada el 21-oct (`9255b84` + T25/T28): **NO LISTA**. T28 cerró 2 de los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md) y T25 regresó los barrenos; Michelle adelantó la fuente U25, pero D34 quedó lejos del pin SW y MH8 cae encima ([estado del 21-oct](v6-estado-21oct.md)). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe |
|
||||
| <span class="badge-v4">V4</span> | En prueba y campo. Sin producción nueva. | — | `hbox-watchdog-v4` | `hbox-tarjetaV4_0.6.0` CAMPO |
|
||||
| <span class="badge-v3">V3</span> | 3 unidades en campo. Registro congelado. | — | `hbox-watchdog-v3-PROD` (no editar) | `hbox-tarjetaV3_4.0.0` |
|
||||
|
||||
@ -52,7 +52,8 @@ Estado de cada versión de la EinarBoard al **21-oct-2026**, con sus documentos
|
||||
- **Ya lo hizo T30** (`7650b96`, 19-oct): vías en paralelo en los cambios de capa de potencia (18 vías), las 4 vías «micro» a vías normales, JP/TP/MOD1 fuera del BOM y del CPL, cajetín Rev 5.6 y stackup JLC04161H-7628.
|
||||
- **Pasada 4 entregada el 21-oct (`85e7602`):** C129 colocado (T31 + F8), rama de Dbk2 fuera, U7 girado (L7.1 a 4.0 mm de U7.8), vías de `/MAIN5V` hacia P11 de vuelta.
|
||||
- **T32 + T33 (21-oct, tarde):** C130 en el esquemático; fuera el corto de R106 y la separación de 0.09 mm; 2.ª vía de VSYS; 3 vías en cada cambio de capa de `/3V3_MODEM` y de `/SYS5V` hacia U12; vías sueltas fuera. DRC: solo los 3 errores de MOD1, 0 sin conectar.
|
||||
- **Falta (pasada 5, ≈ 1 h):** F8 y C130 junto a U7.2 con la 2.ª vía de la entrada de U7, vías al GND de C129 y la 2.ª vía de RS1. Más las exclusiones del DRC: [lista](v56-v57-auditoria-21oct.md#31-v56-hbox-watchdog-v5).
|
||||
- **Pasada 5 (`6458c5d`, 21-oct, noche):** C130 junto a U7.2 con su vía a GND y la vía al GND de C129. **Nuevo:** la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6 (ya no es Kelvin) y la «2.ª vía» de RS1 no está unida en B.Cu.
|
||||
- **Falta (pasada 6, ≈ 30 min):** esa vía fuera del sensado y una 2.ª vía de RS1 unida en F.Cu y B.Cu, la 2.ª vía de la entrada de U7, las exclusiones del DRC y un tramo suelto de `/RS485_B`: [lista](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta).
|
||||
|
||||
L7 y lo heredado de la V5.5 se pueden quedar para el piloto, y se miden en el banco: [**▶ qué es crítico y qué se puede quedar**](v56-clasificacion-19oct.md).
|
||||
|
||||
@ -78,7 +79,8 @@ Lleva Mean Well SD-15B-05 externa (igual que V5.5).
|
||||
- **Lo hace T30:** lo mismo que en la V5.6, más 11 vías de tierra en la entrada y la salida de U25, y borra 12 cobres sueltos.
|
||||
- **Pasada 4, auditada el 21-oct (`1416dcb`):** hechos `/SYS5V` fuera de In2, L22 junto a D34, U7 girado (L7.1 a 0.4 mm), C129 y la serigrafía de Cbst2.
|
||||
- **T32 + T33 (21-oct, tarde):** C130 en el esquemático, 2.ª vía de VSYS y otros 3 cambios de capa del 5 V reforzados, vías sueltas fuera. DRC: solo los 3 errores de MOD1, 0 sin conectar.
|
||||
- **Falta (≈ medio día):** C149/C150 junto a U25, C130 junto a U7.2, la vía de (168.0, 110.6), el GND de C129 y la entrada de RS1: [lista](v56-v57-auditoria-21oct.md#32-v57-hbox-watchdog-v57).
|
||||
- **Pasada 5 (`6458c5d`, 21-oct, noche):** C130 junto a U7.2 con su vía a GND, 2 vías en (168.0, 110.6) y C149/C150 a 1.0 mm de U25.2. **Nuevo:** igual que en la V5.6, la vía de entrada de RS1 quedó encima del hilo de sensado.
|
||||
- **Falta (pasada 6):** RS1 como en la V5.6, C149/C150 girados (lazo de ≈ 56 mm², meta < 30), el GND de C129, la 2.ª vía de U7.2, las exclusiones y un tramo suelto de `/24V`: [lista](v56-v57-auditoria-21oct.md#5-pasada-5-revisada-6458c5d-y-lo-que-falta).
|
||||
|
||||
| | Documentos |
|
||||
|---|---|
|
||||
@ -104,11 +106,15 @@ Lleva Mean Well SD-15B-05 externa (igual que V5.5).
|
||||
- 96 vías de tierra (meta ≥ 400; eran 81 el 16-oct).
|
||||
- Stackup genérico → T28.
|
||||
|
||||
El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a probar sobre `00a9af4` el 19-oct: aplican igual (con los 14 errores esperados de MH4, que se liberan a mano). El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**.
|
||||
El sensado de RS1 ya está bien (hay que conservarlo).
|
||||
|
||||
**21-oct (`9255b84`, Michelle):** adelantó el bloque A (L22 a 2.2 mm del pin SW de U25, 6 vías en su EP) y llevó `/VEH5V` de L22 a RS1 todo por F.Cu. Pero D34 quedó a 13 mm del pin SW: el lazo caliente pasó de ≈ 47 a ≈ 115 mm². **T25 y T28 ya están aplicados** (falta el push): cierran los bloqueantes 1 y 8 y regresan los barrenos; el DRC da 22 errores en MH4 y MH8, que Michelle libera. Detalle: [**▶ estado de la v6 al 21-oct**](v6-estado-21oct.md).
|
||||
|
||||
El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**.
|
||||
|
||||
| | Documentos |
|
||||
|---|---|
|
||||
| Vigentes | [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.1: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) |
|
||||
| Vigentes | [**▶ Estado al 21-oct (T25 + T28)**](v6-estado-21oct.md) · [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.1: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) |
|
||||
| Detalle (28-sep) | [Auditoría completa](v6-auditoria-completa.md) ([inglés](v6-full-audit.md)) · [Pasada 1 — cambios al esquemático](v6-cambios-esquematico.md) · [Pasada 2 — puertos (inglés)](v6-puertos-sin-nueva-version.md) · [Pasada 3 — revisión final (inglés)](v6-revision-final.md) |
|
||||
| Históricos | [Próximos pasos para ordenar (29-sep)](v6-proximos-pasos.md) — superado por el plan de fines de semana · [Auditoría V5.6 · V5.7 · v6 del 16-oct (mañana), §8](auditoria-produccion-16oct.md#8-v6-que-falta) · [Revisión contra lecciones V5.5](v6-revision-lecciones-v55.md) — superada por la auditoría completa · [Auditoría pre-fab 25-sep](v6-auditoria-prefab.md) ([inglés](v6-prefab-audit.md)) — superada por la del 28-sep |
|
||||
| Cantidad | La auditoría de septiembre hablaba de **5 prototipos**; el plan del 13-oct son **130 unidades y todos los reemplazos futuros**. Recomendado: piloto de 20 que pase el banco con el firmware 0.7.x antes de las 130. |
|
||||
@ -116,8 +122,8 @@ El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a
|
||||
**Siguiente paso** ([plan de fines de semana, rev. 1.1](v6-plan-fines-de-semana.md); recorrido una semana el 19-oct):
|
||||
|
||||
- **Lun 19 o mar 20:** pre-órdenes en JLC (THVD1410DR, TPS7A2033PDBVR, J14). No se recorren.
|
||||
- **Sáb 24, 8:00:** Leo corre T25 `--set v6` y T28.
|
||||
- **Sáb 24:** Michelle trabaja la potencia y libera MH4/MH6.
|
||||
- **21-oct:** T25 y T28 ya aplicados; falta el push de Leo.
|
||||
- **Sáb 24:** Michelle regresa D34 junto a U25, libera MH4, MH6 y MH8, y sigue con la potencia ([estado del 21-oct, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir)).
|
||||
- **Dom 25:** RF del módem, U20, GNSS y U12.
|
||||
- **Sáb 31 y dom 1-nov:** pares, puertos, In1, vías de tierra (T29), serigrafía y DRC final. P7/P8 se quitan del esquemático (decisión del 9-oct).
|
||||
- **Lun 2-nov:** pedido del piloto de 20.
|
||||
|
||||
@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v5.6, v5.7, layout, auditor
|
||||
project: Project Drogheda
|
||||
author: Helgen Technologies (revisión: Claude; pedido: Leo)
|
||||
date: 2026-10-21
|
||||
version: 1.1
|
||||
status: ACTIVO — audita HBox_PCB 85e7602 (V5.6) y 1416dcb (V5.7) · 21-oct tarde: T32 (C130) y T33 (arreglos por script) aplicados · lo que queda para Michelle en la sección 3
|
||||
version: 1.2
|
||||
status: ACTIVO — audita HBox_PCB 85e7602 (V5.6) y 1416dcb (V5.7) · 21-oct tarde: T32 (C130) y T33 (arreglos por script) aplicados · 21-oct noche: pasada 5 (6458c5d) revisada, lo que falta en la sección 5
|
||||
audience: Leo, Michelle (layout)
|
||||
---
|
||||
|
||||
@ -17,6 +17,8 @@ audience: Leo, Michelle (layout)
|
||||
|
||||
**Actualización (21-oct, tarde):** el corto, la separación, la 2.ª vía de VSYS y casi todas las vías de potencia ya quedaron con **T33**, y C130 ya está en los dos esquemáticos con **T32**. A Michelle le queda lo que pide mover pistas: C130 junto a U7.2, el GND de C129, la entrada de RS1 y, en la V5.7, C149/C150 ([sección 3](#3-para-michelle-pasada-5-despues-de-t32-y-t33)).
|
||||
|
||||
**Actualización (21-oct, noche):** Michelle ya hizo la pasada 5 (`6458c5d`). C130 quedó bien en las dos, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: **el sensado ya no es Kelvin**. Lo que falta está en la [sección 5](#5-pasada-5-revisada-6458c5d-y-lo-que-falta).
|
||||
|
||||
| Tarjeta | Commit | DRC (errores): commit → con T33 | Sin conectar | Paridad | Veredicto (con T32 y T33) |
|
||||
|---|---|---|---|---|---|
|
||||
| **V5.6** (`hbox-watchdog-v5`) | `85e7602` | **5 nuevos** (un corto y una separación) + los 3 de MOD1 → **solo los 3 de MOD1** | 0 | solo PF* (+ C130 hasta el F8) | **No lista:** el corto y la separación ya salieron. Faltan C130 junto a U7.2, el GND de C129 y la entrada de RS1 (≈ 1 h). Luego Gerber. |
|
||||
@ -140,6 +142,48 @@ Avísale a Leo con los dos hashes: Claude revisa el cobre antes de pedir Gerber.
|
||||
- Si no cabe junto a U7.2, el punto 3 queda en 🟡 y se mide en el piloto: zumbido en U7.2 y caída del riel del módem en J7.1.
|
||||
- **Giro de U7: aceptarlo.** Mejora el nodo SW de 13 a 6–10 mm; lo que empeora, se arregla con los puntos 3 y D-21-1.
|
||||
|
||||
## 5. Pasada 5 revisada (`6458c5d`) y lo que falta
|
||||
|
||||

|
||||
|
||||
**DRC de las dos:** solo los 3 errores de MOD1 (todavía sin excluir), 0 sin conectar y la paridad solo con los PF*: C130 ya entró con F8. Nada protegido se movió.
|
||||
|
||||
| Punto de la sección 3 | V5.6 | V5.7 |
|
||||
|---|---|---|
|
||||
| F8 y C130 junto a U7.2, con su vía a GND | ✅ a 1.8 mm del pin, unido por F.Cu, con su vía | ✅ igual |
|
||||
| 2.ª vía de la entrada de U7 | ❌ sigue una sola, en (139.4, 117.2) | ❌ sigue una sola, en (139.57, 116.57) |
|
||||
| Vía al GND de C129 | ✅ 1 vía a ≤ 0.5 mm | ❌ ninguna |
|
||||
| RS1: 2.ª vía de entrada | ❌ la vía nueva de (149.48, 118.8) solo toca F.Cu; el DRC la marca como colgando | ❌ igual |
|
||||
| RS1: vía de entrada fuera del pad | 0.51 mm del pad, **pero encima del hilo de sensado de U6.6** 🔴 | 0.12 mm (meta ≥ 0.2), **y encima del hilo de sensado** 🔴 |
|
||||
| C149/C150 junto a U25, lazo < 30 mm² | — | 🟠 a 1.0 mm de U25.2, pero sin girar: lazo ≈ 56 mm² (antes ≈ 52) |
|
||||
| 2.ª vía en `/VEH5V` (168.0, 110.6) | — | ✅ 2 vías |
|
||||
| Exclusiones de MH7, MH9 y MH10 | ❌ | ❌ |
|
||||
| Opcional: compensación de U7 más cerca | sin cambio | sin cambio |
|
||||
|
||||
!!! danger "El sensado de RS1 ya no es Kelvin en ninguna de las dos"
|
||||
La vía de entrada de RS1 sí se movió fuera del pad, pero quedó **encima del hilo de 0.25 mm que va de U6.6 (VIN del LTC4040) al pad 1 de RS1**.
|
||||
|
||||
- **Qué cambia:** U6.6 ya no mide el voltaje en el pad sino en la vía. A los 10 mΩ de RS1 se suman unas décimas de mΩ de cobre (≈ 0.3–0.4 mΩ), así que el límite de entrada baja ≈ 3–4 %: ≈ 2.4 A en vez de 2.5 A. Además, parte de la corriente de entrada pasa por el hilo de sensado.
|
||||
- **Qué tan grave es:** no es el defecto de la V5.5 (la carga limitada a ≈ 1 A), pero es justo lo que T27 corrigió, y depende de dónde caiga la vía en cada tarjeta.
|
||||
- **Por qué el DRC no lo marca:** la vía y el hilo son de la misma red.
|
||||
|
||||
**Para Michelle — pasada 6** (≈ 30 min por tarjeta):
|
||||
|
||||
1. **RS1, en las dos:**
|
||||
- recorre la vía de entrada de `/VEH5V` para que quede a **≥ 0.2 mm del hilo de sensado** de U6.6 y a ≥ 0.2 mm del pad de RS1.1, por ejemplo del lado izquierdo del pad;
|
||||
- pon una **2.ª vía** de entrada unida en F.Cu **y** en B.Cu al `/VEH5V` que llega;
|
||||
- la vía de (149.48, 118.8) únela al `/VEH5V` de B.Cu o bórrala;
|
||||
- el hilo de sensado no se mueve: sigue entrando al pad de RS1.1 por su lado, sin tocar nada más.
|
||||
2. **2.ª vía de la entrada de U7, en las dos:** junto a (139.4, 117.2) en la V5.6 y a (139.57, 116.57) en la V5.7.
|
||||
3. **V5.7, GND de C129:** 1–2 vías a ≤ 0.5 mm del pad (152.2, 112.58).
|
||||
4. **V5.7, C149/C150:** gíralos para que su tierra mire a D34 y al EP de U25 (sección 3.2, paso 2). Meta: lazo < 30 mm².
|
||||
5. **Exclusiones** de MH7, MH9 y MH10 en el DRC, en las dos.
|
||||
6. **Tramos sueltos:**
|
||||
- V5.6: el tramo de 0.01 mm de `/RS485_B` en (136.59, 108.89), que regresó;
|
||||
- V5.7: el tramo de 3.6 mm de `/24V` en B.Cu, junto a (182.4, 68.11).
|
||||
|
||||
Cierre igual que en la sección 3. Antes de pedir Gerber, Claude revisa el cobre, sobre todo el sensado de RS1.
|
||||
|
||||
## Cómo se hizo
|
||||
|
||||
- **Archivos:** HBox_PCB `1416dcb` (V5.6 de `85e7602`, V5.7 de `1416dcb`) contra `3daff19`, cuyos PCB son los de `e4aa6e9`. La diferencia se sacó por UUID.
|
||||
@ -160,3 +204,4 @@ Avísale a Leo con los dos hashes: Claude revisa el cobre antes de pedir Gerber.
|
||||
- vías de corte por red, otra vez: en las redes de potencia solo quedan las que le tocan a Michelle, las que se aceptaron el 19-oct (rama de U12, medición de `/BAT_PLUS` y, en la V5.7, las dos de `/24V` hacia U25) y ramas sin corriente (divisores, pull-ups, pines de medición).
|
||||
|
||||
Después se aplicaron a HBox_PCB: los scripts revisan que el archivo sea el auditado y, si cambió, no escriben. Cada uno deja su respaldo (`.T32bak`, `.T33bak`).
|
||||
- **Pasada 5 (noche):** `6458c5d` contra el resultado de T33 (`71499c1`), con los mismos scripts y el mismo DRC. El sensado de RS1 se probó recortando el área del pad del resto del cobre: si U6.6 llega a la fuente sin pasar por el pad, no es Kelvin.
|
||||
|
||||
@ -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"
|
||||
|
||||
102
docs/hardware/v6-estado-21oct.md
Normal file
@ -0,0 +1,102 @@
|
||||
---
|
||||
title: "Estado de la v6 al 21-oct y scripts T25/T28 aplicados"
|
||||
tags: [project-drogheda, hardware, hbox, einarboard, v6, auditoria, t25, t28, barrenos, u25, layout, michelle]
|
||||
project: Project Drogheda
|
||||
author: Helgen Technologies (revisión: Claude; pedido: Leo)
|
||||
date: 2026-10-21
|
||||
version: 1.0
|
||||
status: ACTIVO — HBox_PCB 9255b84 (Michelle, 21-oct 18:14) con T25 y T28 aplicados por Claude · falta commit y push de Leo
|
||||
audience: Leo, Michelle (layout)
|
||||
---
|
||||
|
||||
# Estado de la v6 al 21-oct · T25 y T28 aplicados
|
||||
|
||||
**Pregunta (Leo, 21-oct):** ¿T28 ya quedó o hay que correrlo? ¿Cómo va la v6?
|
||||
|
||||
**Respuesta corta:**
|
||||
|
||||
- **T28 nunca se había corrido, y T25 tampoco.** Se probaron el 16 y el 19-oct para correrlos el sábado 24 a las 8:00 ([plan, §2](v6-plan-fines-de-semana.md#2-sabado-24-800-leo-scripts-t25-y-t28-15-min)). Hoy los probé en una copia del commit de Michelle (`9255b84`) y los apliqué a los archivos de HBox_PCB. **Falta que Leo haga commit y push.**
|
||||
- **La v6 todavía no se puede pedir.** Michelle adelantó el bloque A (fuente U25) y parte del B (`/VEH5V`). Hay avance real, pero **D34 quedó lejos del pin SW de U25**: el lazo caliente creció de ≈ 47 a ≈ 115 mm². Además, **MH8 cae encima de D34** y de 2 vías de GND. No se veía porque los barrenos no estaban en la tarjeta.
|
||||
- **DRC de hoy, con T25 y T28:** 22 errores, todos en MH4 y MH8; 0 sin conectar; la paridad solo marca los 19 PWR_FLAG.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v6, plan, layout, firmware,
|
||||
project: Project Drogheda
|
||||
author: Helgen Technologies (plan: Claude; revisión: Leo)
|
||||
date: 2026-10-16
|
||||
version: 1.1
|
||||
status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00
|
||||
version: 1.2
|
||||
status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00 · 21-oct: T25 y T28 ya aplicados (falta el push)
|
||||
audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana (PM)
|
||||
---
|
||||
|
||||
@ -16,6 +16,9 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
|
||||
!!! note "Rev. 1.1 (19-oct): el plan se recorre una semana"
|
||||
El fin de semana del 17–18 se fue en la pasada 3 de V5.6/V5.7; a la v6 solo entraron 15 vías de tierra (`00a9af4`), así que los 8 bloqueantes de la [auditoría del 16-oct](v6-auditoria-produccion-16oct.md) siguen igual ([re-auditoría del 19-oct, §8](re-auditoria-19oct.md#8-v6)). Mismo trabajo, una semana después: **24–25 oct** y **31 oct–1 nov**; pedido del piloto el **lun 2-nov**; G2 ≈ **22-nov**. T25 y T28 se volvieron a probar sobre `00a9af4` y aplican igual. Lo único que no se recorre son las **pre-órdenes en JLC**: se piden ya.
|
||||
|
||||
!!! warning "21-oct: T25 y T28 ya están aplicados y Michelle adelantó el bloque A"
|
||||
El paso del §2 (sábado 24, 8:00) se hizo el 21-oct sobre `9255b84`; falta el commit y push de Leo. Michelle ya movió la fuente U25 (bloque A), pero D34 quedó lejos del pin SW y MH8 cae encima de D34. Lo primero del sábado: [estado de la v6 al 21-oct, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir).
|
||||
|
||||
**Dos metas:**
|
||||
|
||||
| Meta | Qué significa | Fecha objetivo |
|
||||
@ -39,7 +42,7 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
|
||||
|
||||
| Día | Quién | Qué | Listo cuando |
|
||||
|---|---|---|---|
|
||||
| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2) | push hecho; Michelle avisada |
|
||||
| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2). **Ya se corrieron el 21-oct: falta el push** | push hecho; Michelle avisada |
|
||||
| **Sáb 24-oct** | Michelle | **Bloques A + B:** el camino de la energía (U25, RS1, Q7, U6, L6) + liberar MH4 y MH6 (§3) | criterios del §3.5 |
|
||||
| **Dom 25-oct** | Michelle | **Bloque C:** módem (RF LTE, U20, VBAT). **Bloque D:** GNSS. **Bloque E:** U12 (§4) | criterios del §4.4 |
|
||||
| **Dom 25-oct, noche** | Claude | Re-auditoría del commit del domingo; lista corta para el lunes | lista enviada |
|
||||
@ -59,6 +62,8 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
|
||||
|
||||
## 2. Sábado 24, 8:00 — Leo: scripts T25 y T28 (15 min)
|
||||
|
||||
**21-oct:** ya se corrieron sobre `9255b84` ([estado del 21-oct](v6-estado-21oct.md)). El DRC da 22 errores, en MH4 y en MH8, y no solo los 14 de MH4: D34 quedó dentro de MH8. Solo falta el commit y push de abajo.
|
||||
|
||||
Con KiCad **cerrado** y todo subido. Los dos scripts se probaron juntos en copias del commit `2612891` (16-oct) y del `00a9af4` (el del sábado 17, probado el 19-oct): el DRC solo muestra los errores esperados en MH4.
|
||||
|
||||
!!! warning "Antes: árbol limpio y nadie más con la v6 abierta"
|
||||
|
||||
6
docs/img/mexico.svg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/img/tarjetas/portada-v56.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/img/tarjetas/v55.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/img/tarjetas/v56.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/img/tarjetas/v57.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/img/tarjetas/v6.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
141
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
|
||||
<!--
|
||||
Página de inicio (21-oct-2026). Casi todo se llena solo:
|
||||
- Fase, ruta a producción, fechas y sitios: docs/datos/proyecto.json (lo actualiza Claude o Leo).
|
||||
- Enfoque de la semana: lo escriben Leo y Adriana con «Editar» (se guarda en el servidor, /api/hoy).
|
||||
- Pendientes y actividad: del tablero y de las actas (/api/tareas, /api/actividad).
|
||||
- Documentos recientes: los .md con «date:» en su encabezado (hooks/documentos.py).
|
||||
Aquí solo se edita el texto fijo: atajos, «Cómo trabajamos» y la tabla «¿Qué necesitas hacer?».
|
||||
-->
|
||||
|
||||
Sitio interno de Helgen para producir, probar, instalar y diagnosticar el rastreador **HBOX RTK** (tarjeta EinarBoard). Todo trabajo deja un **acta**; toda unidad tiene un **expediente**. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md).
|
||||
<div class="tb hg-inicio" id="inicio">
|
||||
|
||||
<div class="buscar-aviso" markdown>
|
||||
La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento.
|
||||
<div class="in-portada">
|
||||
<img src="img/tarjetas/portada-v56.webp" alt="Cobre de la EinarBoard V5.6; lo nuevo de la revisión, en blanco" width="2400" height="400" fetchpriority="high">
|
||||
</div>
|
||||
|
||||
## ¿Qué necesitas hacer?
|
||||
<header class="in-cab">
|
||||
<img class="in-logo" src="img/helgen-mascara.svg" alt="" width="200" height="215">
|
||||
<h1>Drogheda</h1>
|
||||
<p class="in-desc">Rastreadores HBOX RTK para el equipo de patio de Hutchison Ports. Aquí se diseña, se produce, se prueba y se instala.</p>
|
||||
</header>
|
||||
|
||||
<div class="in-arriba">
|
||||
<dl class="in-props">
|
||||
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 7.5V12l3 2"/></svg>Fase</dt>
|
||||
<dd id="in-fase"><span class="vacio">…</span></dd>
|
||||
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9zM4 7.5l8 4.5 8-4.5M12 12v9"/></svg>Pedido</dt>
|
||||
<dd id="in-pedido"><span class="vacio">…</span></dd>
|
||||
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h12a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zM8 3v4M16 3v4M4 10h16"/></svg>Próximo hito</dt>
|
||||
<dd id="in-hito"><span class="vacio">…</span></dd>
|
||||
<dt><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5.3a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4zM3.5 19.5c.6-3.2 2.8-5 5.5-5s4.9 1.8 5.5 5M16.8 7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM15.8 14.6c2.3 0 4.2 1.6 4.7 4.4"/></svg>Equipo</dt>
|
||||
<dd id="in-equipo"><span class="vacio">…</span></dd>
|
||||
</dl>
|
||||
<nav class="in-atajos" aria-label="Atajos">
|
||||
<a href="/actas/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5h10a2 2 0 0 1 2 2V19a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6.5a2 2 0 0 1 2-2zM9 3.5h6v2.5H9zM8.5 11h7M8.5 15h5"/></svg><span><b>Llenar un acta</b><span>Banco, ensamble o campo</span></span></a>
|
||||
<a href="/tableros/unidad/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM20 20l-4.4-4.4"/></svg><span><b>Buscar una unidad</b><span>Su expediente, por H00xx o por serie</span></span></a>
|
||||
<a href="/descargas/#firmware-del-hbox-que-binario-va-en-que-tarjeta"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7.5 10.5 12 15l4.5-4.5M5 20h14"/></svg><span><b>Firmware por tarjeta</b><span>Con su SHA-256</span></span></a>
|
||||
<a href="/tableros/pendientes/?mios=1"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM5 20c.8-3.8 3.6-6 7-6s6.2 2.2 7 6"/></svg><span><b>Mis pendientes</b><span>Lo que tengo asignado</span></span></a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="in-enfoque" id="in-enfoque" aria-labelledby="enfoque-h">
|
||||
<img class="in-enfoque-logo" src="img/helgen-mascara.svg" alt="" width="200" height="215">
|
||||
<div class="in-enfoque-c">
|
||||
<div class="in-enfoque-cab"><span class="in-enfoque-cuando">Enfoque de la semana</span></div>
|
||||
<h2 id="enfoque-h">…</h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="in-sec" aria-labelledby="ruta-h">
|
||||
<div class="in-sec-cab">
|
||||
<h2 id="ruta-h">Ruta a producción</h2>
|
||||
<span class="in-leyenda">Línea continua: hecho. Punteada: lo que falta. El anillo marca el paso en el que va cada tarjeta. En cada imagen, lo nuevo de la revisión va en blanco.</span>
|
||||
<a class="in-ver" href="/tableros/ingenieria/">Tablero de Ingeniería</a>
|
||||
</div>
|
||||
<div id="in-ruta"><p class="vacio">…</p></div>
|
||||
</section>
|
||||
|
||||
<section class="in-sec" aria-labelledby="pend-h">
|
||||
<div class="in-sec-cab in-pend-cab">
|
||||
<div class="in-pend-t"><h2 id="pend-h">Pendientes</h2><span class="in-resumen" id="in-resumen"></span></div>
|
||||
<div class="in-pend-acc" id="in-pend-acc"><a class="boton sec" href="/tableros/pendientes/">Abrir el tablero completo</a></div>
|
||||
</div>
|
||||
<div id="in-kb"></div>
|
||||
</section>
|
||||
|
||||
<div class="in-tres">
|
||||
<section class="in-caja" aria-labelledby="hitos-h">
|
||||
<div class="in-caja-cab"><h2 id="hitos-h">Próximas fechas</h2></div>
|
||||
<div id="in-hitos"><p class="vacio">…</p></div>
|
||||
</section>
|
||||
<section class="in-caja" aria-labelledby="act-h">
|
||||
<div class="in-caja-cab"><h2 id="act-h">Actividad</h2><a href="/bitacora/">Bitácora</a></div>
|
||||
<div id="in-actividad"></div>
|
||||
</section>
|
||||
<section class="in-caja" aria-labelledby="docs-h">
|
||||
<div class="in-caja-cab"><h2 id="docs-h">Documentos recientes</h2><a href="/bitacora/#documentos-por-fecha">Todos</a></div>
|
||||
<div id="in-docs"><p class="vacio">…</p></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="in-dos">
|
||||
<section class="in-caja in-donde" aria-labelledby="mapa-h">
|
||||
<div class="in-caja-cab"><h2 id="mapa-h">Dónde estamos</h2><a href="/tableros/sitio/">Sitios</a></div>
|
||||
<div class="in-mapa" id="in-mapa"></div>
|
||||
<ul class="in-sitios" id="in-sitios"></ul>
|
||||
</section>
|
||||
<section class="in-caja" aria-labelledby="como-h">
|
||||
<div class="in-caja-cab"><h2 id="como-h">Cómo trabajamos</h2><a href="/tableros/ingenieria/#filosofia">Con detalle</a></div>
|
||||
<p class="in-nota">Antes de proponer un cambio, revisa que no choque con ninguna de estas reglas.</p>
|
||||
<ul class="in-reglas">
|
||||
<li><b>Primero la seguridad en campo.</b> <span>La unidad vive a más de 45 °C y en batería; todo se prueba en banco antes de llevarlo a campo.</span></li>
|
||||
<li><b>Una etapa probada vale más que una nueva.</b> <span>Los zócalos se quedan; la fuente de la V5.7 viene del bloque ya auditado de la v6.</span></li>
|
||||
<li><b>Redundancia solo con causa.</b> <span>Por eso salió P12 y entró el acelerómetro U26.</span></li>
|
||||
<li><b>Las reglas de fabricación no se negocian.</b> <span>Pasta de U6 al 50 % o menos, nada de potencia por capas internas, lupa en U6 antes de energizar.</span></li>
|
||||
<li><b>Cambios con prueba y rastro.</b> <span>Scripts verificados contra la netlist, una persona por tarjeta a la vez y no habrá v6.1.</span></li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tb hg-inicio in-que" markdown>
|
||||
|
||||
## ¿Qué necesitas hacer? { #que-necesitas-hacer }
|
||||
|
||||
| Quiero… | Abre | Acta |
|
||||
|---|---|---|
|
||||
@ -21,7 +119,7 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J
|
||||
| **Diagnosticar** una unidad que falla en la máquina | [Diagnóstico en campo](diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](formularios/diagnostico.html) |
|
||||
| **Diagnosticar** una unidad en la mesa | [Diagnóstico en banco](procedimientos/diagnostico-banco.md) | [Banco](formularios/banco.html) |
|
||||
| **Instalar en sitio** (ICAVE, LZC, EIT, TIMSA) | [Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md) | [Instalación en campo](formularios/instalacion.html) |
|
||||
| **Ver los tableros** (pendientes, ingeniería, producción, actas) | [Tableros](tableros/index.md) | — |
|
||||
| **Ver los tableros** (ingeniería, producción, actas, inventario) | [Tableros](tableros/index.md) | — |
|
||||
| **Inventario** (existencias, entradas y salidas) | [Inventario](tableros/inventario.md) | — |
|
||||
| **Pedir una tarjeta o saber qué versión va** | [Tarjetas y hardware](hardware/index.md) | — |
|
||||
| **Descargar firmware** (por tarjeta, con SHA-256) | [Descargas](descargas/index.md) | [Reflasheo](formularios/reflasheo.html) |
|
||||
@ -31,31 +129,6 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J
|
||||
| Saber qué tarjeta tengo en la mano | [Manuales por tarjeta](manuales/index.md) | — |
|
||||
| Entender cómo llegamos aquí | [Historia del proyecto](historia/index.md) | — |
|
||||
|
||||
## Tarjetas — estado al 19-oct-2026
|
||||
La lupa de arriba busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md). Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
|
||||
|
||||
| Tarjeta | Estado | Pedido | Firmware | Dónde leer |
|
||||
|---|---|---|---|---|
|
||||
| <span class="badge-v55">V5.5</span> | **En campo.** 15 pzas fabricadas el 22-ago; en ensamble, taller y puertos. No se fabrica más. Hallazgo del 16-oct: el cargador queda limitado a ≈ 1 A de entrada (medir en banco). | — | 0.6.0 CAMPO · 0.6.3 en prueba de banco | [Manual V5](manuales/manual-tarjeta-V5.md) |
|
||||
| <span class="badge-v56">V5.6</span> | **Casi lista.** T30 aplicado; falta la pasada 4, con un crítico nuevo: **C129 en VSYS de U6** (un 0603, sin mover piezas). Después Gerber/BOM/CPL y el piloto de 5. Falta también el firmware 0.7.0. Con Mean Well SD-15B externa. | **113** (piloto de 5 primero) | 0.7.0 (`board_v56.h`), no existe | [Qué es crítico](hardware/v56-clasificacion-19oct.md) · [Re-auditoría 19-oct](hardware/re-auditoria-19oct.md) · [Pasada 4](hardware/v56-v57-layout-pasada-4.md) |
|
||||
| <span class="badge-v57">V5.7</span> | **NO LISTA.** Fuente a bordo (entrada 9–32 V). Pasada 3 hecha: ya replica la V5.6. Bloqueante: `/SYS5V` por In2 bajo K1; falta compactar la fuente U25. T30 + ≈ medio día de la pasada 4; después los 5 prototipos. | **327** (5 prototipos primero) | el mismo 0.7.0 | [Re-auditoría 19-oct, §4](hardware/re-auditoria-19oct.md#4-v57-punto-por-punto) |
|
||||
| <span class="badge-v6">v6</span> | **NO LISTA**, sin cambio desde el 16-oct (solo 15 vías de tierra nuevas): potencia por capas internas, RF del módem por In1, sin barrenos. Plan recorrido una semana: fines de semana 24–25 oct y 31 oct–1 nov; piloto de 20 el lun 2-nov. | **130** + reemplazos (piloto de 20) | 0.7.x (`board_v6.h`), no existe | [Auditoría v6](hardware/v6-auditoria-produccion-16oct.md) · [Plan de fines de semana (rev. 1.1)](hardware/v6-plan-fines-de-semana.md) |
|
||||
| <span class="badge-v4">V4</span> | En prueba y campo (Blackpill + Ethernet/serie). Sin producción nueva. | — | 0.6.0 CAMPO · 0.6.3 en prueba | [Manual V4](manuales/manual-tarjeta-V4.md) |
|
||||
| <span class="badge-v3">V3</span> | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) |
|
||||
| <span class="badge-fw">firmware</span> | El binario se elige **por tarjeta**, nunca por número de versión. 0.6.3 en banco desde el 1-oct, no para campo. | | | [Descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) · [notas de versión](descargas/notas-de-version.md) |
|
||||
|
||||
Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el plan: [Tablero de producción](tableros/produccion.md).
|
||||
|
||||
## Hoy — 19 de octubre de 2026
|
||||
|
||||
- **V5.6 (tarde): qué es crítico y qué se puede quedar** ([clasificación](hardware/v56-clasificacion-19oct.md)). Hay un solo crítico, y es nuevo: U6 no tiene capacitor en sus pines VSYS (viene desde la V5.5). Se agrega C129 (10 µF 0603) sin mover piezas. Además hay que regresar 4 vías que se perdieron en el guardado de las 18:59. L7 y lo heredado de la V5.5 se pueden quedar para el piloto, y se miden en el banco.
|
||||
- **Re-auditoría de las tres tarjetas** sobre la pasada 3 de Michelle: [Re-auditoría del 19-oct](hardware/re-auditoria-19oct.md). **V5.6 casi lista** (T30 + ≈ 1 h); **V5.7** con un bloqueante (`/SYS5V` por In2) y la fuente U25 por compactar; **v6** sin avance en sus 8 bloqueantes.
|
||||
- **Script T30** listo y probado en copias con el DRC de KiCad: vías en paralelo, vías «micro», BOM/CPL, cajetín y stackup de V5.6 y V5.7. Lo corre Leo hoy: [pasada 4, §0](hardware/v56-v57-layout-pasada-4.md#0-antes-de-empezar-leo-10-min).
|
||||
- **[Pasada 4](hardware/v56-v57-layout-pasada-4.md)** (Michelle): parte A (V5.6, ≈ 1 h) el martes 20; parte B (V5.7, ≈ medio día) el 20–21. Después, un solo pedido: 5 V5.6 + 5 V5.7 (≈ 21–22-oct).
|
||||
- **Merge del lunes resuelto** (`240e2c3`; `.gitattributes` con `merge=binary`) y verificado objeto por objeto. Para que no se repita: el auto-sync de las 18:00 no debe subir archivos de KiCad, y una persona por tarjeta a la vez ([§2](hardware/re-auditoria-19oct.md#2-el-merge-del-lunes-que-paso-y-como-evitarlo)).
|
||||
- **v6:** [plan de fines de semana, rev. 1.1](hardware/v6-plan-fines-de-semana.md): 24–25 oct y 31 oct–1 nov, piloto de 20 el lun 2-nov, G2 ≈ 22-nov. Las **pre-órdenes en JLC** (THVD1410DR, TPS7A2033PDBVR, J14) no esperan: hoy o mañana.
|
||||
- **Decisiones de Leo (D-19-1…4):** puertos de V5.6/V5.7 (si van, entran al piloto), Dbk2, auto-sync y clase HV24: [§7](hardware/re-auditoria-19oct.md#7-decisiones-para-leo).
|
||||
- **V5.7 sin Mean Well acepta 9–32 V** (máquinas de 12 y 24 V). Recomendado: TVS bidireccional para que la polaridad invertida no abra el fusible.
|
||||
- **Jesús:** medir en una V5.5 el límite del cargador con TP1 ([§4.5 de la auditoría del 16-oct](hardware/auditoria-produccion-16oct.md#45-prueba-definitiva-con-tp1-clprog-cualquier-tarjeta-cualquier-corriente)): (TP1 ÷ 32) ÷ voltaje en RS1 ≈ 3.1 confirma el defecto.
|
||||
- **Plan de 570 unidades** (113 / 327 / 130). Pendiente: la caja plástica del HBOX, donde el cable Ethernet no cabe. Tareas y responsables en [Pendientes](tableros/pendientes.md).
|
||||
|
||||
Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
|
||||
</div>
|
||||
|
||||
261
docs/inicio/inicio.css
Normal file
@ -0,0 +1,261 @@
|
||||
/* ==========================================================================
|
||||
Página de inicio (overrides/inicio.html, 21-oct-2026)
|
||||
Va sobre los tokens de stylesheets/helgen.css. El tablero de pendientes, la ruta a
|
||||
producción y la actividad traen sus estilos de tableros/tablero.css (todo bajo .tb).
|
||||
========================================================================== */
|
||||
|
||||
/* ---- Portada: el cobre de la V5.6 en blanco ------------------------------ */
|
||||
.hg-inicio .in-portada {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--hg-linea);
|
||||
background: #0E1524; /* la imagen es oscura en los dos modos */
|
||||
}
|
||||
.hg-inicio .in-portada img { display: block; width: 100%; height: 168px; object-fit: cover; object-position: center 45%; }
|
||||
.hg-inicio .in-portada::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(14, 21, 36, 0) 50%, rgba(14, 21, 36, 0.6) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---- Encabezado ------------------------------------------------------------ */
|
||||
.hg-inicio .in-cab { position: relative; }
|
||||
.hg-inicio .in-logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 80px;
|
||||
height: auto;
|
||||
margin: -46px 0 0 24px;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--hg-fondo);
|
||||
}
|
||||
.md-typeset .hg-inicio .in-cab h1 {
|
||||
margin: 12px 0 6px;
|
||||
font-family: var(--hg-fuente-titulo);
|
||||
font-weight: 700;
|
||||
font-size: 46px;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.012em;
|
||||
color: var(--hg-texto-fuerte);
|
||||
}
|
||||
.md-typeset .hg-inicio .in-desc { margin: 0; max-width: 46rem; font-size: 16.5px; line-height: 1.5; color: var(--hg-texto-2); }
|
||||
|
||||
/* ---- Propiedades del proyecto y atajos --------------------------------------- */
|
||||
.hg-inicio .in-arriba { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px 36px; margin-top: 22px; }
|
||||
.md-typeset .hg-inicio dl.in-props {
|
||||
flex: 1 1 520px;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 136px minmax(0, 1fr);
|
||||
gap: 13px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
.md-typeset .hg-inicio .in-props dt {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin: 0; font-size: 14px; font-weight: 400; color: var(--hg-texto-3);
|
||||
}
|
||||
.md-typeset .hg-inicio .in-props dt .i { width: 16px; height: 16px; stroke-width: 1.8; }
|
||||
.md-typeset .hg-inicio .in-props dd { margin: 0; min-width: 0; font-size: 14.5px; line-height: 1.45; color: var(--hg-texto); }
|
||||
.hg-inicio .in-gris { color: var(--hg-texto-2); }
|
||||
.hg-inicio .in-chip {
|
||||
display: inline-block;
|
||||
padding: 2px 9px;
|
||||
border-radius: 6px;
|
||||
background: var(--hg-elevado);
|
||||
color: var(--hg-texto-fuerte);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hg-inicio .in-pedido-l b { font-weight: 700; color: var(--hg-texto-fuerte); }
|
||||
.hg-inicio .in-barra { display: flex; gap: 3px; height: 8px; max-width: 440px; margin: 8px 0 5px; }
|
||||
.hg-inicio .in-seg { position: relative; overflow: hidden; border-radius: 3px; background: color-mix(in srgb, currentColor 24%, transparent); }
|
||||
.hg-inicio .in-seg > span { position: absolute; inset: 0 auto 0 0; background: currentColor; }
|
||||
.hg-inicio .in-barra-l { display: flex; gap: 3px; max-width: 440px; font-size: 12.5px; }
|
||||
.hg-inicio .in-barra-l span { min-width: 0; white-space: nowrap; }
|
||||
.hg-inicio .s-v55 { color: var(--hg-v55); }
|
||||
.hg-inicio .s-v56 { color: var(--hg-v56); }
|
||||
.hg-inicio .s-v57 { color: var(--hg-v57); }
|
||||
.hg-inicio .s-v6 { color: var(--hg-v6); }
|
||||
.hg-inicio .s-v4 { color: var(--hg-v4); }
|
||||
.hg-inicio .in-mini { display: inline-block; margin-top: 3px; font-size: 12.5px; }
|
||||
.hg-inicio a.in-caras { text-decoration: none; vertical-align: middle; margin-right: 6px; }
|
||||
.hg-inicio a.in-caras .av { width: 26px; height: 26px; font-size: 10px; }
|
||||
.hg-inicio a.in-caras .av + .av { margin-left: -5px; }
|
||||
|
||||
.hg-inicio .in-atajos { flex: 1 1 300px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
||||
.hg-inicio .in-atajos a {
|
||||
display: flex; align-items: flex-start; gap: 11px;
|
||||
padding: 12px 13px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--hg-linea);
|
||||
background: var(--hg-plano-2);
|
||||
color: var(--hg-texto);
|
||||
text-decoration: none;
|
||||
}
|
||||
.hg-inicio .in-atajos a:hover { border-color: var(--hg-linea-2); background: var(--hg-plano); }
|
||||
.hg-inicio .in-atajos .i { width: 20px; height: 20px; margin-top: 1px; stroke-width: 1.8; color: var(--hg-ruta); }
|
||||
.hg-inicio .in-atajos b { display: block; font-size: 14.5px; font-weight: 600; line-height: 1.25; color: var(--hg-texto-fuerte); }
|
||||
.hg-inicio .in-atajos span span { display: block; margin-top: 2px; font-size: 12.5px; line-height: 1.3; color: var(--hg-texto-2); }
|
||||
|
||||
/* ---- Enfoque de la semana ---------------------------------------------------- */
|
||||
.hg-inicio .in-enfoque {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 28px;
|
||||
padding: 20px 22px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--hg-linea-2);
|
||||
background: var(--hg-plano);
|
||||
}
|
||||
.hg-inicio .in-enfoque-logo { flex: none; align-self: flex-start; width: 32px; height: auto; margin-top: 3px; }
|
||||
.hg-inicio .in-enfoque-c { flex: 1; min-width: 0; }
|
||||
.hg-inicio .in-enfoque-cab { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; }
|
||||
.hg-inicio .in-enfoque-cuando { font-size: 13px; color: var(--hg-texto-2); }
|
||||
.md-typeset .hg-inicio .in-enfoque h2 {
|
||||
margin: 6px 0 10px;
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.01em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.md-typeset .hg-inicio .in-enfoque ul { margin: 0 0 0 1.15em; }
|
||||
.md-typeset .hg-inicio .in-enfoque li { margin: 0 0 5px; font-size: 15px; line-height: 1.5; color: var(--hg-texto-suave); }
|
||||
.md-typeset .hg-inicio .in-enfoque li code { font-size: 0.86em; }
|
||||
.md-typeset .hg-inicio .in-enfoque-pie { margin: 10px 0 0; font-size: 12.5px; color: var(--hg-texto-3); }
|
||||
.hg-inicio button.in-editar { padding: 5px 11px; font-size: 13px; }
|
||||
.hg-inicio .in-enfoque-ed input, .hg-inicio .in-enfoque-ed textarea { width: 100%; }
|
||||
.hg-inicio .in-enfoque-ed textarea { min-height: 140px; line-height: 1.45; }
|
||||
.hg-inicio .in-enfoque-botones { display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px; }
|
||||
|
||||
/* ---- Secciones grandes ----------------------------------------------------------- */
|
||||
.hg-inicio .in-sec { margin-top: 40px; }
|
||||
.hg-inicio .in-sec-cab { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; margin-bottom: 12px; }
|
||||
.md-typeset .hg-inicio .in-sec-cab h2 { margin: 0; font-size: 25px; line-height: 1.1; }
|
||||
.hg-inicio .in-leyenda { flex: 1 1 320px; font-size: 13.5px; color: var(--hg-texto-2); }
|
||||
.hg-inicio .in-ver { margin-left: auto; font-size: 14px; }
|
||||
.hg-inicio .in-pend-cab { align-items: center; }
|
||||
.hg-inicio .in-pend-t { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
|
||||
.hg-inicio .in-resumen { font-size: 14px; color: var(--hg-texto-2); }
|
||||
.hg-inicio .in-pend-acc { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-left: auto; }
|
||||
.hg-inicio .in-pend-acc .boton.sec { padding: 7px 12px; font-size: 13.5px; }
|
||||
|
||||
/* ---- Cajas de abajo --------------------------------------------------------------- */
|
||||
.hg-inicio .in-tres { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 40px; }
|
||||
.hg-inicio .in-dos { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 16px; margin-top: 16px; }
|
||||
.hg-inicio .in-caja { min-width: 0; padding: 16px 18px; border-radius: 14px; border: 1px solid var(--hg-linea); background: var(--hg-plano-2); }
|
||||
.hg-inicio .in-caja-cab { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
|
||||
.md-typeset .hg-inicio .in-caja-cab h2 { margin: 0; font-size: 20px; line-height: 1.15; }
|
||||
.hg-inicio .in-caja-cab a { font-size: 13.5px; white-space: nowrap; }
|
||||
|
||||
.md-typeset .hg-inicio ol.in-fechas,
|
||||
.md-typeset .hg-inicio ol.in-docs,
|
||||
.md-typeset .hg-inicio ul.in-sitios,
|
||||
.md-typeset .hg-inicio ul.in-reglas { list-style: none; margin: 0; padding: 0; }
|
||||
.md-typeset .hg-inicio .in-fechas li,
|
||||
.md-typeset .hg-inicio .in-docs li,
|
||||
.md-typeset .hg-inicio .in-sitios li,
|
||||
.md-typeset .hg-inicio .in-reglas li { margin: 0; }
|
||||
|
||||
.hg-inicio .in-fechas li {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-top: 1px solid var(--hg-linea-suave);
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.hg-inicio .in-fechas li:first-child,
|
||||
.hg-inicio .in-docs li:first-child,
|
||||
.hg-inicio .in-sitios li:first-child,
|
||||
.hg-inicio .in-reglas li:first-child { border-top: 0; }
|
||||
.hg-inicio .in-f { font-size: 13px; color: var(--hg-texto-2); white-space: nowrap; }
|
||||
.hg-inicio .in-f.hoy { color: var(--hg-acento); font-weight: 600; }
|
||||
.hg-inicio .in-f.sin { color: var(--hg-aviso); }
|
||||
.hg-inicio .in-t { min-width: 0; color: var(--hg-texto); }
|
||||
.hg-inicio a.in-t { text-decoration: none; }
|
||||
.hg-inicio a.in-t:hover { color: var(--hg-liga-hover); }
|
||||
.hg-inicio .es-hito .in-t { font-weight: 600; color: var(--hg-texto-fuerte); }
|
||||
|
||||
.hg-inicio .in-docs li { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 8px 0; border-top: 1px solid var(--hg-linea-suave); }
|
||||
.hg-inicio .in-docs a { display: flex; align-items: flex-start; gap: 9px; min-width: 0; font-size: 14px; line-height: 1.35; color: var(--hg-texto); text-decoration: none; }
|
||||
.hg-inicio .in-docs a .i { width: 16px; height: 16px; margin-top: 2px; color: var(--hg-texto-3); }
|
||||
.hg-inicio .in-docs a:hover { color: var(--hg-liga-hover); }
|
||||
|
||||
/* ---- Dónde estamos ------------------------------------------------------------------ */
|
||||
.hg-inicio .in-mapa { position: relative; margin: 6px 0 8px; }
|
||||
.hg-inicio .in-mapa svg { display: block; width: 100%; height: auto; }
|
||||
.hg-inicio .in-mapa.sin-mapa { min-height: 180px; border-radius: 10px; background: var(--hg-plano); }
|
||||
[data-md-color-scheme="default"] .hg-inicio .in-mapa path[fill="#141E33"] { fill: #E6EAF1; stroke: #D3D9E4; }
|
||||
[data-md-color-scheme="default"] .hg-inicio .in-mapa path[fill="#1B2742"] { fill: #D5DCE8; }
|
||||
[data-md-color-scheme="default"] .hg-inicio .in-mapa path[stroke="#3A507C"] { stroke: #9AA7BE; }
|
||||
.hg-inicio .in-pin { position: absolute; display: flex; align-items: center; transform: translate(-50%, -50%); }
|
||||
.hg-inicio .in-pin-p {
|
||||
width: 11px; height: 11px;
|
||||
border-radius: 50%;
|
||||
background: var(--hg-ruta);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--hg-ruta) 22%, transparent);
|
||||
}
|
||||
.hg-inicio .in-pin.oficina .in-pin-p { box-sizing: border-box; width: 12px; height: 12px; border: 2.5px solid var(--hg-ruta); background: var(--hg-plano-2); }
|
||||
.hg-inicio .in-pin-t {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
white-space: nowrap;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--hg-texto);
|
||||
text-shadow: 0 1px 2px var(--hg-plano-2), 0 0 4px var(--hg-plano-2);
|
||||
}
|
||||
.hg-inicio .in-pin.et-izq .in-pin-t { left: auto; right: 15px; }
|
||||
.hg-inicio .in-pin.et-arriba .in-pin-t { left: 50%; bottom: 13px; transform: translateX(-50%); }
|
||||
.hg-inicio .in-sitios li { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 12px; padding: 7px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 13.5px; }
|
||||
.hg-inicio .in-sitios a { color: var(--hg-texto); text-decoration: none; }
|
||||
.hg-inicio .in-sitios a:hover { color: var(--hg-liga-hover); }
|
||||
.hg-inicio .in-sitios b { margin-right: 3px; font-weight: 600; }
|
||||
|
||||
/* ---- Cómo trabajamos ------------------------------------------------------------------- */
|
||||
.md-typeset .hg-inicio .in-nota { margin: 0 0 6px; font-size: 13.5px; color: var(--hg-texto-2); }
|
||||
.hg-inicio .in-reglas li { padding: 9px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 14px; line-height: 1.45; }
|
||||
.hg-inicio .in-reglas b { display: block; font-weight: 600; color: var(--hg-texto-fuerte); }
|
||||
.hg-inicio .in-reglas span { color: var(--hg-texto-2); }
|
||||
|
||||
/* ---- ¿Qué necesitas hacer? (Markdown al final de index.md) ------------------------------ */
|
||||
.hg-inicio.in-que { margin-top: 40px; }
|
||||
.md-typeset .hg-inicio.in-que h2 { margin: 0 0 12px; font-size: 25px; line-height: 1.1; }
|
||||
.md-typeset .hg-inicio.in-que > p { margin-top: 14px; font-size: 14px; color: var(--hg-texto-2); }
|
||||
|
||||
/* ---- Tamaños ------------------------------------------------------------------------------ */
|
||||
@media (max-width: 1100px) {
|
||||
.hg-inicio .in-tres { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.hg-inicio .in-tres > :last-child { grid-column: 1 / -1; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.hg-inicio .in-tres, .hg-inicio .in-dos { grid-template-columns: minmax(0, 1fr); }
|
||||
.hg-inicio .in-tres > :last-child { grid-column: auto; }
|
||||
.hg-inicio .in-pend-acc { margin-left: 0; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.hg-inicio .in-portada img { height: 104px; }
|
||||
.hg-inicio .in-logo { width: 60px; margin: -34px 0 0 14px; padding: 4px; }
|
||||
.md-typeset .hg-inicio .in-cab h1 { font-size: 36px; }
|
||||
.md-typeset .hg-inicio .in-desc { font-size: 15.5px; }
|
||||
.md-typeset .hg-inicio dl.in-props { grid-template-columns: minmax(0, 1fr); gap: 3px; }
|
||||
.md-typeset .hg-inicio .in-props dt { margin-top: 10px; }
|
||||
.md-typeset .hg-inicio .in-props dt:first-child { margin-top: 0; }
|
||||
.hg-inicio .in-enfoque { gap: 12px; padding: 16px; }
|
||||
.hg-inicio .in-enfoque-logo { display: none; }
|
||||
.md-typeset .hg-inicio .in-enfoque h2 { font-size: 24px; }
|
||||
.hg-inicio .in-fechas li { grid-template-columns: 74px minmax(0, 1fr) auto; }
|
||||
.hg-inicio .in-ver { margin-left: 0; }
|
||||
}
|
||||
|
||||
@media print {
|
||||
.hg-inicio .in-atajos, .hg-inicio .in-pend-acc, .hg-inicio .in-editar { display: none !important; }
|
||||
}
|
||||
282
docs/inicio/inicio.js
Normal file
@ -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) => '<svg class="' + (cls || 'i') + '" viewBox="0 0 24 24" aria-hidden="true"><path d="' + d + '"/></svg>';
|
||||
function hoyISO(delta) { const d = new Date(Date.now() + (delta || 0) * 86400000); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); }
|
||||
const fechaCorta = f => { const m = String(f || '').match(/^(\d{4})-(\d{2})-(\d{2})/); return m ? (+m[3]) + ' ' + MES[+m[2] - 1] : ''; };
|
||||
const plural = (n, uno, varios) => n + ' ' + (n === 1 ? uno : varios);
|
||||
const esPrueba = s => String(s || '').toUpperCase().includes('PRUEBA');
|
||||
|
||||
async function json(url, opts) {
|
||||
const r = await fetch(url, Object.assign({ cache: 'no-store', credentials: 'same-origin' }, opts || {}));
|
||||
let j = null; try { j = await r.json(); } catch (e) { /* sin JSON */ }
|
||||
if (!r.ok || !j || j.ok === false) throw new Error((j && j.error) || ('HTTP ' + r.status));
|
||||
return j;
|
||||
}
|
||||
function falla(cont, que, err) {
|
||||
cont.innerHTML = '';
|
||||
cont.append(el('p', { class: 'vacio', text: 'No se pudo cargar ' + que + (err && err.message ? ' (' + err.message + ')' : '') + '.' }));
|
||||
}
|
||||
|
||||
// texto del enfoque: **negritas**, `código` y [ligas](/ruta/ o https://…); lo demás se escapa
|
||||
function enLinea(s) {
|
||||
return esc(s)
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
||||
.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^)\s]*)\)/g, '<a href="$2">$1</a>');
|
||||
}
|
||||
|
||||
const equipo = () => (window.Kanban && window.Kanban.equipo) || {};
|
||||
const nombreCorto = id => (window.Kanban && window.Kanban.nombreCorto) ? window.Kanban.nombreCorto(id) : ((equipo()[id] || {}).nombre || id || '').split(' ')[0];
|
||||
const lista = xs => xs.length <= 1 ? (xs[0] || '') : xs.slice(0, -1).join(', ') + ' y ' + xs[xs.length - 1];
|
||||
const avatares = ids => { const g = el('span', { class: 'avs' }); (ids || []).forEach(id => g.append(window.Kanban.avatar(id))); return g; };
|
||||
|
||||
// ---------------------------------------------------------------- arriba: fase, pedido, hito, equipo
|
||||
function pintarFase(P) {
|
||||
const d = $('in-fase'); d.innerHTML = '';
|
||||
d.append(el('span', { class: 'in-chip', text: P.fase.nombre }), ' ', el('span', { class: 'in-gris', text: P.fase.texto }));
|
||||
}
|
||||
|
||||
function pintarPedido(P, plan, actas) {
|
||||
const d = $('in-pedido'); d.innerHTML = '';
|
||||
const vs = (plan && plan.versiones && plan.versiones.length) ? plan.versiones : PLAN_RESPALDO.versiones;
|
||||
const total = (plan && plan.pedido_total) || vs.reduce((n, v) => n + (v.objetivo || 0), 0);
|
||||
const ens = (actas || []).filter(a => a.tipo === 'ensamble' && a.resultado !== 'FALLA' && !esPrueba(a.serie));
|
||||
const hechas = v => { const pref = String(v.serie_prefijo || '').toUpperCase(); return pref ? ens.filter(a => String((a.componentes || {}).einar || '').toUpperCase().startsWith(pref + '-')).length : 0; };
|
||||
const cuenta = vs.reduce((n, v) => n + hechas(v), 0);
|
||||
const linea = el('div', { class: 'in-pedido-l' });
|
||||
linea.append(el('b', { text: total + ' HBOX' }), ' ',
|
||||
el('span', { class: 'in-gris', text: (actas ? plural(cuenta, 'ensamblada', 'ensambladas') + '; ' : '') + (P.pedido_nota || '') }));
|
||||
const barra = el('div', { class: 'in-barra', role: 'img', 'aria-label': vs.map(v => v.objetivo + ' ' + v.version).join(', ') });
|
||||
const leyenda = el('div', { class: 'in-barra-l' });
|
||||
vs.forEach(v => {
|
||||
const cls = BADGE[v.version] || 'v4';
|
||||
const seg = el('span', { class: 'in-seg s-' + cls, style: 'flex:' + (v.objetivo || 1) });
|
||||
const pct = v.objetivo ? Math.min(100, Math.round(100 * hechas(v) / v.objetivo)) : 0;
|
||||
seg.append(el('span', { style: 'width:' + pct + '%' }));
|
||||
barra.append(seg);
|
||||
leyenda.append(el('span', { class: 's-' + cls, style: 'flex:' + (v.objetivo || 1), text: v.objetivo + ' ' + v.version }));
|
||||
});
|
||||
d.append(linea, barra, leyenda);
|
||||
d.append(el('a', { class: 'in-mini', href: '/tableros/produccion/', text: 'Avance contra el plan' }));
|
||||
}
|
||||
|
||||
function hitosVigentes(P) {
|
||||
const hoy = hoyISO();
|
||||
return (P.hitos || []).filter(h => !h.fecha || (h.fin || h.fecha) >= hoy)
|
||||
.sort((a, b) => ((a.fecha ? 1 : 0) - (b.fecha ? 1 : 0)) || String(a.fecha).localeCompare(String(b.fecha)));
|
||||
}
|
||||
function pintarHito(P) {
|
||||
const d = $('in-hito'); d.innerHTML = '';
|
||||
const h = hitosVigentes(P)[0];
|
||||
if (!h) { d.append(el('span', { class: 'vacio', text: 'Sin hitos por delante.' })); return; }
|
||||
d.append(el('span', { text: h.texto }), ' ', el('span', { class: 'in-gris', text: h.cuando }));
|
||||
}
|
||||
|
||||
function pintarEquipo(P) {
|
||||
const d = $('in-equipo'); d.innerHTML = '';
|
||||
const activos = Object.entries(equipo()).filter(([, m]) => m.activo !== false && m.nombre)
|
||||
.sort((a, b) => ((AREAS.indexOf(a[1].area) + 1 || 99) - (AREAS.indexOf(b[1].area) + 1 || 99)) || a[1].nombre.localeCompare(b[1].nombre));
|
||||
const caras = el('a', { class: 'avs in-caras', href: '/tableros/equipo/', title: 'Equipo: ' + activos.map(([, m]) => m.nombre).join(', ') });
|
||||
activos.slice(0, 6).forEach(([id]) => caras.append(window.Kanban.avatar(id)));
|
||||
if (activos.length > 6) caras.append(el('span', { class: 'av libre', text: '+' + (activos.length - 6) }));
|
||||
d.append(caras);
|
||||
if (P.gestion && P.gestion.texto) d.append(' ', el('span', { class: 'in-gris', text: P.gestion.texto }));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- enfoque de la semana
|
||||
const ORDEN_EDITORES = ['leo', 'adriana'];
|
||||
function fechaLarga(iso) {
|
||||
const f = String(iso || '').slice(0, 10); if (!f) return '';
|
||||
const d = new Date(f + 'T12:00:00');
|
||||
return DIAS[d.getDay()] + ' ' + d.getDate() + ' de ' + MESES[d.getMonth()];
|
||||
}
|
||||
function cuandoEditado(iso) {
|
||||
const f = String(iso || '').slice(0, 10), h = String(iso || '').slice(11, 16);
|
||||
if (f === hoyISO()) return 'hoy a las ' + h;
|
||||
if (f === hoyISO(-1)) return 'ayer a las ' + h;
|
||||
return 'el ' + fechaLarga(iso) + (h ? ' a las ' + h : '');
|
||||
}
|
||||
|
||||
function pintarEnfoque(P, r) {
|
||||
const c = document.querySelector('#in-enfoque .in-enfoque-c'); c.innerHTML = '';
|
||||
const h = r && r.enfoque, e = h || P.enfoque_inicial || { titulo: 'Sin enfoque escrito todavía.', puntos: [] };
|
||||
const editores = ((r && r.editores) || ORDEN_EDITORES).slice().sort((a, b) => ((ORDEN_EDITORES.indexOf(a) + 1 || 9) - (ORDEN_EDITORES.indexOf(b) + 1 || 9)));
|
||||
const cab = el('div', { class: 'in-enfoque-cab' });
|
||||
cab.append(el('span', { class: 'in-enfoque-cuando', text: 'Enfoque de la semana' + (h ? '. Escrito el ' + fechaLarga(h.editado_en) : '') }));
|
||||
if (r && r.puede_editar) {
|
||||
const b = el('button', { type: 'button', class: 'sec in-editar', html: svg(LAPIZ) + 'Editar' });
|
||||
b.addEventListener('click', () => editarEnfoque(P, r, e));
|
||||
cab.append(b);
|
||||
}
|
||||
c.append(cab, el('h2', { id: 'enfoque-h', text: e.titulo }));
|
||||
if ((e.puntos || []).length) { const ul = el('ul'); e.puntos.forEach(p => ul.append(el('li', { html: enLinea(p) }))); c.append(ul); }
|
||||
const pie = el('p', { class: 'in-enfoque-pie' });
|
||||
if (h) pie.textContent = 'Lo escriben ' + lista(editores.map(nombreCorto)) + '. Editado ' + cuandoEditado(h.editado_en) + ' por ' + nombreCorto(h.editado_por) + '.';
|
||||
else if (P.enfoque_inicial && P.enfoque_inicial.nota) pie.innerHTML = enLinea(P.enfoque_inicial.nota);
|
||||
if (!r) pie.append(el('span', { class: 'in-gris', text: ' (El servidor no respondió: este es el texto inicial.)' }));
|
||||
c.append(pie);
|
||||
}
|
||||
|
||||
function editarEnfoque(P, r, e) {
|
||||
const c = document.querySelector('#in-enfoque .in-enfoque-c'); c.innerHTML = '';
|
||||
const f = el('form', { class: 'in-enfoque-ed' });
|
||||
const tit = el('input', { type: 'text', maxlength: '160', id: 'enf-tit', required: '' }); tit.value = e.titulo || '';
|
||||
const pts = el('textarea', { rows: '6', id: 'enf-pts' }); pts.value = (e.puntos || []).join('\n');
|
||||
const msg = el('div', { class: 'msg', role: 'status' });
|
||||
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' });
|
||||
const guardar = el('button', { type: 'submit', text: 'Guardar' });
|
||||
f.append(el('div', { class: 'in-enfoque-cab' }, el('span', { class: 'in-enfoque-cuando', text: 'Editar el enfoque de la semana' })),
|
||||
el('label', { for: 'enf-tit', text: 'Frase de la semana (una línea)' }), tit,
|
||||
el('label', { for: 'enf-pts', text: 'Puntos: uno por línea, hasta 8' }), pts,
|
||||
el('p', { class: 'ayuda', html: 'Se puede usar <code>**negritas**</code>, <code>`código`</code> y <code>[ligas](/hardware/…/)</code>. Se guarda con tu nombre y la hora; lo anterior queda en el historial del servidor.' }),
|
||||
msg, el('div', { class: 'in-enfoque-botones' }, cancelar, guardar));
|
||||
c.append(f);
|
||||
tit.focus();
|
||||
cancelar.addEventListener('click', () => pintarEnfoque(P, r));
|
||||
f.addEventListener('submit', async ev => {
|
||||
ev.preventDefault();
|
||||
const d = { titulo: tit.value.trim(), puntos: pts.value.split('\n').map(x => x.trim()).filter(Boolean) };
|
||||
if (!d.titulo) { msg.className = 'msg err'; msg.textContent = 'Escribe la frase de la semana.'; return; }
|
||||
if (d.puntos.length > 8) { msg.className = 'msg err'; msg.textContent = 'Son ' + d.puntos.length + ' puntos; deja 8 o menos.'; return; }
|
||||
guardar.disabled = true;
|
||||
try {
|
||||
const j = await json('/api/hoy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
|
||||
r = Object.assign({}, r, { enfoque: j.enfoque });
|
||||
pintarEnfoque(P, r);
|
||||
} catch (err) {
|
||||
msg.className = 'msg err'; msg.textContent = 'No se guardó: ' + err.message;
|
||||
guardar.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- próximas fechas
|
||||
function pintarFechas(P, tareas) {
|
||||
const cont = $('in-hitos'); cont.innerHTML = '';
|
||||
const hoy = hoyISO(), tope = hoyISO(10);
|
||||
const filas = hitosVigentes(P).map(h => ({ fecha: h.fecha, cuando: h.cuando, texto: h.texto, quien: h.quien || [], hito: true }));
|
||||
(tareas || []).forEach(t => {
|
||||
if (t.estado === 'hecho' || !t.fecha_limite || t.fecha_limite < hoy || t.fecha_limite > tope) return;
|
||||
filas.push({ fecha: t.fecha_limite, cuando: t.fecha_limite === hoy ? 'Hoy' : fechaCorta(t.fecha_limite), texto: t.titulo, quien: window.Kanban.idsResp(t), id: t.id });
|
||||
});
|
||||
filas.sort((a, b) => ((a.fecha ? 1 : 0) - (b.fecha ? 1 : 0)) || String(a.fecha).localeCompare(String(b.fecha)));
|
||||
if (!filas.length) { cont.append(el('p', { class: 'vacio', text: 'Nada con fecha en los próximos 10 días.' })); return; }
|
||||
const ol = el('ol', { class: 'in-fechas' });
|
||||
filas.slice(0, 7).forEach(x => {
|
||||
const li = el('li', { class: x.hito ? 'es-hito' : 'es-pendiente' });
|
||||
li.append(el('span', { class: 'in-f' + (x.cuando === 'Hoy' ? ' hoy' : '') + (x.fecha ? '' : ' sin'), text: x.cuando }));
|
||||
li.append(x.id ? el('a', { class: 'in-t', href: '/tableros/pendientes/?id=' + encodeURIComponent(x.id), text: x.texto }) : el('span', { class: 'in-t', text: x.texto }));
|
||||
li.append(avatares(x.quien));
|
||||
ol.append(li);
|
||||
});
|
||||
cont.append(ol);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- documentos recientes
|
||||
async function pintarDocs() {
|
||||
const cont = $('in-docs');
|
||||
try {
|
||||
const j = await json('/datos/documentos.json');
|
||||
cont.innerHTML = '';
|
||||
const docs = (j.documentos || []).slice(0, 6);
|
||||
if (!docs.length) { cont.append(el('p', { class: 'vacio', text: 'Sin documentos con fecha.' })); return; }
|
||||
const ol = el('ol', { class: 'in-docs' });
|
||||
docs.forEach(d => ol.append(el('li', null, el('a', { href: d.url, html: svg(DOC) + '<span>' + esc(d.titulo) + '</span>' }), el('span', { class: 'in-f', text: fechaCorta(d.fecha) }))));
|
||||
cont.append(ol);
|
||||
} catch (err) { falla(cont, 'la lista de documentos', err); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- dónde estamos
|
||||
async function pintarMapa(P) {
|
||||
const cont = $('in-mapa'), ul = $('in-sitios');
|
||||
try {
|
||||
const r = await fetch('/img/mexico.svg', { cache: 'force-cache' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const doc = new DOMParser().parseFromString(await r.text(), 'image/svg+xml');
|
||||
const mapa = doc.documentElement;
|
||||
if (mapa.nodeName.toLowerCase() !== 'svg') throw new Error('mapa inválido');
|
||||
mapa.removeAttribute('width'); mapa.removeAttribute('height');
|
||||
mapa.setAttribute('role', 'img'); mapa.setAttribute('aria-label', 'Mapa de México con los puertos y la oficina de Querétaro');
|
||||
cont.innerHTML = '';
|
||||
cont.append(document.importNode(mapa, true));
|
||||
} catch (err) { cont.classList.add('sin-mapa'); }
|
||||
const pin = (s, oficina) => {
|
||||
const p = el('span', { class: 'in-pin' + (oficina ? ' oficina' : '') + ' et-' + (s.etiqueta || 'der'), style: 'left:' + s.x + '%;top:' + s.y + '%' });
|
||||
p.append(el('span', { class: 'in-pin-p', 'aria-hidden': 'true' }), el('span', { class: 'in-pin-t', text: s.nombre }));
|
||||
return p;
|
||||
};
|
||||
(P.sitios || []).forEach(s => cont.append(pin(s)));
|
||||
if (P.oficina) cont.append(pin(Object.assign({ etiqueta: 'arriba' }, P.oficina), true));
|
||||
ul.innerHTML = '';
|
||||
(P.sitios || []).forEach(s => {
|
||||
const estado = el('span', { class: 'in-gris', text: '…' });
|
||||
ul.append(el('li', null, el('a', { href: '/tableros/sitio/?id=' + encodeURIComponent(s.id) }, el('b', { text: s.nombre }), ' ' + s.ciudad), estado));
|
||||
json('/api/sitio/unidades?id=' + encodeURIComponent(s.id)).then(j => {
|
||||
const n = j.total_unidades || 0, inst = (j.unidades || []).filter(u => u.instalada).length;
|
||||
const partes = n ? [plural(n, 'unidad con actas', 'unidades con actas'), inst ? plural(inst, 'instalada', 'instaladas') : 'ninguna instalada'] : ['Sin unidades registradas'];
|
||||
if (s.nota) partes.push(s.nota);
|
||||
estado.textContent = partes.join(' · ');
|
||||
}).catch(() => { estado.textContent = s.nota || ''; });
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- arranque
|
||||
async function iniciar() {
|
||||
if (!$('inicio') || !window.Kanban) return;
|
||||
const K = window.Kanban;
|
||||
let P;
|
||||
try { P = await json('/datos/proyecto.json'); }
|
||||
catch (err) { falla($('in-ruta'), 'el estado del proyecto', err); return; }
|
||||
|
||||
pintarFase(P);
|
||||
pintarHito(P);
|
||||
pintarFechas(P, []);
|
||||
window.Ruta.montar($('in-ruta'), P, { enlace: t => '/tableros/ingenieria/#' + t.clave });
|
||||
|
||||
// tablero de pendientes (extracto) y lo que depende de él
|
||||
K.montar($('in-kb'), {
|
||||
filtros: 'min', vistas: true, compacto: true, limite: { otros: 4, hecho: 3 }, limitePersona: 2,
|
||||
enlaceMas: '/tableros/pendientes/', nuevoPorColumna: false, barraEn: $('in-pend-acc'),
|
||||
onCargado: tareas => pintarFechas(P, tareas),
|
||||
onPintado: (vis, info) => {
|
||||
const ab = vis.filter(t => t.estado !== 'hecho').length, ve = vis.filter(t => t.vencida).length;
|
||||
$('in-resumen').textContent = plural(ab, 'abierto', 'abiertos') + (ve ? ', ' + plural(ve, 'vencido', 'vencidos') : '') + (info.filtro.mios ? ', solo los tuyos' : '');
|
||||
}
|
||||
});
|
||||
K.cargarEquipo().then(() => pintarEquipo(P));
|
||||
// el enfoque espera al equipo para escribir los nombres («Lo escriben Leo y Adriana»)
|
||||
Promise.all([json('/api/hoy').catch(() => null), K.cargarEquipo().catch(() => null)]).then(([r]) => pintarEnfoque(P, r));
|
||||
Promise.all([json('/api/plan').catch(() => null), json('/api/actas').catch(() => null)])
|
||||
.then(([p, a]) => pintarPedido(P, p && p.plan, a ? (a.actas || []) : null));
|
||||
window.Actividad.montar($('in-actividad'), { dias: 7, limite: 6, corto: 46, mas: '/bitacora/#actividad' });
|
||||
pintarDocs();
|
||||
pintarMapa(P);
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', iniciar); else iniciar();
|
||||
})();
|
||||
@ -79,6 +79,12 @@
|
||||
--hg-aviso: #F2C46B; --hg-aviso-bg: #3A3020;
|
||||
--hg-info: #9DB8FF; --hg-info-bg: #1B2847;
|
||||
--hg-pend: #8C99B5; --hg-pend-bg: #1F2A42;
|
||||
--hg-halo-curso: rgba(81, 243, 218, 0.22);
|
||||
--hg-halo-bloqueado: rgba(255, 148, 148, 0.22);
|
||||
|
||||
/* ruta a producción: lo hecho va en blanco (21-oct: «la tarjeta en blanco, no en verde») */
|
||||
--hg-ruta: #F2F5FA;
|
||||
--hg-ruta-linea: rgba(242, 245, 250, 0.82);
|
||||
|
||||
/* pistas del tablero */
|
||||
--hg-p-ingenieria: #8FB0FF;
|
||||
@ -140,6 +146,11 @@ html[data-tema="claro"],
|
||||
--hg-aviso: #8A5A00; --hg-aviso-bg: #FFF3D6;
|
||||
--hg-info: #2F55B5; --hg-info-bg: #E6EDFC;
|
||||
--hg-pend: #65728D; --hg-pend-bg: #E8ECF3;
|
||||
--hg-halo-curso: rgba(20, 104, 92, 0.18);
|
||||
--hg-halo-bloqueado: rgba(180, 35, 24, 0.16);
|
||||
|
||||
--hg-ruta: #1F2A40;
|
||||
--hg-ruta-linea: rgba(31, 42, 64, 0.78);
|
||||
|
||||
--hg-p-ingenieria: #3D63C9;
|
||||
--hg-p-produccion: #1E8A5E;
|
||||
|
||||
156
docs/tableros/actividad.js
Normal file
@ -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 };
|
||||
})();
|
||||
@ -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 |
|
||||
|
||||
@ -8,19 +8,23 @@ hide:
|
||||
|
||||
<div class="tb tb-ingenieria">
|
||||
<header class="tb-cab">
|
||||
<div class="tb-cab__t"><h1>Ingeniería</h1><span class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6 — estado al 19-oct-2026</span></div>
|
||||
<div class="tb-cab__t"><h1>Ingeniería</h1><span class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6</span></div>
|
||||
</header>
|
||||
|
||||
<details class="info tb-como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>Arriba, una tarjeta por versión de EinarBoard: estado, siguiente paso, responsable y sus documentos. Es texto fijo: cuando cambie una decisión, pide a Leo o a Claude que lo actualice aquí.</li>
|
||||
<li>Arriba, la <b>ruta a producción</b> (los siete pasos de cada versión) y una tarjeta por versión de EinarBoard: estado, siguiente paso, responsables y sus documentos. Es texto fijo de <code>docs/datos/proyecto.json</code>, el mismo que usa la página de inicio: cuando cambie una decisión, pide a Leo o a Claude que lo actualice ahí.</li>
|
||||
<li>En medio, los <b>pendientes de la pista Ingeniería</b>: son los mismos del <a href="/tableros/pendientes/">tablero de pendientes</a>, filtrados. Puedes moverlos desde aquí.</li>
|
||||
<li>Abajo, las últimas decisiones con fecha y la filosofía de diseño. Antes de proponer un cambio, revisa que no contradiga una decisión ya tomada.</li>
|
||||
<li>Para una tarjeta nueva o un cambio de pedido, primero el pendiente, luego el trabajo: así queda el rastro de quién y cuándo.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<h2 class="ing-h">Ruta a producción</h2>
|
||||
<p class="sub">Línea continua: hecho. Punteada: lo que falta. El anillo marca el paso en el que va cada tarjeta. En cada imagen, lo nuevo de la revisión va en blanco.</p>
|
||||
<div id="ruta" class="ing-ruta"><p class="vacio">…</p></div>
|
||||
|
||||
<div class="versiones" id="versiones"></div>
|
||||
|
||||
<section class="bloque">
|
||||
@ -33,6 +37,8 @@ hide:
|
||||
<h2>Últimas decisiones</h2>
|
||||
<p class="sub">Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.</p>
|
||||
<ul class="decisiones">
|
||||
<li><b>21-oct (noche)</b> — <b>Pasada 5 revisada</b> (<code>6458c5d</code>): C129 y C130 quedaron bien en las dos, pero la vía de entrada de RS1 quedó encima del hilo de sensado de U6.6: <b>el sensado ya no es Kelvin</b> (el límite de entrada baja ≈ 3–4 %). Falta la <b>pasada 6</b>, ≈ 30 min por tarjeta; en la V5.7, además, girar C149/C150 (lazo de ≈ 56 mm²). <b>v6:</b> T25 y T28 aplicados sobre <code>9255b84</code> (falta el push de Leo); T28 cerró 2 de los 8 bloqueantes, pero D34 quedó a 13 mm del pin SW de U25 (lazo caliente de ≈ 115 mm², antes ≈ 47) y MH8 cae encima de D34. <a href="/hardware/v56-v57-auditoria-21oct/#5-pasada-5-revisada-6458c5d-y-lo-que-falta">Pasada 5 revisada</a> · <a href="/hardware/v6-estado-21oct/">Estado de la v6</a></li>
|
||||
<li><b>21-oct (tarde)</b> — <b>Auditoría de la pasada 4 de Michelle</b> (HBox_PCB <code>85e7602</code> V5.6, <code>1416dcb</code> V5.7). <b>V5.6:</b> la pasada dejó un corto nuevo (<code>/MODEM_EN</code> a GND en R106) y una pista de <code>/SYS5V</code> a 0.09 mm de una vía; el giro de U7 mejoró el nodo SW (L7 a 4 mm de U7). <b>V5.7:</b> ya sin el bloqueante de K1. <b>D-21-1, aprobada por Leo:</b> C130 de 100 nF en la entrada de U7, en las dos. En la tarde quedaron <b>T32</b> (C130 en los esquemáticos) y <b>T33</b> (el corto, la separación, la 2.ª vía de VSYS y las vías de potencia): DRC solo con los 3 errores de MOD1. Falta la <b>pasada 5</b> de Michelle (V5.6 ≈ 1 h, V5.7 ≈ medio día); después, revisión del cobre y Gerber. <a href="/hardware/v56-v57-auditoria-21oct/">Auditoría del 21-oct</a></li>
|
||||
<li><b>19-oct (19:30)</b> — <b>V5.6: qué es crítico y qué se puede quedar</b> (archivo de trabajo de las 18:59, sobre T30). <b>Un solo crítico, nuevo:</b> U6 (LTC4040) no tiene capacitor en sus pines VSYS; el más cercano, C34, está a 11.5 mm (viene desde la V5.5). La hoja de datos lo pone por encima de todo en el layout. Se agrega <b>C129</b> (10 µF 0603, C19702) junto a los pines 1/24, sin mover piezas. Regresar 2 vías de <code>/MAIN5V</code> y 2 de <code>/3V3_MODEM</code> que se perdieron al guardar. L7 lejos de U7 y lo heredado de la V5.5 (L6, L8, Dbk1) se quedan para el piloto, y se miden en el banco. <a href="/hardware/v56-clasificacion-19oct/">Clasificación</a></li>
|
||||
<li><b>19-oct (17:10)</b> — <b>Re-auditoría V5.6 / V5.7 / v6</b> sobre la pasada 3 de Michelle (HBox_PCB <code>4f4681f</code>). <b>V5.6 casi lista</b>: <code>/BAT_PLUS</code> fuera de capas internas, <code>/3V3_MODEM</code> a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto; faltan T30 + ≈ 1 h (L7/Dbk2, salida VSYS de U6, 3 vías, exclusiones del DRC). <b>V5.7 NO LISTA</b>: ya replica la V5.6, pero <code>/SYS5V</code> va 10.9 mm por In2 a 0.5 mm bajo K1 (bloqueante) y el lazo de entrada de U25 mide 52 mm². <b>v6 sin avance</b> en sus 8 bloqueantes (solo 15 vías GND): plan recorrido una semana, piloto de 20 el lun 2-nov; pre-órdenes de JLC ya. <b>Script T30</b> probado en copias en la Mac (DRC sin infracciones nuevas, 0 sin conectar). Merge del lunes (<code>40decf8</code>) arreglado por Leo (<code>240e2c3</code> + <code>merge=binary</code>) y verificado objeto por objeto. Decisiones para Leo: D-19-1 puertos (si van, entran al piloto), D-19-2 Dbk2, D-19-3 sacar KiCad del auto-sync de las 18:00, D-19-4 clase HV24. <a href="/hardware/re-auditoria-19oct/">Re-auditoría</a> · <a href="/hardware/v56-v57-layout-pasada-4/">Pasada 4</a> · <a href="/hardware/v6-plan-fines-de-semana/">Plan v6 rev. 1.1</a></li>
|
||||
<li><b>16-oct (17:30)</b> — <b>Auditoría de producción de la v6: NO LISTA.</b> Ruteo completo (<code>2612891</code>, DRC 0 errores), pero: U25 con el código del AP64350, sin barrenos, 5 V por In1/In2 a 0.5 mm, cargador con pistas de 0.5 mm y 1 vía en el EP, RF del módem por In1, 81 vías de tierra, stackup genérico. Scripts listos: T25 <code>--set v6</code> y <b>T28</b> (probados con DRC). <b>Plan de fines de semana</b>: layout el 17–18 y el 24–25 oct, pedido del piloto de 20 el 26-oct, firmware y banco hasta ≈ 15-nov. P7/P8 se quitan (decisión del 9-oct). <a href="/hardware/v6-auditoria-produccion-16oct/">Auditoría v6</a> · <a href="/hardware/v6-plan-fines-de-semana/">Plan</a></li>
|
||||
@ -54,7 +60,7 @@ hide:
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="bloque">
|
||||
<section class="bloque" id="filosofia">
|
||||
<h2>Filosofía de diseño</h2>
|
||||
<p class="sub">Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en <a href="/hardware/">Tarjetas y hardware</a>.</p>
|
||||
<ul class="filosofia">
|
||||
@ -67,49 +73,52 @@ hide:
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/tableros/ruta.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const VERSIONES = [
|
||||
{ v: 'V5.5', cls: 'v55', badge: 'badge-v55', estado: 'En campo — 15 pzas (lote 22-ago), fw 0.6.x',
|
||||
siguiente: 'Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a V5.6. Inspeccionar U6 con lupa en cada tarjeta. Medir en una unidad el límite de entrada del cargador (≈ 1 A, auditoría 16-oct §4.3).',
|
||||
quien: 'Jesús (pruebas) · Alan y Daniel (campo)',
|
||||
docs: [['Manual V5', '/manuales/manual-tarjeta-V5/'], ['Auditoría 16-oct §4', '/hardware/auditoria-produccion-16oct/'], ['Lecciones V5.5 → v6', '/hardware/v6-revision-lecciones-v55/'], ['Acta de tarjeta V5', '/formularios/v5.html']] },
|
||||
{ v: 'V5.6', cls: 'v56', badge: 'badge-v56', estado: 'Casi lista: T30 aplicado (7650b96); falta la pasada 4, parte A, con un crítico nuevo: C129 en VSYS de U6 (19-oct, tarde)',
|
||||
siguiente: 'Pasada 4, parte A: primero C129 junto a VSYS de U6 (crítico); regresar 4 vías perdidas; quitar la rama de Dbk2; exclusiones del DRC → Claude re-audita y genera Gerber/BOM/CPL → pedido del piloto de 5 (≈ 21–22-oct, con los 5 de V5.7) → banco con firmware 0.7.0 (incluye medir VSYS, el riel del módem y el GNSS) → las 108 restantes (con SD-15B). Antes del pedido: decisión D-19-1 (puertos).',
|
||||
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido, firmware) · Jesús (banco)',
|
||||
docs: [['Qué es crítico (19-oct, tarde)', '/hardware/v56-clasificacion-19oct/'], ['Pasada 4 — V5.6 y V5.7', '/hardware/v56-v57-layout-pasada-4/'], ['Re-auditoría 19-oct', '/hardware/re-auditoria-19oct/'], ['Auditoría 16-oct (requisitos)', '/hardware/auditoria-produccion-16oct/'], ['ECO V5.6', '/hardware/v56-eco/'], ['Hoja de datos', '/hardware/v56-datasheet/'], ['Pinout', '/hardware/v56-pinout/']] },
|
||||
{ v: 'V5.7', cls: 'v57', badge: 'badge-v57', estado: 'Re-auditada 19-oct — NO LISTA: pasada 3 hecha (f3f35f4), ya replica la V5.6; bloqueante /SYS5V por In2 bajo K1; lazo de entrada de U25 de 52 mm²',
|
||||
siguiente: 'Lun 19: T30 (Leo). Mar 20 – mié 21: Michelle pasada 4, parte B (≈ medio día: /SYS5V fuera de In2, C149/C150 y L22 junto a U25, lo mismo que V5.6) → re-auditoría → 5 prototipos con el piloto de V5.6 (ECO §6: 9/12/24/32 V, polaridad, temperatura) → pedido 327. Entrada 9–32 V.',
|
||||
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido)',
|
||||
docs: [['Pasada 4 — parte B', '/hardware/v56-v57-layout-pasada-4/#parte-b-v57-hbox-watchdog-v57-medio-dia'], ['Re-auditoría 19-oct §4', '/hardware/re-auditoria-19oct/#4-v57-punto-por-punto'], ['ECO V5.7 — fuente a bordo', '/hardware/v57-eco-fuente/']] },
|
||||
{ v: 'v6', cls: 'v6', badge: 'badge-v6', estado: 'Re-auditada 19-oct: NO LISTA, sin cambio — solo 15 vías de tierra nuevas (00a9af4); siguen los 8 bloqueantes del 16-oct',
|
||||
siguiente: 'Lun 19 – mar 20: pre-órdenes JLC (THVD1410DR, TPS7A2033PDBVR, J14). Sáb 24 8:00: Leo corre T25 --set v6 + T28. Sáb 24: Michelle bloques A y B (potencia) + MH4/MH6. Dom 25: bloques C, D y E (RF del módem, U20, GNSS, U12). Sáb 31 – dom 1-nov: pares, puertos, In1, T29 (vías GND), serigrafía, DRC final. Lun 2-nov: piloto de 20. Firmware v6 los fines de semana 7–8 y 14–15 nov; banco 21–22 nov (G2). No habrá v6.1.',
|
||||
quien: 'Michelle (layout) · Leo · Claude · Adriana (pre-órdenes)',
|
||||
docs: [['Plan de fines de semana (rev. 1.1)', '/hardware/v6-plan-fines-de-semana/'], ['Auditoría v6 (16-oct)', '/hardware/v6-auditoria-produccion-16oct/'], ['Re-auditoría 19-oct §8', '/hardware/re-auditoria-19oct/#8-v6'], ['Plan de corrección (28-sep)', '/hardware/v6-plan-correccion/'], ['Manual v6', '/manuales/manual-tarjeta-v6/'], ['Plan de cobre', '/hardware/EinarBoard-v6_Plan-de-Cobre_ES.html']] },
|
||||
];
|
||||
// Ruta y tarjetas por versión: docs/datos/proyecto.json (el mismo archivo que usa la página de inicio).
|
||||
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
|
||||
const cont = document.getElementById('versiones');
|
||||
const contadores = {};
|
||||
VERSIONES.forEach(x => {
|
||||
const c = el('div', { class: 'ver ' + x.cls });
|
||||
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.v }), el('span', { class: x.badge, text: x.v })));
|
||||
c.append(el('div', { class: 'est', text: x.estado }));
|
||||
c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' + x.siguiente }));
|
||||
const qd = el('div', { class: 'quien', html: Kanban.icono('equipo') }); qd.append(el('span', { text: x.quien })); c.append(qd);
|
||||
const d = el('div', { class: 'docs' }); x.docs.forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
|
||||
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.v] = cnt; c.append(cnt);
|
||||
cont.append(c);
|
||||
let TAREAS = null;
|
||||
|
||||
function contar() {
|
||||
if (!TAREAS) return;
|
||||
Object.keys(contadores).forEach(v => {
|
||||
const mios = TAREAS.filter(t => t.version_tarjeta === v && t.estado !== 'hecho');
|
||||
const bloq = mios.filter(t => t.estado === 'bloqueado').length, venc = mios.filter(t => t.vencida).length;
|
||||
const n = (k, a, b) => k + ' ' + (k === 1 ? a : b);
|
||||
const a = el('a', { href: '/tableros/pendientes/?pista=ingenieria', text: n(mios.length, 'pendiente abierto', 'pendientes abiertos') + (bloq ? ' · ' + n(bloq, 'bloqueado', 'bloqueados') : '') + (venc ? ' · ' + n(venc, 'vencido', 'vencidos') : '') });
|
||||
contadores[v].innerHTML = ''; contadores[v].append(a);
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/datos/proyecto.json', { cache: 'no-store' }).then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(P => {
|
||||
const m = String(P.actualizado || '').match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (m) document.getElementById('meta').textContent = 'V5.5 · V5.6 · V5.7 · v6, al día el ' + (+m[3]) + ' ' + MES[+m[2] - 1];
|
||||
Ruta.montar(document.getElementById('ruta'), P, { enlace: t => '#' + t.clave });
|
||||
(P.tarjetas || []).forEach(x => {
|
||||
const c = el('div', { class: 'ver ' + x.clave, id: x.clave });
|
||||
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.version }), el('span', { class: BADGE[x.version] || 'badge-v4', text: x.version })));
|
||||
c.append(el('div', { class: 'est', text: x.detalle || x.estado || '' }));
|
||||
if (x.siguiente) c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' }, document.createTextNode(x.siguiente)));
|
||||
if (x.quien) { const qd = el('div', { class: 'quien', html: Kanban.icono('equipo') }); qd.append(el('span', { text: x.quien })); c.append(qd); }
|
||||
const d = el('div', { class: 'docs' }); (x.docs || []).forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
|
||||
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.version] = cnt; c.append(cnt);
|
||||
cont.append(c);
|
||||
});
|
||||
contar();
|
||||
if (location.hash) { const t = document.getElementById(location.hash.slice(1)); if (t) t.scrollIntoView(); }
|
||||
}).catch(e => {
|
||||
document.getElementById('ruta').innerHTML = '';
|
||||
document.getElementById('ruta').append(el('div', { class: 'err', text: 'No se pudo leer el estado de las tarjetas (' + e.message + ').' }));
|
||||
});
|
||||
|
||||
Kanban.montar(document.getElementById('kb'), {
|
||||
pista: 'ingenieria', filtros: true,
|
||||
onCargado: tareas => {
|
||||
VERSIONES.forEach(x => {
|
||||
const mios = tareas.filter(t => t.version_tarjeta === x.v && t.estado !== 'hecho');
|
||||
const bloq = mios.filter(t => t.estado === 'bloqueado').length, venc = mios.filter(t => t.vencida).length;
|
||||
const a = el('a', { href: '/tableros/pendientes/?pista=ingenieria', text: mios.length + ' pendiente(s) abierto(s)' + (bloq ? ' · ' + bloq + ' bloqueado(s)' : '') + (venc ? ' · ' + venc + ' vencido(s)' : '') });
|
||||
contadores[x.v].innerHTML = ''; contadores[x.v].append(a);
|
||||
});
|
||||
}
|
||||
pista: 'ingenieria', filtros: true, vistas: true,
|
||||
onCargado: tareas => { TAREAS = tareas; contar(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@ -5,11 +5,20 @@
|
||||
Uso: const kb = Kanban.montar(document.getElementById('kb'), {
|
||||
pista: 'ingenieria', // opcional: fija la pista (oculta su filtro y la precarga en "Nuevo")
|
||||
filtros: true, // barra de filtros (responsable / versión / buscar / pista / solo los míos)
|
||||
compacto: false, // tarjetas más pequeñas (tableros que solo muestran un extracto)
|
||||
compacto: false, // tarjetas sin ligas ni etiquetas (la página de inicio)
|
||||
botonNuevoEn: elemento, // opcional: dónde va el botón «Nuevo pendiente» (p. ej. el encabezado)
|
||||
onCargado: (tareas, meta) => {} // callback después de cada carga
|
||||
onCargado: (tareas, meta) => {}, // callback después de cada carga
|
||||
// agregados el 21-oct (página de inicio y vista por persona):
|
||||
vistas: true, // botones «Tablero · Por persona» (?vista=persona en la dirección)
|
||||
limite: 4, // máximo de pendientes por columna; el resto, «N más» → enlaceMas
|
||||
limitePersona: 3, // máximo por celda en la vista por persona
|
||||
enlaceMas: '/tableros/pendientes/', // a dónde llevan los «N más» (sin él, se despliegan ahí)
|
||||
nuevoPorColumna: true, // el «+ Nuevo» al pie de cada columna
|
||||
barraEn: elemento, // los botones de la barra van dentro de este elemento (filtros: 'min')
|
||||
onPintado: (visibles, info) => {} // después de cada pintado: lo que se ve con los filtros
|
||||
});
|
||||
kb.recargar();
|
||||
filtros: true = barra completa · 'min' = solo vistas, «Solo los míos» y «Nuevo» · false = sin barra.
|
||||
|
||||
Servidor: GET /api/tareas · POST /api/tareas · PATCH /api/tarea?id= · POST /api/tareas/orden ·
|
||||
DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial.
|
||||
@ -51,7 +60,8 @@
|
||||
flecha: 'M5 12h14M13 6l6 6-6 6',
|
||||
unidad: 'M5 7.5 12 4l7 3.5v9L12 20l-7-3.5zM5 7.5l7 3.5 7-3.5M12 11v9',
|
||||
actualizar: 'M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4',
|
||||
imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z'
|
||||
imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z',
|
||||
columnas: 'M5 4.5h14a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 18V6A1.5 1.5 0 0 1 5 4.5zM9.3 4.5v15M14.7 4.5v15'
|
||||
};
|
||||
const icono = (n, cls) => '<svg class="i' + (cls ? ' ' + cls : '') + '" viewBox="0 0 24 24" aria-hidden="true"><path d="' + (ICONOS[n] || '') + '"/></svg>';
|
||||
// Íconos de estado de las columnas (con relleno; colores de los tokens).
|
||||
@ -66,14 +76,16 @@
|
||||
const norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim().replace(/[^a-z0-9_\-.]/g, '');
|
||||
let EQUIPO = {};
|
||||
let equipoProm = null;
|
||||
// junta los datos de cada persona sin borrar lo que ya se sabía (p. ej. el rol)
|
||||
function fundir(mapa) { Object.keys(mapa || {}).forEach(id => { EQUIPO[id] = Object.assign({}, EQUIPO[id] || {}, mapa[id]); }); return EQUIPO; }
|
||||
function cargarEquipo() {
|
||||
if (!equipoProm) equipoProm = fetch('/api/equipo?todos=1', { cache: 'no-store' }).then(r => r.json()).then(j => {
|
||||
const m = {}; (j.miembros || []).forEach(x => m[x.id] = { nombre: x.nombre, activo: x.activo !== false, area: x.area, tiene_login: x.tiene_login });
|
||||
const m = {}; (j.miembros || []).forEach(x => m[x.id] = { nombre: x.nombre, activo: x.activo !== false, area: x.area, rol: x.rol || '', tiene_login: x.tiene_login });
|
||||
return m;
|
||||
}).catch(() => ({})).then(m => { if (!Object.keys(m).length) RESPONSABLES.forEach(id => m[id] = { nombre: id, activo: true }); EQUIPO = Object.assign(EQUIPO, m); return EQUIPO; });
|
||||
}).catch(() => ({})).then(m => { if (!Object.keys(m).length && !Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => m[id] = { nombre: id, activo: true }); return fundir(m); });
|
||||
return equipoProm;
|
||||
}
|
||||
function usarEquipo(mapa) { if (mapa && Object.keys(mapa).length) EQUIPO = Object.assign(EQUIPO, mapa); if (!Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => EQUIPO[id] = { nombre: id, activo: true }); return EQUIPO; }
|
||||
function usarEquipo(mapa) { if (mapa && Object.keys(mapa).length) fundir(mapa); if (!Object.keys(EQUIPO).length) RESPONSABLES.forEach(id => EQUIPO[id] = { nombre: id, activo: true }); return EQUIPO; }
|
||||
const partesResp = t => String((t && t.responsable) || '').split(/[,;\/]+/).map(x => x.trim()).filter(Boolean);
|
||||
// nombres a mostrar: cada parte del campo responsable → nombre del miembro, o el texto tal cual
|
||||
function nombresResp(t, eq) {
|
||||
@ -86,6 +98,12 @@
|
||||
return partesResp(t).map(p => { const a = norm(p.replace(/\s+/g, '')), b = norm(p); return eq[a] ? a : (eq[b] ? b : null); }).filter(Boolean);
|
||||
}
|
||||
const libresResp = (t, eq) => { eq = eq || EQUIPO; return partesResp(t).filter(p => !eq[norm(p.replace(/\s+/g, ''))] && !eq[norm(p)]); };
|
||||
// nombre corto para frases («Leo movió…»): el primer nombre, con los apodos de la casa
|
||||
const APODOS = { leopoldo: 'Leo' };
|
||||
function nombreCorto(id) {
|
||||
const n = String((EQUIPO[id] || {}).nombre || id || '').trim().split(/\s+/)[0];
|
||||
return APODOS[norm(n)] || (EQUIPO[id] ? n : n.charAt(0).toUpperCase() + n.slice(1));
|
||||
}
|
||||
|
||||
// ---- avatares: iniciales sobre el color de la persona
|
||||
function iniciales(nombre) {
|
||||
@ -140,8 +158,28 @@
|
||||
let tareas = [], admin = false, user = '', porId = {};
|
||||
const q0 = new URLSearchParams(location.search);
|
||||
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1' };
|
||||
const vistasOn = !!opts.vistas;
|
||||
let vista = vistasOn && (q0.get('vista') === 'persona' || (!q0.get('vista') && opts.vista === 'persona')) ? 'persona' : 'tablero';
|
||||
const desplegadas = new Set(); // celdas de la vista por persona que ya muestran todo
|
||||
root.classList.add('kanban-root');
|
||||
const contenedor = root.closest('.tb') || root;
|
||||
// los filtros que vale la pena compartir viajan en la dirección (?mios=1, ?vista=persona)
|
||||
function ponURL(k, v) {
|
||||
if (opts.url === false) return;
|
||||
const u = new URL(location.href);
|
||||
if (v) u.searchParams.set(k, v); else u.searchParams.delete(k);
|
||||
history.replaceState(null, '', u.pathname + u.search + u.hash);
|
||||
}
|
||||
// dirección del tablero completo con lo que se está viendo (para los «N más» de la página de inicio)
|
||||
function enlaceCompleto(extra) {
|
||||
const u = new URLSearchParams();
|
||||
if (filtro.mios) u.set('mios', '1');
|
||||
if (vista === 'persona') u.set('vista', 'persona');
|
||||
if (opts.pista) u.set('pista', opts.pista);
|
||||
Object.entries(extra || {}).forEach(([k, v]) => v && u.set(k, v));
|
||||
const q = u.toString();
|
||||
return (opts.enlaceMas || '/tableros/pendientes/') + (q ? '?' + q : '');
|
||||
}
|
||||
|
||||
// ---------- barra de filtros ----------
|
||||
const barra = el('div', { class: 'kb-barra' });
|
||||
@ -151,16 +189,29 @@
|
||||
const selVer = el('select', { title: 'Versión de tarjeta', 'aria-label': 'Versión de tarjeta' }); selVer.append(el('option', { value: '', text: 'Tarjeta: todas' }));
|
||||
VERSIONES.filter(v => v !== '—').forEach(v => selVer.append(el('option', { value: v, text: v })));
|
||||
const selHechas = el('select', { title: 'Hechos', 'aria-label': 'Hechos' });
|
||||
[['recientes', 'Hechos: últimos 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
|
||||
[['recientes', 'Hechos: 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
|
||||
const buscar = el('input', { type: 'search', placeholder: 'Buscar en pendientes', 'aria-label': 'Buscar en pendientes' });
|
||||
const btnMios = el('button', { type: 'button', class: 'kb-solo', 'aria-pressed': String(filtro.mios), html: icono('persona') + 'Solo los míos' });
|
||||
const btnNuevo = el('button', { type: 'button', class: 'kb-nuevo', html: icono('mas') + 'Nuevo pendiente' });
|
||||
if (opts.filtros !== false) {
|
||||
const selVista = el('div', { class: 'kb-vistas', role: 'group', 'aria-label': 'Vista' });
|
||||
[['tablero', 'Tablero', 'columnas'], ['persona', 'Por persona', 'equipo']].forEach(([k, n, ic]) => {
|
||||
const b = el('button', { type: 'button', 'data-vista': k, 'aria-pressed': String(vista === k), html: icono(ic) + n });
|
||||
b.addEventListener('click', () => { if (vista === k) return; vista = k; ponURL('vista', k === 'persona' ? 'persona' : null); pintar(); });
|
||||
selVista.append(b);
|
||||
});
|
||||
const modo = opts.filtros === undefined ? true : opts.filtros;
|
||||
if (modo) {
|
||||
if (vistasOn) barra.append(selVista);
|
||||
barra.append(btnMios);
|
||||
if (!opts.pista) barra.append(selPista);
|
||||
barra.append(selResp, selVer, selHechas, buscar);
|
||||
if (modo !== 'min') { if (!opts.pista) barra.append(selPista); barra.append(selResp, selVer, selHechas, buscar); }
|
||||
if (opts.botonNuevoEn) opts.botonNuevoEn.append(btnNuevo); else barra.append(btnNuevo);
|
||||
root.append(barra);
|
||||
if (opts.barraEn) {
|
||||
// la página ya puso ahí sus propios elementos (p. ej. una liga): las vistas y «Solo los míos»
|
||||
// van antes, «Nuevo pendiente» al final
|
||||
const nodos = [...barra.childNodes];
|
||||
nodos.filter(n => n !== btnNuevo).reverse().forEach(n => opts.barraEn.prepend(n));
|
||||
if (nodos.includes(btnNuevo)) opts.barraEn.append(btnNuevo);
|
||||
} else root.append(barra);
|
||||
}
|
||||
const aviso = el('div', { class: 'msg', role: 'status' }); root.append(aviso);
|
||||
function mensaje(txt, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = txt; clearTimeout(aviso._t); aviso._t = setTimeout(() => { aviso.className = 'msg'; }, bien ? 3500 : 8000); }
|
||||
@ -172,7 +223,7 @@
|
||||
ESTADOS.forEach(([k, nombre]) => {
|
||||
const c = el('div', { class: 'col e-' + k, 'data-estado': k });
|
||||
c.append(el('h3', { html: ESTADO_SVG[k] + '<span>' + nombre + '</span><span class="n">0</span>' }));
|
||||
if (k !== 'hecho') {
|
||||
if (k !== 'hecho' && opts.nuevoPorColumna !== false) {
|
||||
const mas = el('button', { type: 'button', class: 'kb-mas', html: icono('mas') + 'Nuevo' });
|
||||
mas.addEventListener('click', () => abrir(null, k));
|
||||
c.append(mas);
|
||||
@ -191,6 +242,8 @@
|
||||
cols[k] = c; kb.append(c);
|
||||
});
|
||||
root.append(kb);
|
||||
const per = el('div', { class: 'kb-personas', hidden: '' });
|
||||
root.append(per);
|
||||
|
||||
// ---------- filtrado y pintado ----------
|
||||
function visibles() {
|
||||
@ -235,13 +288,13 @@
|
||||
const bl = bloqueosAbiertos(t);
|
||||
if (bl.length) c.append(el('div', { class: 'nota', html: icono('aviso') + '<span>Espera: ' + esc(bl.map(b => corto(b.titulo.split(':')[0], 42)).join(' · ')) + '</span>' }));
|
||||
// ligas
|
||||
if ((t.enlaces || []).length) {
|
||||
if (!opts.compacto && (t.enlaces || []).length) {
|
||||
const e = el('div', { class: 'enl' });
|
||||
t.enlaces.slice(0, 2).forEach(x => { const a = el('a', { href: x.url, html: icono('liga') + '<span>' + esc(x.titulo || x.url) + '</span>' }); a.addEventListener('click', ev => ev.stopPropagation()); e.append(a); });
|
||||
if (t.enlaces.length > 2) e.append(el('span', { class: 'vacio', text: '+' + (t.enlaces.length - 2) + ' más' }));
|
||||
c.append(e);
|
||||
}
|
||||
if ((t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); }
|
||||
if (!opts.compacto && (t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); }
|
||||
// responsables y fecha
|
||||
const pie = el('div', { class: 'pie' });
|
||||
pie.append(t.responsable ? avatares(t) : el('span', { class: 'vacio', text: 'Sin responsable' }));
|
||||
@ -261,12 +314,21 @@
|
||||
function pintar() {
|
||||
porId = {}; tareas.forEach(t => porId[t.id] = t);
|
||||
const vis = visibles();
|
||||
ESTADOS.forEach(([k]) => {
|
||||
const c = cols[k]; c.querySelectorAll('.card').forEach(x => x.remove());
|
||||
selVista.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vista === vista)));
|
||||
kb.hidden = vista === 'persona';
|
||||
per.hidden = vista !== 'persona';
|
||||
if (vista === 'persona') pintarPersonas(vis);
|
||||
else ESTADOS.forEach(([k]) => {
|
||||
const c = cols[k]; c.querySelectorAll('.card, .kb-mas-n').forEach(x => x.remove());
|
||||
const lst = vis.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
|
||||
// con tope (página de inicio), en Hecho van primero los más recientes
|
||||
if (opts.limite && k === 'hecho') lst.sort((a, b) => String(b.modificado_en || '').localeCompare(String(a.modificado_en || '')));
|
||||
c.querySelector('.n').textContent = lst.length;
|
||||
const mas = c.querySelector('.kb-mas');
|
||||
lst.forEach(t => mas ? c.insertBefore(tarjeta(t), mas) : c.append(tarjeta(t)));
|
||||
const pon = x => mas ? c.insertBefore(x, mas) : c.append(x);
|
||||
const tope = !opts.limite ? Infinity : (typeof opts.limite === 'object' ? (opts.limite[k] || opts.limite.otros || 4) : opts.limite);
|
||||
lst.slice(0, tope).forEach(t => pon(tarjeta(t)));
|
||||
if (lst.length > tope) pon(el('a', { class: 'kb-mas-n', href: enlaceCompleto(), text: (lst.length - tope) + ' más' }));
|
||||
});
|
||||
// responsables presentes en los datos (más los conocidos)
|
||||
selResp.querySelectorAll('option:not(:first-child)').forEach(o => o.remove());
|
||||
@ -278,6 +340,86 @@
|
||||
btnMios.setAttribute('aria-pressed', String(filtro.mios));
|
||||
btnMios.disabled = !user;
|
||||
btnMios.title = user ? 'Solo los pendientes de ' + ((EQUIPO[norm(user)] || {}).nombre || user) : 'No se sabe quién eres (falta X-Remote-User)';
|
||||
if (opts.onPintado) opts.onPintado(vis, { vista, filtro, user, admin });
|
||||
}
|
||||
|
||||
// ---------- vista por persona: una fila por responsable, una columna por estado ----------
|
||||
function filtrosActivos() { return !!((filtro.pista && !opts.pista) || filtro.responsable || filtro.version || filtro.buscar.trim() || filtro.mios); }
|
||||
function itemPersona(t) {
|
||||
const b = el('button', { type: 'button', class: 'kb-p-item' + (t.vencida ? ' vencida' : '') + (t.estado === 'hecho' ? ' e-hecho' : ''), title: t.titulo });
|
||||
b.append(el('span', { class: 'kb-p-tit', text: t.titulo }));
|
||||
const f = fechaDe(t);
|
||||
if (f) b.append(el('span', { class: 'fecha ' + f.cls, text: f.texto }));
|
||||
b.addEventListener('click', () => abrir(t));
|
||||
return b;
|
||||
}
|
||||
function pintarPersonas(vis) {
|
||||
per.innerHTML = '';
|
||||
const grupos = new Map();
|
||||
const agrega = (clave, t, libre) => { if (!grupos.has(clave)) grupos.set(clave, { clave, libre, tareas: [] }); grupos.get(clave).tareas.push(t); };
|
||||
vis.forEach(t => {
|
||||
const ids = idsResp(t), libres = libresResp(t);
|
||||
ids.forEach(id => agrega(id, t));
|
||||
libres.forEach(x => agrega('libre:' + norm(x), t, x));
|
||||
if (!ids.length && !libres.length) agrega('_nadie', t, 'Sin responsable');
|
||||
});
|
||||
const abiertos = g => g.tareas.filter(t => t.estado !== 'hecho').length;
|
||||
const nombre = g => g.libre || (EQUIPO[g.clave] || {}).nombre || g.clave;
|
||||
const filas = [...grupos.values()].sort((a, b) => (abiertos(b) - abiertos(a)) || nombre(a).localeCompare(nombre(b)));
|
||||
|
||||
const cab = el('div', { class: 'kb-p-fila kb-p-cab', 'aria-hidden': 'true' });
|
||||
cab.append(el('span', { text: 'Persona' }));
|
||||
ESTADOS.forEach(([k, n]) => cab.append(el('span', { html: ESTADO_SVG[k] + '<span>' + (k === 'hecho' && filtro.hechas === 'recientes' ? 'Hecho, 14 días' : n) + '</span>' })));
|
||||
per.append(cab);
|
||||
if (!filas.length) per.append(el('p', { class: 'vacio', text: 'Ningún pendiente con estos filtros.' }));
|
||||
|
||||
const N = opts.limitePersona || 3;
|
||||
filas.forEach(g => {
|
||||
const fila = el('div', { class: 'kb-p-fila' });
|
||||
const quien = el('div', { class: 'kb-p-quien' });
|
||||
quien.append(g.libre ? avatar(null, g.libre) : avatar(g.clave));
|
||||
const m = EQUIPO[g.clave] || {};
|
||||
const ab = abiertos(g), bl = g.tareas.filter(t => t.estado === 'bloqueado').length, ve = g.tareas.filter(t => t.vencida).length;
|
||||
const carga = ab ? ab + (ab === 1 ? ' abierto' : ' abiertos') + (bl ? ', ' + bl + (bl === 1 ? ' bloqueado' : ' bloqueados') : '') + (ve ? ', ' + ve + (ve === 1 ? ' vencido' : ' vencidos') : '') : 'Nada abierto';
|
||||
const txt = el('span', { class: 'kb-p-nom' });
|
||||
txt.append(el('b', { text: nombre(g) }));
|
||||
if (m.rol) txt.append(el('span', { class: 'kb-p-rol', text: m.rol }));
|
||||
txt.append(el('span', { class: 'kb-p-carga' + (ve ? ' mal' : (bl ? ' aviso' : '')), text: carga }));
|
||||
quien.append(txt);
|
||||
fila.append(quien);
|
||||
ESTADOS.forEach(([k, n]) => {
|
||||
const celda = el('div', { class: 'kb-p-celda e-' + k, 'data-col': n });
|
||||
let lst = g.tareas.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
|
||||
if (k === 'hecho') lst = lst.sort((a, b) => String(b.modificado_en || '').localeCompare(String(a.modificado_en || '')));
|
||||
const clave = g.clave + '|' + k;
|
||||
const todo = desplegadas.has(clave);
|
||||
(todo ? lst : lst.slice(0, N)).forEach(t => celda.append(itemPersona(t)));
|
||||
if (!todo && lst.length > N) {
|
||||
if (opts.enlaceMas) celda.append(el('a', { class: 'kb-mas-n', href: enlaceCompleto(g.libre ? {} : { responsable: g.clave }), text: (lst.length - N) + ' más' }));
|
||||
else {
|
||||
const b = el('button', { type: 'button', class: 'kb-mas-n', text: (lst.length - N) + ' más' });
|
||||
b.addEventListener('click', () => { desplegadas.add(clave); pintar(); });
|
||||
celda.append(b);
|
||||
}
|
||||
}
|
||||
if (!lst.length) celda.classList.add('vacia');
|
||||
fila.append(celda);
|
||||
});
|
||||
per.append(fila);
|
||||
});
|
||||
|
||||
// quienes no tienen nada abierto (solo sin filtros: con filtros sería engañoso)
|
||||
if (!filtrosActivos()) {
|
||||
const con = new Set(filas.filter(g => abiertos(g)).map(g => g.clave));
|
||||
const libres = Object.entries(EQUIPO).filter(([id, m]) => m.activo !== false && !con.has(id) && m.nombre)
|
||||
.sort((a, b) => a[1].nombre.localeCompare(b[1].nombre));
|
||||
if (libres.length) {
|
||||
const f = el('div', { class: 'kb-p-libres' });
|
||||
f.append(el('span', { class: 'kb-p-libres-t', text: 'Sin pendientes abiertos' }));
|
||||
libres.forEach(([id]) => f.append(el('span', { class: 'persona' }, avatar(id), document.createTextNode(nombreCorto(id)))));
|
||||
per.append(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function recargar() {
|
||||
@ -416,19 +558,16 @@
|
||||
selVer.addEventListener('change', () => { filtro.version = selVer.value; pintar(); });
|
||||
selHechas.addEventListener('change', () => { filtro.hechas = selHechas.value; pintar(); });
|
||||
buscar.addEventListener('input', () => { filtro.buscar = buscar.value; pintar(); });
|
||||
btnMios.addEventListener('click', () => {
|
||||
filtro.mios = !filtro.mios; pintar();
|
||||
const u = new URL(location.href); if (filtro.mios) u.searchParams.set('mios', '1'); else u.searchParams.delete('mios');
|
||||
history.replaceState(null, '', u.pathname + u.search + u.hash);
|
||||
});
|
||||
btnMios.addEventListener('click', () => { filtro.mios = !filtro.mios; ponURL('mios', filtro.mios ? '1' : null); pintar(); });
|
||||
btnNuevo.addEventListener('click', () => abrir(null));
|
||||
if (q0.get('pista') && !opts.pista) { selPista.value = q0.get('pista'); filtro.pista = q0.get('pista'); }
|
||||
if (q0.get('responsable')) { filtro.responsable = q0.get('responsable').toLowerCase(); }
|
||||
if (q0.get('id')) setTimeout(() => { const t = porId[q0.get('id')]; if (t) abrir(t); }, 600);
|
||||
|
||||
if (vistasOn) cargarEquipo().then(() => { if (vista === 'persona' && tareas.length) pintar(); });
|
||||
recargar();
|
||||
return { recargar, abrir, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; } };
|
||||
return { recargar, abrir, pintar, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; }, get vista() { return vista; } };
|
||||
}
|
||||
|
||||
window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, avatar, avatares, icono, fechaDe, get equipo() { return EQUIPO; } };
|
||||
window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, nombreCorto, avatar, avatares, icono, fechaDe, fechaCorta, get equipo() { return EQUIPO; } };
|
||||
})();
|
||||
|
||||
@ -22,6 +22,7 @@ hide:
|
||||
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.</li>
|
||||
<li>Al terminar, muévelo a <b>Hecho</b>. No lo borres: solo el administrador borra, y el historial sirve de bitácora.</li>
|
||||
<li>Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.</li>
|
||||
<li><b>Por persona</b> muestra una fila por responsable con sus pendientes en cada estado: sirve para ver quién está cargado y quién está libre. La dirección (<code>?vista=persona</code>, <code>?mios=1</code>) se puede compartir tal cual.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
@ -32,6 +33,7 @@ hide:
|
||||
(function () {
|
||||
Kanban.montar(document.getElementById('kb'), {
|
||||
filtros: true,
|
||||
vistas: true,
|
||||
botonNuevoEn: document.getElementById('acciones-kb'),
|
||||
onCargado: (tareas, meta) => {
|
||||
const abiertas = tareas.filter(t => t.estado !== 'hecho').length;
|
||||
|
||||
96
docs/tableros/ruta.js
Normal file
@ -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 };
|
||||
})();
|
||||
@ -245,7 +245,7 @@
|
||||
/* ---- Tablero de pendientes ---------------------------------------------- */
|
||||
.tb .kb-barra { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 14px; }
|
||||
.tb .kb-barra select, .tb .kb-barra input { padding: 7px 10px; font-size: 13.5px; }
|
||||
.tb .kb-barra input[type="search"] { flex: 0 1 210px; min-width: 140px; }
|
||||
.tb .kb-barra input[type="search"] { flex: 1 1 150px; min-width: 130px; max-width: 280px; }
|
||||
.tb .kb-barra .cnt { color: var(--hg-texto-3); font-size: 13px; }
|
||||
.tb .kb-barra .kb-nuevo { margin-left: auto; }
|
||||
.tb .kb-solo {
|
||||
@ -338,6 +338,7 @@
|
||||
.tb .card .mover { display: flex; gap: 6px; align-items: center; }
|
||||
.tb .card .mover select { padding: 5px 8px; font-size: 13px; flex: 1; }
|
||||
@media (min-width: 981px) { .tb .card .mover { display: none; } .tb .kb.movil .card .mover { display: flex; } }
|
||||
.tb .kb.compacto .card .mover { display: none; } /* el extracto del inicio: se mueve desde el detalle o el tablero completo */
|
||||
|
||||
/* ---- Diálogo (detalle y alta) -------------------------------------------- */
|
||||
.tb dialog {
|
||||
@ -524,6 +525,147 @@
|
||||
.tb .visita .cl { font-weight: 700; }
|
||||
.tb .marca-unidad { border-radius: 10px; padding: 10px 14px; margin: 0 0 14px; font-size: 14.5px; }
|
||||
|
||||
/* ---- Vistas del tablero y «N más» (21-oct) -------------------------------- */
|
||||
.tb .kb-vistas { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--hg-linea-2); border-radius: 10px; background: var(--hg-plano-2); }
|
||||
.tb .kb-vistas button { background: transparent; border: 0; color: var(--hg-texto-2); font-size: 13.5px; font-weight: 500; padding: 5px 11px; border-radius: 7px; }
|
||||
.tb .kb-vistas button[aria-pressed="true"] { background: var(--hg-elevado); color: var(--hg-texto-fuerte); font-weight: 600; }
|
||||
.tb .kb-vistas button:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
|
||||
.tb .kb-vistas .i { width: 15px; height: 15px; }
|
||||
.tb .kb-mas-n { align-self: flex-start; padding: 4px; font-size: 13px; color: var(--hg-texto-2); text-decoration: none; }
|
||||
.tb a.kb-mas-n:hover { color: var(--hg-liga-hover); text-decoration: underline; }
|
||||
.tb button.kb-mas-n { background: none; border: 0; font-weight: 500; }
|
||||
.tb button.kb-mas-n:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
|
||||
.tb [hidden] { display: none !important; }
|
||||
|
||||
/* ---- Vista por persona: una fila por responsable --------------------------- */
|
||||
.tb .kb-personas { border: 1px solid var(--hg-linea); border-radius: 12px; background: var(--hg-plano-2); }
|
||||
.tb .kb-p-fila { display: grid; grid-template-columns: minmax(200px, 1.15fr) repeat(4, minmax(0, 1fr)); gap: 0 14px; align-items: start; padding: 12px 16px; border-top: 1px solid var(--hg-linea); }
|
||||
.tb .kb-p-fila:first-child { border-top: 0; }
|
||||
.tb .kb-p-cab { padding-top: 11px; padding-bottom: 11px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); }
|
||||
.tb .kb-p-cab > span { display: inline-flex; align-items: center; gap: 7px; }
|
||||
.tb .kb-p-cab svg { width: 15px; height: 15px; flex: none; }
|
||||
.tb .kb-p-quien { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
|
||||
.tb .kb-p-quien .av { width: 30px; height: 30px; font-size: 11px; }
|
||||
.tb .kb-p-nom { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
|
||||
.tb .kb-p-nom b { font-weight: 600; color: var(--hg-texto-fuerte); }
|
||||
.tb .kb-p-rol { font-size: 12.5px; color: var(--hg-texto-3); }
|
||||
.tb .kb-p-carga { margin-top: 2px; font-size: 12.5px; color: var(--hg-texto-2); }
|
||||
.tb .kb-p-carga.mal { color: var(--hg-bloqueado); }
|
||||
.tb .kb-p-carga.aviso { color: var(--hg-aviso); }
|
||||
.tb .kb-p-celda { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
||||
.tb button.kb-p-item {
|
||||
display: flex; flex-direction: column; align-items: stretch; gap: 3px;
|
||||
width: 100%; padding: 8px 10px; border-radius: 8px;
|
||||
border: 1px solid var(--hg-linea); background: var(--hg-plano); color: var(--hg-texto);
|
||||
font-size: 13.5px; font-weight: 500; line-height: 1.35; text-align: left; white-space: normal;
|
||||
}
|
||||
.tb button.kb-p-item:hover:not(:disabled) { filter: none; border-color: var(--hg-linea-2); color: var(--hg-texto-fuerte); }
|
||||
.tb button.kb-p-item.vencida { border-color: color-mix(in srgb, var(--hg-bloqueado) 45%, var(--hg-linea)); }
|
||||
.tb button.kb-p-item.e-hecho { color: var(--hg-texto-2); }
|
||||
.tb .kb-p-tit { overflow-wrap: anywhere; }
|
||||
.tb .kb-p-item .fecha { font-size: 12px; font-weight: 400; color: var(--hg-texto-3); }
|
||||
.tb .kb-p-item .fecha.hoy { color: var(--hg-acento); font-weight: 600; }
|
||||
.tb .kb-p-item .fecha.mal { color: var(--hg-bloqueado); font-weight: 600; }
|
||||
.tb .kb-p-libres { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 12px 16px; border-top: 1px solid var(--hg-linea); font-size: 13px; color: var(--hg-texto-2); }
|
||||
.tb .kb-p-libres-t { color: var(--hg-texto-3); margin-right: 2px; }
|
||||
@media (max-width: 900px) {
|
||||
.tb .kb-p-cab { display: none; }
|
||||
.tb .kb-p-cab + .kb-p-fila { border-top: 0; }
|
||||
.tb .kb-p-fila { grid-template-columns: 1fr 1fr; gap: 10px 12px; }
|
||||
.tb .kb-p-quien { grid-column: 1 / -1; }
|
||||
.tb .kb-p-celda::before { content: attr(data-col); font-size: 12px; font-weight: 600; color: var(--hg-texto-3); }
|
||||
.tb .kb-p-celda.vacia { display: none; }
|
||||
}
|
||||
@media (max-width: 520px) { .tb .kb-p-fila { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ---- Ruta a producción (inicio e Ingeniería) -------------------------------
|
||||
Línea continua = hecho (blanco en oscuro, tinta en claro); punteada = lo que falta;
|
||||
anillo cian = el paso en curso; coral = detenido. */
|
||||
.tb .ruta { border: 1px solid var(--hg-linea); border-radius: 14px; background: var(--hg-plano-2); padding: 2px 18px; }
|
||||
.tb .ruta-fila { display: grid; grid-template-columns: minmax(230px, 1.4fr) minmax(0, 5fr); gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--hg-linea); }
|
||||
.tb .ruta-fila:first-child { border-top: 0; }
|
||||
.tb .ruta-cab { padding: 12px 0 8px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); align-items: end; }
|
||||
.tb .ruta-cab-t { color: var(--hg-texto-3); font-weight: 500; }
|
||||
/* (Material pone display: flow-root a .md-typeset ol:not([hidden]); por eso el selector largo) */
|
||||
.tb .ruta-pasos-cab,
|
||||
.md-typeset .tb ol.ruta-pasos:not([hidden]) { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(var(--ruta-pasos, 7), minmax(0, 1fr)); }
|
||||
.tb .ruta-pasos-cab span { text-align: center; padding: 0 4px; }
|
||||
.md-typeset .tb ol.ruta-pasos { list-style: none; margin: 0; padding: 0; }
|
||||
.md-typeset .tb .ruta-pasos li { margin: 0; }
|
||||
.tb .ruta-paso { position: relative; display: flex; flex-direction: column; align-items: center; min-width: 0; padding: 0 4px; text-align: center; }
|
||||
.tb .ruta-paso::before,
|
||||
.tb .ruta-paso::after { content: ""; position: absolute; top: 11px; width: 50%; border-top: 1px dashed var(--hg-linea-2); }
|
||||
.tb .ruta-paso::before { left: 0; }
|
||||
.tb .ruta-paso::after { right: 0; }
|
||||
.tb .ruta-paso.entra::before,
|
||||
.tb .ruta-paso.sale::after { top: 10px; border-top: 3px solid var(--hg-ruta-linea); }
|
||||
.tb .ruta-paso.primero::before,
|
||||
.tb .ruta-paso.ultimo::after { display: none; }
|
||||
.tb .ruta-punto { position: relative; z-index: 1; box-sizing: border-box; width: 16px; height: 16px; margin: 3px 0 8px; border-radius: 50%; border: 2px solid var(--hg-linea-2); background: var(--hg-plano-2); }
|
||||
.tb .p-hecho .ruta-punto { border: 3.5px solid var(--hg-ruta); }
|
||||
.tb .p-ahora .ruta-punto,
|
||||
.tb .p-bloqueado .ruta-punto { width: 22px; height: 22px; margin: 0 0 5px; border: 4px solid var(--hg-acento); box-shadow: 0 0 0 5px var(--hg-halo-curso); }
|
||||
.tb .p-bloqueado .ruta-punto { border-color: var(--hg-bloqueado); box-shadow: 0 0 0 5px var(--hg-halo-bloqueado); }
|
||||
.tb .ruta-txt { font-size: 13px; line-height: 1.3; color: var(--hg-texto-2); overflow-wrap: anywhere; }
|
||||
.tb .p-hecho .ruta-txt { color: var(--hg-texto-suave); }
|
||||
.tb .p-ahora .ruta-txt { color: var(--hg-acento); font-weight: 700; }
|
||||
.tb .p-bloqueado .ruta-txt { color: var(--hg-bloqueado); font-weight: 700; }
|
||||
.tb .ruta-sub { margin-top: 2px; font-size: 12px; line-height: 1.3; color: var(--hg-texto-3); overflow-wrap: anywhere; }
|
||||
.tb a.ruta-tarjeta { display: flex; gap: 12px; align-items: center; min-width: 0; color: inherit; text-decoration: none; }
|
||||
.tb a.ruta-tarjeta img { flex: none; width: 100px; height: auto; aspect-ratio: 7 / 5; object-fit: cover; border-radius: 7px; border: 1px solid var(--hg-linea); background: #0E1524; filter: brightness(1.2) contrast(1.05); }
|
||||
.tb a.ruta-tarjeta:hover img { border-color: var(--hg-linea-2); }
|
||||
.tb a.ruta-tarjeta:hover .ruta-nuevo { color: var(--hg-texto-suave); }
|
||||
.tb .ruta-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.tb .ruta-ver { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.tb .ruta-estado { font-size: 14px; font-weight: 700; color: var(--hg-texto); }
|
||||
.tb .tono-hecho .ruta-estado { color: var(--hg-texto-fuerte); }
|
||||
.tb .tono-ahora .ruta-estado { color: var(--hg-acento); }
|
||||
.tb .tono-aviso .ruta-estado { color: var(--hg-aviso); }
|
||||
.tb .tono-bloqueado .ruta-estado { color: var(--hg-bloqueado); }
|
||||
.tb .ruta-nuevo { font-size: 13px; line-height: 1.35; color: var(--hg-texto-2); }
|
||||
.md-typeset .tb p.ruta-resumen { display: none; }
|
||||
@media (max-width: 1000px) {
|
||||
.tb .ruta-fila { grid-template-columns: 1fr; gap: 12px; }
|
||||
.tb .ruta-cab { display: none; }
|
||||
.tb .ruta-cab + .ruta-fila { border-top: 0; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.tb .ruta { padding: 2px 14px; }
|
||||
.tb .ruta-txt, .tb .ruta-sub { display: none; }
|
||||
.tb .ruta-punto { margin-bottom: 0; }
|
||||
.md-typeset .tb p.ruta-resumen { display: block; margin: 0; font-size: 13.5px; line-height: 1.4; color: var(--hg-texto-suave); }
|
||||
}
|
||||
|
||||
/* ---- Actividad (inicio y Bitácora) ------------------------------------------ */
|
||||
.md-typeset .tb ol.act-lista { list-style: none; margin: 0; padding: 0; }
|
||||
.md-typeset .tb .act-lista li { margin: 0; }
|
||||
.tb .act { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 1px 10px; padding: 9px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 14px; line-height: 1.4; }
|
||||
.tb .act:first-child { border-top: 0; }
|
||||
.tb .act .av { grid-row: span 2; width: 26px; height: 26px; margin-top: 1px; font-size: 10px; }
|
||||
.tb .act-t { color: var(--hg-texto-suave); overflow-wrap: anywhere; }
|
||||
.tb .act-t b { font-weight: 600; color: var(--hg-texto-fuerte); }
|
||||
.tb .act-t a { color: var(--hg-texto); text-decoration: none; }
|
||||
.tb .act-t a:hover { color: var(--hg-liga-hover); text-decoration: underline; }
|
||||
.tb .act-nota { display: block; margin-top: 4px; padding: 6px 9px; border-radius: 8px; background: var(--hg-plano); font-size: 13px; color: var(--hg-texto-2); }
|
||||
.tb .act-res { padding: 1px 7px; border-radius: 5px; font-size: 12px; font-weight: 600; background: var(--hg-hecho-bg); color: var(--hg-hecho); white-space: nowrap; }
|
||||
.tb .act-res.mal { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); }
|
||||
.tb .act-f { grid-column: 2; font-size: 12.5px; color: var(--hg-texto-3); }
|
||||
.md-typeset .tb h3.act-dia { margin: 18px 0 2px; font-family: var(--hg-fuente); font-size: 13px; font-weight: 600; color: var(--hg-texto-3); }
|
||||
.md-typeset .tb h3.act-dia:first-child { margin-top: 0; }
|
||||
.tb .act-mas { display: inline-block; margin-top: 8px; font-size: 13px; }
|
||||
|
||||
/* ---- Ingeniería: la ruta arriba; Bitácora: página de lectura ------------------- */
|
||||
.md-typeset .tb h2.ing-h { margin: 4px 0 4px; }
|
||||
.tb .ing-ruta { margin-bottom: 16px; }
|
||||
.tb .versiones .ver { scroll-margin-top: 4.5rem; }
|
||||
.md-typeset .tb-bitacora h2 { margin: 30px 0 10px; }
|
||||
.md-typeset .tb-bitacora h3 { margin: 24px 0 8px; font-family: var(--hg-fuente-titulo); font-size: 18px; font-weight: 600; }
|
||||
.tb-bitacora #bit-actividad { padding: 8px 16px 12px; border: 1px solid var(--hg-linea); border-radius: 12px; background: var(--hg-plano-2); }
|
||||
.md-typeset .tb-bitacora dd { margin-bottom: 6px; }
|
||||
|
||||
/* texto solo para lectores de pantalla */
|
||||
.tb .vh { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
|
||||
@media print {
|
||||
.tb .tb-cab__acc, .tb .buscar, .tb button, .tb .kb-barra, .tb details.tb-como { display: none !important; }
|
||||
.tb section.bloque, .tb .kpi, .tb .card, .tb .cel, .tb .cadena .paso { background: #fff !important; border-color: #999 !important; color: #000 !important; }
|
||||
|
||||
52
hooks/documentos.py
Normal file
@ -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))
|
||||
@ -56,6 +56,7 @@ extra_javascript:
|
||||
hooks:
|
||||
- hooks/iconos.py # íconos del menú lateral (también para páginas que llegan por sync-docs.sh)
|
||||
- hooks/frontmatter.py # encabezados YAML con «: » sin comillas: se leen igual (no toca los archivos)
|
||||
- hooks/documentos.py # «Documentos recientes» del inicio: site/datos/documentos.json (páginas con date:)
|
||||
|
||||
plugins:
|
||||
- search:
|
||||
@ -150,6 +151,7 @@ nav:
|
||||
- hardware/index.md
|
||||
- Manuales por tarjeta: manuales/index.md
|
||||
- Auditorías y pasadas:
|
||||
- 21 oct · Estado v6 (T25 + T28): hardware/v6-estado-21oct.md
|
||||
- 21 oct · Auditoría pasada 4: hardware/v56-v57-auditoria-21oct.md
|
||||
- 19 oct · Críticos V5.6: hardware/v56-clasificacion-19oct.md
|
||||
- 19 oct · Re-auditoría: hardware/re-auditoria-19oct.md
|
||||
|
||||
17
overrides/inicio.html
Normal file
@ -0,0 +1,17 @@
|
||||
{#-
|
||||
Página de inicio (docs/index.md con `template: inicio.html`, 21-oct-2026).
|
||||
Extiende la plantilla de los tableros (tablero.css y kanban.js, porque el inicio lleva el
|
||||
tablero de pendientes) y agrega lo suyo: inicio/inicio.css, la ruta a producción
|
||||
(tableros/ruta.js), la actividad (tableros/actividad.js) e inicio/inicio.js, que llena la página
|
||||
con docs/datos/proyecto.json y con el servidor (/api/...).
|
||||
-#}
|
||||
{% extends "tablero.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{ 'inicio/inicio.css' | url }}">
|
||||
<link rel="preload" href="{{ 'img/tarjetas/portada-v56.webp' | url }}" as="image" type="image/webp">
|
||||
<script src="{{ 'tableros/ruta.js' | url }}"></script>
|
||||
<script src="{{ 'tableros/actividad.js' | url }}"></script>
|
||||
<script src="{{ 'inicio/inicio.js' | url }}" defer></script>
|
||||
{% endblock %}
|
||||
@ -2,7 +2,8 @@
|
||||
Plantilla de los tableros (Pendientes, Ingeniería, Producción, Actas, Inventario,
|
||||
Sitios, Equipo, Expediente y Visitas de campo). Se elige con `template: tablero.html`
|
||||
en el front matter de la página.
|
||||
- Usa todo el ancho (sin la columna de texto de los documentos).
|
||||
- Usa todo el ancho (sin la columna de texto de los documentos). Una página puede pedir
|
||||
otro ancho en su front matter, p. ej. `ancho: 52rem` (la Bitácora) o `ancho: 64rem` (inicio).
|
||||
- Carga tableros/tablero.css (estilos del tablero, sobre los tokens de helgen.css)
|
||||
y tableros/kanban.js (el tablero de pendientes) antes del contenido, para que
|
||||
el script de cada página pueda llamar a Kanban.montar(...).
|
||||
@ -12,6 +13,6 @@
|
||||
{% block extrahead %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{ 'tableros/tablero.css' | url }}">
|
||||
<style>:root{--hg-ancho-contenido:none}</style>
|
||||
<style>:root{--hg-ancho-contenido:{{ page.meta.ancho | default('none') }}}</style>
|
||||
<script src="{{ 'tableros/kanban.js' | url }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@ -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
|
||||
|
||||
22
scripts/tarjetas/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Imágenes de las tarjetas (portada y ruta a producción)
|
||||
|
||||
Las imágenes de `docs/img/tarjetas/` (la franja de la portada del inicio y una miniatura por versión) se
|
||||
dibujan con el cobre real de cada `.kicad_pcb`, en blanco sobre el fondo del sitio. Lo nuevo de cada
|
||||
revisión (las redes que no estaban en la versión anterior) va en blanco sólido; el resto, tenue.
|
||||
Decisión de Leo del 21-oct-2026: «la tarjeta en blanco, no en verde».
|
||||
|
||||
Las del 21-oct salieron del cobre del 20-oct. Para rehacerlas (en la fase 3 esto lo hará `sync-docs.sh`):
|
||||
|
||||
```bash
|
||||
cd hbox-docs/scripts/tarjetas
|
||||
K=../../../03-Architecture-Hardware/kicad
|
||||
python3 leer_pcb.py $K/hbox-watchdog-v5.kicad_pcb /tmp/tarjetas/pcb_v56.json
|
||||
python3 leer_pcb.py $K/hbox-watchdog-v57.kicad_pcb /tmp/tarjetas/pcb_v57.json
|
||||
python3 leer_pcb.py $K/hbox-watchdog-v6.kicad_pcb /tmp/tarjetas/pcb_v6.json
|
||||
# la V5.5 sale del commit con el que se pidió (22-ago): git show <commit>:hbox-watchdog-v5.kicad_pcb > v55.kicad_pcb
|
||||
python3 leer_pcb.py v55.kicad_pcb /tmp/tarjetas/pcb_v55.json
|
||||
python3 tarjetas_blanco.py /tmp/tarjetas /tmp/tarjetas/svg
|
||||
python3 rasterizar.py ../../docs/img/tarjetas /tmp/tarjetas/svg/*.svg # necesita Pillow y Playwright (Chromium)
|
||||
```
|
||||
|
||||
Solo con KiCad cerrado y sobre archivos ya subidos (no a media pasada de layout).
|
||||
63
scripts/tarjetas/leer_pcb.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""Lee un .kicad_pcb (KiCad 8/9/10) sin KiCad y guarda en JSON lo que hace falta para dibujarlo:
|
||||
pistas, arcos, vías, pads (con su red y su capa), contorno y zonas rellenadas.
|
||||
Uso: python3 leer_pcb.py tarjeta.kicad_pcb pcb_v56.json"""
|
||||
import re, json, math, sys
|
||||
src = open(sys.argv[1], encoding='utf-8').read()
|
||||
tok_re = re.compile(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()"]+')
|
||||
def parse(s):
|
||||
stack=[[]]
|
||||
for m in tok_re.finditer(s):
|
||||
t=m.group(0)
|
||||
if t=='(':
|
||||
stack.append([])
|
||||
elif t==')':
|
||||
x=stack.pop(); stack[-1].append(x)
|
||||
else:
|
||||
if t.startswith('"'): t=t[1:-1]
|
||||
stack[-1].append(t)
|
||||
return stack[0][0]
|
||||
root=parse(src)
|
||||
def find(node, key):
|
||||
return [c for c in node if isinstance(c,list) and c and c[0]==key]
|
||||
def one(node,key):
|
||||
r=find(node,key); return r[0] if r else None
|
||||
out={'segments':[], 'vias':[], 'pads':[], 'edges':[], 'zones':[], 'arcs':[]}
|
||||
for seg in find(root,'segment'):
|
||||
s=one(seg,'start'); e=one(seg,'end'); w=one(seg,'width'); l=one(seg,'layer'); n=one(seg,'net')
|
||||
out['segments'].append([float(s[1]),float(s[2]),float(e[1]),float(e[2]),float(w[1]),l[1],n[1] if n else ''])
|
||||
for a in find(root,'arc'):
|
||||
s=one(a,'start'); m=one(a,'mid'); e=one(a,'end'); w=one(a,'width'); l=one(a,'layer'); n=one(a,'net')
|
||||
out['arcs'].append([float(s[1]),float(s[2]),float(m[1]),float(m[2]),float(e[1]),float(e[2]),float(w[1]),l[1],n[1] if n else ''])
|
||||
for v in find(root,'via'):
|
||||
at=one(v,'at'); sz=one(v,'size'); n=one(v,'net')
|
||||
out['vias'].append([float(at[1]),float(at[2]),float(sz[1]),n[1] if n else ''])
|
||||
for fp in find(root,'footprint'):
|
||||
at=one(fp,'at'); fx,fy=float(at[1]),float(at[2]); frot=float(at[3]) if len(at)>3 else 0.0
|
||||
layer=one(fp,'layer')[1]
|
||||
ref=''
|
||||
for p in find(fp,'property'):
|
||||
if len(p)>2 and p[1]=='Reference': ref=p[2]
|
||||
for pad in find(fp,'pad'):
|
||||
pat=one(pad,'at'); px,py=float(pat[1]),float(pat[2]); prot=float(pat[3]) if len(pat)>3 else 0.0
|
||||
size=one(pad,'size'); w,h=float(size[1]),float(size[2])
|
||||
shape=pad[3] if len(pad)>3 else 'rect'
|
||||
typ=pad[2] if len(pad)>2 else 'smd'
|
||||
n=one(pad,'net'); net=n[1] if n and len(n)>1 else ''
|
||||
# KiCad: pad position rotated by footprint rotation (clockwise in screen coords since y down)
|
||||
r=math.radians(-frot)
|
||||
ax=fx+px*math.cos(r)-py*math.sin(r); ay=fy+px*math.sin(r)+py*math.cos(r)
|
||||
lay=one(pad,'layers'); lays=lay[1:] if lay else []
|
||||
out['pads'].append([round(ax,3),round(ay,3),w,h,prot,shape,typ,net,ref,layer,lays])
|
||||
for g in root:
|
||||
if isinstance(g,list) and g and g[0] in ('gr_line','gr_rect','gr_arc','gr_circle','gr_poly'):
|
||||
l=one(g,'layer')
|
||||
if l and l[1]=='Edge.Cuts':
|
||||
out['edges'].append(g)
|
||||
for z in find(root,'zone'):
|
||||
n=one(z,'net'); net=n[1] if n and len(n)>1 else ''
|
||||
for fpoly in find(z,'filled_polygon'):
|
||||
l=one(fpoly,'layer'); pts=one(fpoly,'pts')
|
||||
xy=[(float(p[1]),float(p[2])) for p in pts[1:] if isinstance(p,list) and p[0]=='xy']
|
||||
out['zones'].append([l[1] if l else '', net, xy])
|
||||
json.dump(out, open(sys.argv[2] if len(sys.argv) > 2 else 'pcb.json', 'w'))
|
||||
print({k: len(v) for k, v in out.items()})
|
||||
26
scripts/tarjetas/rasterizar.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""SVG → WebP con Chromium (Playwright) y Pillow, para que el texto y los trazos salgan iguales que en el navegador.
|
||||
Uso: python3 rasterizar.py SALIDA_DIR a.svg [b.svg …]"""
|
||||
import asyncio, io, os, re, sys
|
||||
from PIL import Image
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
OUT = sys.argv[1]
|
||||
|
||||
|
||||
async def main():
|
||||
async with async_playwright() as p:
|
||||
b = await p.chromium.launch()
|
||||
for f in sys.argv[2:]:
|
||||
txt = open(f).read()
|
||||
w, h = map(int, re.search(r'width="(\d+)" height="(\d+)"', txt).groups())
|
||||
pg = await b.new_page(viewport={'width': w, 'height': h})
|
||||
await pg.set_content('<html><body style="margin:0">' + txt + '</body></html>')
|
||||
png = await pg.screenshot(clip={'x': 0, 'y': 0, 'width': w, 'height': h})
|
||||
await pg.close()
|
||||
im = Image.open(io.BytesIO(png)).convert('RGB')
|
||||
dst = os.path.join(OUT, os.path.basename(f)[:-4] + '.webp')
|
||||
im.save(dst, 'WEBP', quality=84, method=6)
|
||||
print(dst, im.size, os.path.getsize(dst), 'bytes')
|
||||
await b.close()
|
||||
|
||||
asyncio.run(main())
|
||||
131
scripts/tarjetas/tarjetas_blanco.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""Cobre de las EinarBoard en blanco sobre noche (21-oct): portada (V5.6) y miniaturas por versión.
|
||||
Lo nuevo de cada revisión va en blanco sólido; el resto del cobre en blanco tenue.
|
||||
Uso: python3 tarjetas_blanco.py DIR_JSON DIR_SALIDA (pcb_v55/v56/v57/v6.json, de leer_pcb.py)
|
||||
Genera SVG; la conversión a WebP la hace rasterizar.py."""
|
||||
import json, os, re, sys
|
||||
|
||||
SRC, OUT = sys.argv[1], sys.argv[2]
|
||||
D = {v: json.load(open(os.path.join(SRC, f'pcb_{v}.json'))) for v in ('v55', 'v56', 'v57', 'v6')}
|
||||
|
||||
|
||||
def nets(d):
|
||||
return {p[7] for p in d['pads'] if p[7]} | {s[6] for s in d['segments'] if s[6]}
|
||||
|
||||
|
||||
def ok(n):
|
||||
return n and n != 'GND' and not n.startswith('unconnected')
|
||||
|
||||
|
||||
NUEVO = {'v55': set(),
|
||||
'v56': {n for n in nets(D['v56']) - nets(D['v55']) if ok(n)},
|
||||
'v57': {n for n in nets(D['v57']) - nets(D['v56']) if ok(n)},
|
||||
'v6': {n for n in nets(D['v6']) - nets(D['v57'])
|
||||
if ok(n) and re.search(r'GNSS|MODEM|4G|ANT|RF_|NETLIGHT|1V8|F9P|LTE', n)}}
|
||||
|
||||
|
||||
def contorno(d):
|
||||
xs, ys = [], []
|
||||
for e in d['edges']:
|
||||
for k in e[1:]:
|
||||
if isinstance(k, list) and k and k[0] in ('start', 'end'):
|
||||
xs.append(float(k[1])); ys.append(float(k[2]))
|
||||
return min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)
|
||||
|
||||
|
||||
# opacidades: (zonas, B.Cu, capas internas, F.Cu, pads, vías) y lo nuevo (F.Cu, internas, pads)
|
||||
TEMAS = {
|
||||
'mini': dict(fondo='#111A2C', z=0.035, b=0.12, i=0.08, f=0.28, pad=0.38, via=0.32, nf=1.0, ni=0.55, npad=0.95, min_w=0.55),
|
||||
'portada': dict(fondo='#0E1524', z=0.03, b=0.10, i=0.07, f=0.22, pad=0.30, via=0.26, nf=0.95, ni=0.5, npad=0.9, min_w=0.8),
|
||||
}
|
||||
|
||||
|
||||
def svg(d, hl, crop, size, tema):
|
||||
T = TEMAS[tema]
|
||||
x0, y0, W, H = crop
|
||||
OW, OH = size
|
||||
s = max(OW / W, OH / H) # llena el recuadro (la portada recorta arriba y abajo)
|
||||
ox, oy = (OW - W * s) / 2, (OH - H * s) / 2
|
||||
X = lambda x: ox + (x - x0) * s
|
||||
Y = lambda y: oy + (y - y0) * s
|
||||
o = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{OW}" height="{OH}" viewBox="0 0 {OW} {OH}">',
|
||||
f'<rect width="{OW}" height="{OH}" fill="{T["fondo"]}"/>']
|
||||
|
||||
def grupo(op, partes):
|
||||
if partes:
|
||||
o.append(f'<g opacity="{op}" fill="#fff" stroke="#fff">' + ''.join(partes) + '</g>')
|
||||
|
||||
def lineas(capas, solo=None, sin=None):
|
||||
r = []
|
||||
for x1, y1, x2, y2, w, l, n in d['segments']:
|
||||
if l not in capas or (solo is not None and n not in solo) or (sin is not None and n in sin):
|
||||
continue
|
||||
r.append(f'<line x1="{X(x1):.1f}" y1="{Y(y1):.1f}" x2="{X(x2):.1f}" y2="{Y(y2):.1f}" '
|
||||
f'stroke-width="{max(w * s, T["min_w"]):.2f}" stroke-linecap="round" fill="none"/>')
|
||||
return r
|
||||
|
||||
def pads(solo=None, sin=None):
|
||||
r = []
|
||||
for ax, ay, w, h, prot, shape, typ, n, ref, layer, lays in d['pads']:
|
||||
if not any(l in ('F.Cu', '*.Cu') for l in lays):
|
||||
continue
|
||||
if (solo is not None and n not in solo) or (sin is not None and n in sin):
|
||||
continue
|
||||
cx, cy, ww, hh = X(ax), Y(ay), w * s, h * s
|
||||
if shape == 'circle':
|
||||
r.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{ww / 2:.1f}" stroke="none"/>')
|
||||
else:
|
||||
rr = min(ww, hh) / 2 if shape == 'oval' else (min(ww, hh) * 0.18 if shape == 'roundrect' else 0)
|
||||
r.append(f'<rect x="{cx - ww / 2:.1f}" y="{cy - hh / 2:.1f}" width="{ww:.1f}" height="{hh:.1f}" rx="{rr:.1f}" '
|
||||
f'stroke="none" transform="rotate({-prot:.0f} {cx:.1f} {cy:.1f})"/>')
|
||||
return r
|
||||
|
||||
def vias(solo=None, sin=None):
|
||||
return [f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="{sz * s / 2:.1f}" stroke="none"/>'
|
||||
for x, y, sz, n in d['vias'] if not ((solo is not None and n not in solo) or (sin is not None and n in sin))]
|
||||
|
||||
zonas = []
|
||||
for lay, net, pts in d['zones']:
|
||||
if lay in ('F.Cu', 'B.Cu') and pts:
|
||||
zonas.append('<path d="M' + ' L'.join(f'{X(x):.1f},{Y(y):.1f}' for x, y in pts) + 'Z" '
|
||||
f'stroke-width="{0.12 * s:.2f}" fill-opacity="0.6"/>')
|
||||
grupo(T['z'], zonas)
|
||||
grupo(T['b'], lineas({'B.Cu'}, sin=hl))
|
||||
grupo(T['i'], lineas({'In1.Cu', 'In2.Cu'}, sin=hl))
|
||||
grupo(T['f'], lineas({'F.Cu'}, sin=hl))
|
||||
grupo(T['pad'], pads(sin=hl))
|
||||
grupo(T['via'], vias(sin=hl))
|
||||
if hl:
|
||||
grupo(T['ni'], lineas({'B.Cu', 'In1.Cu', 'In2.Cu'}, solo=hl))
|
||||
grupo(T['nf'], lineas({'F.Cu'}, solo=hl))
|
||||
grupo(T['npad'], pads(solo=hl) + vias(solo=hl))
|
||||
# barrenos: del color del fondo, encima de todo
|
||||
hoyos = [f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="{sz * s / 4.2:.1f}"/>' for x, y, sz, n in d['vias']]
|
||||
for ax, ay, w, h, prot, shape, typ, n, ref, layer, lays in d['pads']:
|
||||
if typ == 'thru_hole':
|
||||
hoyos.append(f'<circle cx="{X(ax):.1f}" cy="{Y(ay):.1f}" r="{min(w, h) * s * 0.28:.1f}"/>')
|
||||
o.append(f'<g fill="{T["fondo"]}">' + ''.join(hoyos) + '</g>')
|
||||
# contorno de la tarjeta (solo en miniaturas)
|
||||
if tema == 'mini':
|
||||
bx, by, bw, bh = contorno(d)
|
||||
o.append(f'<rect x="{X(bx):.1f}" y="{Y(by):.1f}" width="{bw * s:.1f}" height="{bh * s:.1f}" rx="{1.2 * s:.1f}" '
|
||||
f'fill="none" stroke="#fff" stroke-opacity="0.35" stroke-width="{max(0.35 * s, 1):.2f}"/>')
|
||||
o.append('</svg>')
|
||||
return '\n'.join(o)
|
||||
|
||||
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
for v, d in D.items():
|
||||
bx, by, bw, bh = contorno(d)
|
||||
m = 2.0
|
||||
open(os.path.join(OUT, f'{v}.svg'), 'w').write(svg(d, NUEVO[v], (bx - m, by - m, bw + 2 * m, bh + 2 * m), (420, 300), 'mini'))
|
||||
print(v, 'nuevo:', len(NUEVO[v]), 'redes; contorno', round(bw, 1), 'x', round(bh, 1), 'mm')
|
||||
|
||||
# portada: franja de la V5.6 donde más cobre nuevo hay (ventana de 20 mm de alto)
|
||||
d = D['v56']; hl = NUEVO['v56']
|
||||
bx, by, bw, bh = contorno(d)
|
||||
W = bw - 16; H = W * 400 / 2400
|
||||
mejor = max((sum(abs(s[2] - s[0]) + abs(s[3] - s[1]) for s in d['segments'] if s[6] in hl and y <= (s[1] + s[3]) / 2 <= y + H), y)
|
||||
for y in [by + 1 + k * 0.5 for k in range(int((bh - H - 2) / 0.5))])
|
||||
y0 = mejor[1]
|
||||
print('portada: y0 =', round(y0, 1), 'mm; mm de cobre nuevo en la franja:', round(mejor[0], 1))
|
||||
open(os.path.join(OUT, 'portada-v56.svg'), 'w').write(svg(d, hl, (bx + 8, y0, W, H), (2400, 400), 'portada'))
|
||||