Modo crisis: semáforo, sitio por pendiente, juntas; lote de crisis

This commit is contained in:
Leopoldo Rodríguez 2026-10-22 13:59:27 -06:00
parent 8a58719901
commit 0e828510a9
23 changed files with 990 additions and 76 deletions

View File

@ -23,6 +23,9 @@ Adriana, Michelle, Jesús and the field technicians work from it; Claude closes
|---|---|---|---|
| **Pendientes** (kanban, «Por persona», Ingeniería/Producción views) | server `/srv/projects/drogheda/tareas` | everyone in the UI; **Claude only through lotes** | `backend/lotes/AAAA-MM-DD_tema.json` → Leo runs `./scripts/tablero.sh revisar` then `aplicar`. Format and rules: `backend/lotes/README.md` |
| **Actividad** (home, Bitácora) | automatic: task history + actas | — | nothing to write; lotes show up as «Claude terminó / creó / comentó en …» |
| **Semáforo** (home, 22-oct) and per-site color | `docs/datos/proyecto.json` → `semaforo` (color rojo/amarillo/verde, titulo, texto, causas with **bold**/[links](/…), destraba, actualizado) + `estado`/`nota`/`meta` on each `sitios[]` item and on `oficina` | Leo decides the color; Claude or Leo edit | update it in every crisis or junta hand-off; map pins and the site list link to `/tableros/pendientes/?sitio=<id>` |
| **Sitio of each task** (22-oct) | task field `sitios` = list of icave · timsa · eit · lzc · qro (no field = qro, Querétaro: fabricación e ingeniería) | everyone in the UI (site checkboxes); Claude through lotes (`"sitios": ["eit"]`) | filter `?sitio=eit,timsa`, view «Por sitio» (`?vista=sitio`); `GET /api/tareas` advertises the list; a lote with `sitios` needs the restarted backend |
| **Juntas** (standup + quick wins, 22-oct) | `docs/tableros/juntas.md` (process, agenda, cadence, register) | Claude or Leo | Leo records with Gemini → Claude turns the transcript into a lote → Leo applies; add a row to «Registro» each time; tags `crisis`, `quick-win`; track `gestion` |
| **Enfoque de la semana** (home) | server `/api/hoy` (`enfoque.json`) | Leo and Adriana only | Claude proposes the text in the hand-off; `enfoque_inicial` in `proyecto.json` shows only while nobody has written one |
| **Ruta a producción**, version cards, fase, pedido, próximas fechas, map pins | `docs/datos/proyecto.json` | Claude or Leo | edit by hand (keep the one-array-per-line format); step states `hecho · ahora · bloqueado · falta`, card tone `hecho · ahora · aviso · bloqueado`; bump `actualizado` |
| **Últimas decisiones** (Ingeniería) | `docs/tableros/ingenieria.md` | Claude or Leo | new `<li><b>DD-mmm (hora)</b> — …</li>` at the top, with the decision number (D-xx-n) and links |

View File

@ -325,6 +325,34 @@ dice por qué antes de escribir nada.
| «los cambios N ya no son los que se aplicaron» | alguien editó un lote ya aplicado | un lote aplicado no se edita: Claude escribe otro |
| un cambio «se salta» y no debía | el título del pendiente real es distinto | pásale a Claude la línea de `revisar`; escribe un lote nuevo con el id correcto |
## A6 · 22-oct: modo crisis — semáforo, sitio de cada pendiente y juntas
Qué cambia: la página de inicio muestra el **semáforo del proyecto** (rojo) y el color de cada sitio (`docs/datos/proyecto.json`);
cada pendiente lleva su **sitio** (`sitios`: icave, timsa, eit, lzc, qro; sin sitio = Querétaro), con filtro y vista **Por sitio**
en Pendientes; pista nueva **Gestión**; página nueva **Juntas**. `backend/tablero.py` acepta `sitios` en los lotes, salta un lote
con errores sin frenar los demás y recorta notas de más de 300 caracteres (lo avisa).
**Cambió `backend/app.py`: hay que reiniciar el servicio ANTES de aplicar lotes con `sitios`.** Con el backend viejo, `revisar`
marca esos cambios como inválidos («el backend todavía no conoce «sitios»: reinicia actas-hbox») y no los manda.
```bash
cd ~/Documents/Claude/Projects/Drogheda/hbox-docs
mkdocs build --strict
git add -A && git commit -m "Modo crisis: semáforo, sitio por pendiente, juntas" && git push
cd .. && git add hbox-docs && git commit -m "hbox-docs: modo crisis" && git push && cd hbox-docs # sube el submódulo
./scripts/deploy.sh # sitio + backend (rsync); la foto todavía con el backend viejo
ssh leo@git.helgen.online 'sudo systemctl restart actas-hbox'
curl -s -u leo:PASS 'https://drogheda.helgen.online/api/tareas?sitio=eit' | head -c 300 # «sitios» en la respuesta
./scripts/tablero.sh revisar # ya con sitios
./scripts/tablero.sh aplicar
```
| Síntoma | Causa | Arreglo |
|---|---|---|
| En Pendientes no aparecen los botones de sitio | navegador con el `kanban.js` viejo | recarga (los `.js` llevan `?v=` desde el 21-oct) |
| `Sitio inválido: …` al guardar | sitio que no existe | icave, timsa, eit, lzc o qro (también acepta Veracruz, Manzanillo, Ensenada, LCT, Querétaro) |
| `revisar` dice «el backend todavía no conoce «sitios»» | no se reinició el servicio | el `restart` de arriba |
---
## B · Verificación después de desplegar

View File

@ -29,7 +29,7 @@ de TIPOS_UNICOS un módulo no puede aparecer en dos actas reales (409).
Colecciones agregadas el 13-oct-2026 (tableros, inventario, sitios) — un JSON por registro + _indice.csv,
campos de auditoría creado_por/creado_en/modificado_por/modificado_en/historial; cualquier usuario
autenticado crea y modifica, solo un administrador borra:
GET/POST /api/tareas · PATCH/DELETE /api/tarea?id= · GET /api/tareas/resumen · POST /api/tareas/orden
GET/POST /api/tareas (?sitio=eit,timsa filtra por sitio; 22-oct) · PATCH/DELETE /api/tarea?id= · GET /api/tareas/resumen · POST /api/tareas/orden
GET /api/plan · PUT /api/plan (admin)
GET/POST /api/inventario · PATCH /api/inventario/item?id= · POST /api/inventario/movimiento
GET /api/inventario/movimientos?item_id= · GET /api/inventario/export.csv
@ -769,7 +769,16 @@ EQUIPO_DIR = os.environ.get("EQUIPO_DIR", os.path.join(DATOS_BASE, "equipo"))
# 2 workers). Vive en la carpeta de datos porque /etc/nginx no es escribible por el servicio.
HTPASSWD_LOCK = os.environ.get("HTPASSWD_LOCK", os.path.join(DATOS_BASE, ".htpasswd.lock"))
PISTAS = ("ingenieria", "produccion", "mecanica", "compras", "campo")
PISTAS = ("ingenieria", "produccion", "mecanica", "compras", "campo", "gestion") # gestion: finanzas, trámites, juntas (22-oct)
# Sitio de cada pendiente (22-oct): dónde pasa el trabajo. Un pendiente puede tocar varios sitios.
# (id, nombre, corto). qro = Querétaro: fabricación e ingeniería (oficina).
SITIOS_TAREA = (("icave", "ICAVE · Veracruz", "ICAVE"), ("timsa", "TIMSA · Manzanillo", "TIMSA"),
("eit", "EIT · Ensenada", "EIT"), ("lzc", "LCT · Lázaro Cárdenas", "LCT"),
("qro", "Querétaro · fabricación e ingeniería", "QRO"))
SITIOS_TAREA_IDS = tuple(s[0] for s in SITIOS_TAREA)
SITIO_OMISION = ["qro"] # pendientes sin sitio (los de antes del 22-oct) cuentan como Querétaro
ALIAS_SITIO = {"lct": "lzc", "lazarocardenas": "lzc", "lazaro-cardenas": "lzc", "veracruz": "icave",
"manzanillo": "timsa", "ensenada": "eit", "queretaro": "qro", "oficina": "qro"}
AREAS = ("direccion", "operaciones", "ingenieria", "layout", "produccion", "pruebas", "fabricacion",
"software", "campo", "asistente")
ESTADOS = ("por-hacer", "en-curso", "bloqueado", "hecho")
@ -1184,7 +1193,7 @@ def usuario_borrar():
# ------------------------------------------------------------------ tareas (tablero)
COLS_TAREAS = ["id", "titulo", "pista", "estado", "responsable", "responsable_ids", "fecha_limite", "version_tarjeta",
COLS_TAREAS = ["id", "titulo", "pista", "estado", "responsable", "responsable_ids", "fecha_limite", "version_tarjeta", "sitios",
"orden", "etiquetas", "bloqueada_por", "creado_por", "creado_en", "modificado_por", "modificado_en"]
@ -1269,9 +1278,36 @@ def _tarea_valida(d, base=None):
if v not in VERSIONES_TARJETA:
return None, f"Versión de tarjeta inválida: {v}. Usa una de {', '.join(VERSIONES_TARJETA)}."
pon("version_tarjeta", v)
if alta or "sitios" in d:
ids, err = _sitios_validos(d.get("sitios"), alta)
if err:
return None, err
pon("sitios", ids)
return t, None
def _sitios_validos(v, alta):
"""['eit', 'Manzanillo'] → (['eit', 'timsa'], None). Vacío: en un alta, Querétaro; en un cambio, error."""
if v in (None, "", []):
if alta:
return list(SITIO_OMISION), None
return None, "Elige al menos un sitio (" + ", ".join(SITIOS_TAREA_IDS) + ")."
if isinstance(v, str):
v = [x for x in re.split(r"[,;/\s]+", v) if x]
if not isinstance(v, list):
return None, "«sitios» es una lista de ids: " + ", ".join(SITIOS_TAREA_IDS) + "."
ids = []
for x in v:
k = _norm_id(x)
k = ALIAS_SITIO.get(k, k)
if k not in SITIOS_TAREA_IDS:
return None, (f"Sitio inválido: {x}. Usa: icave (Veracruz), timsa (Manzanillo), eit (Ensenada), "
"lzc (LCT, Lázaro Cárdenas) o qro (Querétaro).")
if k not in ids:
ids.append(k)
return ids, None
def _guardar_tarea(t):
_escribir_json(_ruta_registro(TAREAS_DIR, t["id"]), t)
_reescribir_indice(TAREAS_DIR, _listar_json(TAREAS_DIR), COLS_TAREAS)
@ -1289,12 +1325,17 @@ def tareas_listar():
resp = _texto(request.args.get("responsable"), 80).lower()
resp_id = _norm_id(resp)
version = _texto(request.args.get("version"), 10)
sitio = {ALIAS_SITIO.get(_norm_id(x), _norm_id(x)) for x in _texto(request.args.get("sitio"), 80).split(",") if x.strip()}
hoy = datetime.date.today().isoformat()
miembros = _miembros()
out = []
for t in _listar_json(TAREAS_DIR):
if "responsable_ids" not in t: # pendientes guardados antes del 14-oct
t["responsable_ids"] = _ids_responsable(t.get("responsable"), miembros)[0]
if not t.get("sitios"): # pendientes de antes del 22-oct: Querétaro
t["sitios"] = list(SITIO_OMISION)
if sitio and not sitio.intersection(t["sitios"]):
continue
if pista and t.get("pista") != pista:
continue
if estado and t.get("estado") != estado:
@ -1309,7 +1350,8 @@ def tareas_listar():
t.get("orden", 0), t.get("creado_en", "")))
equipo = {k: {"nombre": m.get("nombre"), "activo": m.get("activo", True), "area": m.get("area")} for k, m in miembros.items()}
return jsonify(ok=True, total=len(out), tareas=out, pistas=PISTAS, estados=ESTADOS, equipo=equipo,
versiones=VERSIONES_TARJETA, admin=is_admin(), user=current_user())
versiones=VERSIONES_TARJETA, sitios=[{"id": i, "nombre": n, "corto": c} for i, n, c in SITIOS_TAREA],
admin=is_admin(), user=current_user())
@app.post("/api/tareas")
@ -1354,6 +1396,8 @@ def tarea_modificar():
base = _leer_json(ruta)
if not base:
return jsonify(ok=False, error=f"No existe el pendiente {rid}."), 404
if not base.get("sitios"): # pendiente de antes del 22-oct: es de Querétaro
base["sitios"] = list(SITIO_OMISION)
d = _json_body()
if d is None:
return jsonify(ok=False, error="JSON inválido"), 400

View File

@ -10,7 +10,7 @@
"poner": {
"estado": "hecho"
},
"nota": "Cerrado por la auditoría final del 22-oct: los paquetes HBOX_v5_Rev6_PROD (V5.6) y HBOX_v5_Rev7_PROD (V5.7) son idénticos a una regeneración con relleno fresco (0.000 mm² de diferencia en el cobre; máscaras, pasta, serigrafía y taladros iguales); DRC solo con los 3 errores de MOD1, 0 sin conectar, paridad solo PWR_FLAG; RS1 Kelvin en VIN y CLN en las dos; BOM y CPL cuadran con el PCB (226 / 229 piezas, todas con LCSC). Lo que sigue es el pedido (t-v56-piloto) y P-1 (t-p1-relleno-commit). Ver /hardware/auditoria-final-22oct/"
"nota": "Cerrado por la auditoría final del 22-oct: los paquetes HBOX_v5_Rev6_PROD (V5.6) y HBOX_v5_Rev7_PROD (V5.7) son idénticos a una regeneración con relleno fresco (0.000 mm² de diferencia en el cobre; máscaras, pasta, serigrafía y taladros iguales); DRC solo con los 3 errores de MOD1, 0 sin conectar, paridad solo PWR_FLAG; RS1 Kelvin en VIN y CLN en las dos; BOM y CPL cuadran con el PCB (226 / 229 piezas, todas con LCSC). Lo que sigue es el pedido (t-v56-piloto) y P-1 (t-p1-relleno-commit). Ver /hardware/auditoria-final-22oct/ Re-verificado a las 13:30 sobre los archivos de las 13:00 (P-1 cerrado): mismo cobre, mismo paquete."
},
{
"id": "t-v56-piloto",
@ -89,13 +89,13 @@
{
"crear": {
"id": "t-p1-relleno-commit",
"titulo": "P-1: abrir V5.6 y V5.7 en KiCad, B (rellenar), guardar y commit — el repo trae el relleno viejo",
"titulo": "P-1: commit de kicad/ y production/ con el relleno fresco (el relleno ya quedó en los archivos a las 13:00)",
"pista": "ingenieria",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "V5.6",
"fecha_limite": "2026-10-22",
"descripcion": "Los dos .kicad_pcb del repo traen el relleno de zonas de la pasada 5. El Gerber del Fabrication Toolkit sí lleva el relleno fresco (verificado: 0.000 mm² de diferencia), pero una exportación sin rellenar (kicad-cli, o el GUI sin B) manda cobre de GND sobre la vía de /SYS5V de T35 y las vías de RS1 de T34. Son 5 min: abrir cada tarjeta, B, guardar, DRC (solo los 3 de MOD1), commit «V5.6/V5.7: relleno fresco (P-1)», push y bump del submódulo. Regla nueva (D-22-3): todo script T que toque cobre termina con rellenado en el GUI antes del commit.",
"descripcion": "Hecho a las 13:00: Leo rellenó las zonas, guardó los dos .kicad_pcb y regeneró HBOX_v5_Rev6_PROD / Rev7_PROD. Re-verificado a las 13:30: relleno guardado = relleno fresco (0.000 mm² en las dos), mismo cobre y mismo paquete que en la mañana, BOM/CPL iguales, DRC igual. Falta: commit «V5.6/V5.7: relleno fresco (P-1) + paquetes 13:00», push y bump del submódulo. Regla D-22-3: todo script T que toque cobre termina con rellenado en el GUI antes del commit; después de cualquier cambio, scripts/verify_mac.sh + gcmp_b.py (5 min) antes de pagar.",
"enlaces": [
{
"titulo": "P-1 en la auditoría final (§2)",

View File

@ -0,0 +1,493 @@
{
"lote": "2026-10-22_crisis",
"autor": "claude",
"fecha": "2026-10-22",
"motivo": "Modo crisis (Leo, 22-oct): semáforo rojo. Sin pago del mes no hay compras; meta de 1–2 unidades por sitio con TIMSA, EIT e ICAVE bloqueados; software: retranslator de Wialon y desconexiones súbitas; juntas con Gemini; clasificación por sitio; instalaciones sin vehículo ni prueba",
"fuente": "/",
"cambios": [
{
"crear": {
"id": "t-cobranza-octubre",
"titulo": "Cobranza: conseguir el pago del mes — sin él no hay compras",
"pista": "gestion",
"estado": "en-curso",
"responsable": "leo, adriana",
"version_tarjeta": "—",
"sitios": [
"qro"
],
"descripcion": "22-oct: no se ha recibido ningún pago este mes. Operamos muy justos y no se han hecho compras significativas. Es la causa principal del semáforo rojo.\nCuando paguemos a proveedores se destraban la mayoría de las tareas: material para las instalaciones, ferretería de EIT y TIMSA, pre-órdenes de la v6 y el resto de las compras del pedido de 570.",
"etiquetas": [
"crisis",
"finanzas"
]
},
"si_no_hay": [
"pago del mes",
"cobranza"
]
},
{
"crear": {
"id": "t-compras-prioridad",
"titulo": "Lista de compras priorizada, lista para el día que llegue el pago",
"pista": "compras",
"estado": "por-hacer",
"responsable": "adriana, leo",
"version_tarjeta": "—",
"sitios": [
"qro"
],
"descripcion": "Se puede hacer hoy, sin dinero (quick win). Orden propuesto, a confirmar en la junta: 1) lo que destraba instalaciones (ferretería de EIT y TIMSA, material en sitio); 2) piloto 5 V5.6 + 5 V5.7 si no se pagó antes; 3) pre-órdenes de la v6 (stock limitado de THVD1410DR, TPS7A2033 y J14); 4) material del pedido de 570. Con monto y proveedor por renglón.",
"etiquetas": [
"quick-win",
"finanzas"
]
},
"si_no_hay": [
"compras priorizada",
"lista de compras"
]
},
{
"id": "t-compras-materiales",
"poner": {
"estado": "bloqueado",
"bloqueada_por": [
"t-cobranza-octubre"
]
},
"nota": "22-oct, modo crisis: sin el pago del mes no hay compras. Bloqueado hasta la cobranza (t-cobranza-octubre); el orden de compra queda en t-compras-prioridad."
},
{
"id": "t-preordenes-v6",
"poner": {
"estado": "bloqueado",
"bloqueada_por": [
"t-cobranza-octubre"
]
},
"nota": "22-oct: vencido el 20-oct y sin pago del mes: bloqueado hasta la cobranza. Ojo: el stock de THVD1410DR, TPS7A2033 y J14 es limitado; es de lo primero en cuanto haya pago."
},
{
"id": "t-ferreteria-eit-timsa",
"poner": {
"estado": "bloqueado",
"bloqueada_por": [
"t-cobranza-octubre"
],
"sitios": [
"eit",
"timsa"
]
},
"nota": "22-oct: sigue sin cerrarse (vencido el 14-oct) y sin pago del mes no hay compras. Es de lo primero en la lista: destraba las instalaciones de EIT y TIMSA."
},
{
"id": "t-preorden-108-322-bom",
"poner": {
"bloqueada_por": [
"t-v56-banco-piloto",
"t-v57-banco-prototipos",
"t-cobranza-octubre"
]
},
"nota": "22-oct, modo crisis: además de los bancos del piloto, la pre-orden espera el pago del mes (t-cobranza-octubre)."
},
{
"id": "d-22-1-go-piloto",
"nota": "Con el semáforo en rojo, decidir también si el piloto 5 + 5 se paga ya o espera la cobranza del mes (t-cobranza-octubre). Se ve en la junta del 22-oct."
},
{
"crear": {
"id": "t-asignar-campo",
"titulo": "Asignar un responsable de campo por sitio (ICAVE, TIMSA, EIT, LCT)",
"pista": "gestion",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "—",
"fecha_limite": "2026-10-22",
"sitios": [
"icave",
"timsa",
"eit",
"lzc"
],
"descripcion": "En la junta del 22-oct. Los pendientes de instalación por sitio están a nombre de Leo mientras tanto; al asignar, cambiar el responsable de cada uno.",
"etiquetas": [
"quick-win"
]
},
"si_no_hay": [
"responsable de campo"
]
},
{
"crear": {
"id": "t-registrar-instaladas",
"titulo": "Registrar las 3–4 unidades ya instaladas: serie, sitio, vehículo (tipo y número económico), fecha y técnico",
"pista": "campo",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "—",
"fecha_limite": "2026-10-24",
"sitios": [
"icave",
"timsa",
"eit",
"lzc"
],
"descripcion": "22-oct: hay 3 o 4 unidades instaladas y no sabemos en qué vehículo está cada una. Llenar el acta de instalación de cada una con la fecha real (serie, sitio, tipo de equipo y número económico, técnico), para que aparezcan en el tablero de Instalaciones con su vehículo y su estado. Lo hace quien estuvo en la instalación; responsable por sitio: t-asignar-campo.",
"enlaces": [
{
"titulo": "Instalaciones por sitio",
"url": "/tableros/instalaciones/"
},
{
"titulo": "Acta de instalación",
"url": "/formularios/instalacion.html"
}
],
"etiquetas": [
"quick-win",
"instalación"
]
},
"si_no_hay": [
"ya instaladas",
"registrar las unidades"
]
},
{
"crear": {
"id": "t-prueba-en-sitio",
"titulo": "Prueba en sitio después de instalar: hacerla y registrarla en cada unidad instalada",
"pista": "campo",
"estado": "por-hacer",
"responsable": "leo, claude",
"version_tarjeta": "—",
"sitios": [
"icave",
"timsa",
"eit",
"lzc"
],
"descripcion": "22-oct: hay metas de instalación pero no hay prueba. Cada unidad instalada pasa una prueba en sitio con su registro: fix RTK visto en el monitor, reporta en el monitor y en Wialon, posición coherente en el mapa y fotos. Una unidad cuenta para la meta de 1–2 por sitio solo con la prueba aprobada. El registro nuevo vive en el tablero de Instalaciones.",
"enlaces": [
{
"titulo": "Instalaciones por sitio",
"url": "/tableros/instalaciones/"
}
],
"etiquetas": [
"instalación",
"prueba"
]
},
"si_no_hay": [
"prueba en sitio"
]
},
{
"crear": {
"id": "t-timsa-tramite-vehiculo",
"titulo": "TIMSA: resolver el trámite administrativo del vehículo en Manzanillo",
"pista": "gestion",
"estado": "bloqueado",
"responsable": "adriana, leo",
"version_tarjeta": "—",
"sitios": [
"timsa"
],
"descripcion": "22-oct: TIMSA está detenido por un trámite administrativo del vehículo que tenemos en Manzanillo. Qué falta, quién lo resuelve y para cuándo: por confirmar en la junta (en septiembre el bloqueo fueron las placas y el permiso del vehículo).",
"etiquetas": [
"crisis",
"trámite"
]
},
"si_no_hay": [
"trámite administrativo del vehículo",
"trámite del vehículo"
]
},
{
"crear": {
"id": "t-timsa-instalar",
"titulo": "TIMSA (Manzanillo): instalar 1–2 unidades",
"pista": "campo",
"estado": "bloqueado",
"responsable": "leo",
"version_tarjeta": "—",
"sitios": [
"timsa"
],
"bloqueada_por": [
"t-timsa-tramite-vehiculo"
],
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas por sitio. Bloqueado por el trámite del vehículo. Al instalar: checklist de puesta en marcha, acta de instalación con el vehículo y prueba en sitio (t-prueba-en-sitio). Responsable de campo: t-asignar-campo.",
"enlaces": [
{
"titulo": "Checklist de puesta en marcha",
"url": "/procedimientos/v5-checklist-puesta-en-marcha/"
},
{
"titulo": "Acta de instalación",
"url": "/formularios/instalacion.html"
}
],
"etiquetas": [
"meta-sitio"
]
},
"si_no_hay": [
"TIMSA (Manzanillo): instalar",
"instalar en TIMSA"
]
},
{
"crear": {
"id": "t-eit-material",
"titulo": "EIT: seguir el material enviado desde Manzanillo (guía, fecha de llegada y acta de recepción)",
"pista": "campo",
"estado": "en-curso",
"responsable": "adriana",
"version_tarjeta": "—",
"sitios": [
"eit",
"timsa"
],
"descripcion": "22-oct: EIT (Ensenada) espera el material que salió de Manzanillo. Registrar la guía y la fecha estimada aquí; al llegar, acta de recepción. Con eso se destraba la instalación de EIT.",
"enlaces": [
{
"titulo": "Acta de recepción",
"url": "/formularios/recepcion.html"
}
],
"etiquetas": [
"crisis",
"envío"
]
},
"si_no_hay": [
"material enviado desde Manzanillo"
]
},
{
"crear": {
"id": "t-eit-instalar",
"titulo": "EIT (Ensenada): instalar 1–2 unidades",
"pista": "campo",
"estado": "bloqueado",
"responsable": "leo",
"version_tarjeta": "—",
"sitios": [
"eit"
],
"bloqueada_por": [
"t-eit-material"
],
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas por sitio. Espera el material que viene de Manzanillo. Al instalar: checklist de puesta en marcha, acta de instalación con el vehículo y prueba en sitio (t-prueba-en-sitio). Responsable de campo: t-asignar-campo.",
"enlaces": [
{
"titulo": "Checklist de puesta en marcha",
"url": "/procedimientos/v5-checklist-puesta-en-marcha/"
},
{
"titulo": "Acta de instalación",
"url": "/formularios/instalacion.html"
}
],
"etiquetas": [
"meta-sitio"
]
},
"si_no_hay": [
"EIT (Ensenada): instalar",
"instalar en EIT"
]
},
{
"crear": {
"id": "t-icave-campo",
"titulo": "ICAVE (Veracruz): investigar en campo las fallas abiertas en la próxima ventana de buen clima",
"pista": "campo",
"estado": "bloqueado",
"responsable": "leo",
"version_tarjeta": "—",
"sitios": [
"icave"
],
"descripcion": "22-oct: el mal clima en Veracruz deja poco tiempo en campo y hay fallas que solo se pueden investigar ahí. Preparar desde hoy la lista de unidades y qué medir en cada una, para aprovechar la ventana. Regla: nada se reinicia antes de capturar la evidencia; acta de diagnóstico en campo con fotos.",
"enlaces": [
{
"titulo": "Diagnóstico en campo",
"url": "/diagnostico/procedimiento-diagnostico-campo/"
},
{
"titulo": "Acta de diagnóstico en campo",
"url": "/formularios/diagnostico.html"
}
],
"etiquetas": [
"crisis",
"clima"
]
},
"si_no_hay": [
"ventana de buen clima"
]
},
{
"crear": {
"id": "t-icave-unidades",
"titulo": "ICAVE (Veracruz): confirmar 1–2 unidades instaladas y reportando bien",
"pista": "campo",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "—",
"sitios": [
"icave"
],
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas por sitio. En ICAVE ya hay unidades; confirmar cuántas reportan bien en el monitor y en Wialon, y cuáles van a la lista de t-icave-campo.",
"etiquetas": [
"meta-sitio"
]
},
"si_no_hay": [
"ICAVE (Veracruz): confirmar"
]
},
{
"crear": {
"id": "t-lct-instalar",
"titulo": "LCT (Lázaro Cárdenas): instalar 1–2 unidades — estado por confirmar en la junta",
"pista": "campo",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "—",
"sitios": [
"lzc"
],
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas por sitio. El estado de LCT no se revisó el 22-oct: confirmar en la junta qué hay en sitio y qué falta.",
"enlaces": [
{
"titulo": "Acta de instalación",
"url": "/formularios/instalacion.html"
}
],
"etiquetas": [
"meta-sitio"
]
},
"si_no_hay": [
"LCT (Lázaro Cárdenas): instalar",
"instalar en LCT"
]
},
{
"id": "t-sitios-alta",
"poner": {
"titulo": "Dar de alta los 4 sitios (ICAVE, LCT, EIT, TIMSA) con contactos y unidades instaladas",
"sitios": [
"icave",
"timsa",
"eit",
"lzc"
]
},
"nota": "Clasificado por sitio (22-oct). LCT es la sigla correcta (21-oct)."
},
{
"id": "t-acta-instalacion",
"poner": {
"sitios": [
"icave",
"timsa",
"eit",
"lzc"
]
},
"nota": "Es para las instalaciones de los cuatro puertos: hace falta para la meta de 1–2 unidades por sitio (22-oct)."
},
{
"crear": {
"id": "t-wialon-retranslator",
"titulo": "Software: retranslator de Wialon hacia nuestro servidor para comparar en el monitor RTK contra el GPS legado",
"pista": "ingenieria",
"estado": "por-hacer",
"responsable": "gibranlp, leo",
"version_tarjeta": "—",
"sitios": [
"qro",
"icave"
],
"descripcion": "Que Wialon reenvíe (retranslator) las posiciones del GPS legado (CalAmp) a nuestro servidor, para ver en el monitor la posición del RTK contra la del legado en la misma máquina. Sirve para justificar con datos los viajes de ICAVE. No requiere compras (quick win).",
"etiquetas": [
"software",
"quick-win"
]
},
"si_no_hay": [
"retranslator de Wialon"
]
},
{
"crear": {
"id": "t-desconexiones",
"titulo": "Software: investigar de inmediato las desconexiones súbitas",
"pista": "ingenieria",
"estado": "por-hacer",
"responsable": "gibranlp, claude",
"version_tarjeta": "—",
"fecha_limite": "2026-10-23",
"sitios": [
"qro"
],
"descripcion": "Pedido de Leo (22-oct): inmediato. Juntar la lista de desconexiones (unidad, sitio, hora), revisar los registros del monitor y de la unidad (Grafana/Loki) y descartar primero lo barato (backend, SIM, caster) antes de culpar al hardware. Un pendiente por causa encontrada.",
"enlaces": [
{
"titulo": "Protocolo de diagnóstico remoto",
"url": "/diagnostico/protocolo-diagnostico-remoto/"
}
],
"etiquetas": [
"software",
"crisis"
]
},
"si_no_hay": [
"desconexiones súbitas"
]
},
{
"crear": {
"id": "t-juntas",
"titulo": "Juntas periódicas (standup + quick wins) grabadas con Gemini; Claude actualiza el tablero",
"pista": "gestion",
"estado": "en-curso",
"responsable": "leo, adriana, claude",
"version_tarjeta": "—",
"sitios": [
"qro",
"icave",
"timsa",
"eit",
"lzc"
],
"descripcion": "Cada junta: Leo pasa la transcripción o las notas de Gemini a Claude; Claude escribe un lote (cierra, abre y comenta, con responsable y fecha); Leo lo revisa y lo aplica, y queda en la Actividad. Agenda fija: semáforo, cada sitio, bloqueos, quick wins y acuerdos. Frecuencia: se acuerda en la junta del 22-oct.",
"enlaces": [
{
"titulo": "Juntas",
"url": "/tableros/juntas/"
}
],
"etiquetas": [
"proceso"
]
},
"si_no_hay": [
"juntas periódicas"
]
}
]
}

View File

@ -58,7 +58,7 @@
]
},
"si_no_hay": [
"T39"
"push de T39"
]
},
{

View File

@ -22,10 +22,11 @@ y queda en el historial del pendiente con su autor y su nota. En la página de i
alguien deshace un cambio a mano, el lote no lo vuelve a hacer.
4. **No se toca lo que ya está en Hecho**, salvo que el cambio lo diga con `si_estado`.
5. **Cada cambio a un pendiente existente lleva `nota`**: el porqué, en una o dos frases (≤ 300
caracteres). Es lo que la gente lee en el historial.
6. **Se busca antes de crear.** `crear` usa un `id` fijo (si ya existe, se salta) y `si_no_hay` evita
caracteres; si es más larga, se recorta a 300 y `revisar` lo avisa). Es lo que la gente lee en el historial.
6. **Un lote con errores de formato se salta** (y `revisar` dice por qué); los demás lotes sí se aplican.
7. **Se busca antes de crear.** `crear` usa un `id` fijo (si ya existe, se salta) y `si_no_hay` evita
duplicar un pendiente que alguien ya abrió a mano con otro id.
7. **Leo revisa antes de aplicar.** `revisar` muestra cada cambio contra el tablero real; lo que no
8. **Leo revisa antes de aplicar.** `revisar` muestra cada cambio contra el tablero real; lo que no
cuadra se salta y se dice por qué (no existe, ya está en Hecho, el título coincide con varios…).
## Formato
@ -71,9 +72,9 @@ y queda en el historial del pendiente con su autor y su nota. En la página de i
| `crear` | cambio | Pendiente nuevo, con los mismos campos que el formulario del sitio más un `id` fijo (`t-…`, `d-…`): `titulo`, `pista`, `responsable` obligatorios |
| `si_no_hay` | cambio | Con `crear`: no lo crea si hay un pendiente abierto cuyo título contenga alguno de estos textos |
Valores: `pista` ∈ ingenieria · produccion · mecanica · compras · campo; `estado` ∈ por-hacer ·
Valores: `pista` ∈ ingenieria · produccion · mecanica · compras · campo · gestion (desde el 22-oct); `estado` ∈ por-hacer ·
en-curso · bloqueado · hecho; `version_tarjeta` ∈ V5.5 · V5.6 · V5.7 · v6 · —; `responsable`, ids del
[Equipo](https://drogheda.helgen.online/tableros/equipo/) separados por coma; `fecha_limite` AAAA-MM-DD.
[Equipo](https://drogheda.helgen.online/tableros/equipo/) separados por coma; `fecha_limite` AAAA-MM-DD; `sitios` (desde el 22-oct) = lista de icave (Veracruz) · timsa (Manzanillo) · eit (Ensenada) · lzc (LCT) · qro (Querétaro: fabricación e ingeniería) — un pendiente puede tener varios; sin `sitios`, cuenta como Querétaro. Con un backend sin reiniciar, `revisar` marca los cambios con `sitios` como inválidos.
## La foto

View File

@ -45,7 +45,7 @@ ESTADOS = ("por-hacer", "en-curso", "bloqueado", "hecho")
ABIERTOS = ("por-hacer", "en-curso", "bloqueado")
NOMBRE = {"por-hacer": "Por hacer", "en-curso": "En curso", "bloqueado": "Bloqueado", "hecho": "Hecho"}
CAMPOS = ("titulo", "descripcion", "pista", "estado", "responsable", "fecha_limite", "enlaces", "etiquetas",
"bloqueada_por", "version_tarjeta")
"bloqueada_por", "version_tarjeta", "sitios")
LOTE_ID = re.compile(r"^\d{4}-\d{2}-\d{2}_[a-z0-9][a-z0-9-]{1,60}$")
TAREA_ID = re.compile(r"^[a-z0-9][a-z0-9-]{2,59}$")
MES = ("ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic")
@ -110,7 +110,8 @@ def llamar(metodo, ruta, usuario, cuerpo=None):
"¿Está arriba el servicio actas-hbox?")
VALORES = {} # pista y version_tarjeta válidas, tal como las publica el backend en GET /api/tareas
VALORES = {} # pista, version_tarjeta y sitios válidos, tal como los publica el backend en GET /api/tareas
SITIOS_INFO = [] # [{id, nombre, corto}] del backend (desde el 22-oct)
def leer_tareas():
@ -118,15 +119,31 @@ def leer_tareas():
if st != 200 or not j.get("ok"):
raise SinBackend(f"No pude leer los pendientes: {j.get('error') or st}")
VALORES.update(pista=list(j.get("pistas") or []), version_tarjeta=list(j.get("versiones") or []))
SITIOS_INFO[:] = [x for x in (j.get("sitios") or []) if isinstance(x, dict) and x.get("id")]
VALORES["sitios"] = [x["id"] for x in SITIOS_INFO]
return j.get("tareas") or [], j.get("equipo") or {}
def valores_malos(campos):
"""Lo que el backend rechazaría: se ve en «revisar» en vez de fallar al aplicar."""
"""Lo que el backend rechazaría (o ignoraría): se ve en «revisar» en vez de fallar al aplicar."""
malos = []
for k, validos in VALORES.items():
if k in campos and validos and campos[k] not in validos:
if k == "sitios" or k not in campos:
continue
if validos and campos[k] not in validos:
malos.append(f"{k} «{campos[k]}» no existe (usa {', '.join(validos)})")
if "sitios" in campos:
validos = VALORES.get("sitios") or []
if not validos:
# un backend sin reiniciar guardaría el pendiente y tiraría «sitios» sin avisar
malos.append("el backend todavía no conoce «sitios»: reinicia actas-hbox")
else:
lista = campos["sitios"] if isinstance(campos["sitios"], list) else [campos["sitios"]]
fuera = [str(x) for x in lista if str(x) not in validos]
if not lista:
malos.append("«sitios» vacío: pon al menos uno")
elif fuera:
malos.append(f"sitios {', '.join(fuera)} no existen (usa {', '.join(validos)})")
return malos
@ -193,8 +210,7 @@ def validar_cambio(c):
nota = str(c.get("nota") or "").strip()
if not nota:
e.append("falta «nota»: el porqué del cambio, queda en el historial del pendiente")
elif len(nota) > 300:
e.append(f"«nota» de {len(nota)} caracteres; el backend guarda 300")
# más de 300 caracteres: el backend guarda 300; se recorta y «revisar» lo dice (no bloquea el lote)
p = c.get("poner", {})
if not isinstance(p, dict):
e.append("«poner» debe ser un objeto")
@ -265,6 +281,9 @@ def igual(campo, actual, nuevo):
return _ids(actual) == _ids(nuevo)
if campo in ("etiquetas", "bloqueada_por"):
return sorted(norm(x) for x in (actual or [])) == sorted(norm(x) for x in (nuevo or []))
if campo == "sitios":
lst = lambda v: v if isinstance(v, list) else ([v] if v else [])
return sorted({norm(x) for x in lst(actual)}) == sorted({norm(x) for x in lst(nuevo)})
if campo == "enlaces":
par = lambda lista: [(str(x.get("titulo", "")).strip(), str(x.get("url", "")).strip())
for x in (lista or []) if isinstance(x, dict)]
@ -309,8 +328,12 @@ def _cambio_a(t, c):
if valores_malos(poner):
return "✗ " + f"{_quien_es(t)}: " + "; ".join(valores_malos(poner)), None
partes = [f"{k}: {corto(t.get(k), 30)} → {corto(v, 30)}" for k, v in poner.items()]
detalle = f"{_quien_es(t)}: " + ("; ".join(partes) + " · " if partes else "") + f"nota «{corto(c['nota'], 80)}»"
return detalle, ("PATCH", "/api/tarea?id=" + urllib.parse.quote(t["id"]), dict(poner, _nota=c["nota"].strip()), t["id"])
nota = c["nota"].strip()
aviso = ""
if len(nota) > 300:
nota, aviso = nota[:299].rstrip() + "…", f" (nota de {len(c['nota'].strip())} caracteres, recortada a 300)"
detalle = f"{_quien_es(t)}: " + ("; ".join(partes) + " · " if partes else "") + f"nota «{corto(nota, 80)}»{aviso}"
return detalle, ("PATCH", "/api/tarea?id=" + urllib.parse.quote(t["id"]), dict(poner, _nota=nota), t["id"])
def planear(c, tareas):
@ -333,6 +356,10 @@ def planear(c, tareas):
datos = [cuerpo["pista"], NOMBRE.get(cuerpo["estado"], cuerpo["estado"]), cuerpo["responsable"]]
if cuerpo.get("fecha_limite"):
datos.append("vence " + fecha_corta(cuerpo["fecha_limite"]))
if cuerpo.get("sitios"): # 22-oct: el sitio también se revisa antes de aplicar
cortos = {x["id"]: x.get("corto") or x["id"] for x in SITIOS_INFO}
sit = cuerpo["sitios"] if isinstance(cuerpo["sitios"], list) else [cuerpo["sitios"]]
datos.append("sitio " + " · ".join(cortos.get(x, x) for x in sit))
return "crear", f"nuevo {cuerpo['id']} «{corto(cuerpo['titulo'], 70)}» ({', '.join(datos)})", \
[("POST", "/api/tareas", cuerpo, cuerpo["id"])]
if c.get("id"):
@ -382,7 +409,13 @@ def simular(tareas, peticion):
def correr(rutas, aplicar, silencioso=False):
archivos = archivos_de(rutas)
lotes = [(a, cargar_lote(a)) for a in archivos] # todos válidos o ninguno se aplica
# un lote con errores de formato se salta (y se dice por qué); los demás sí se revisan y aplican
lotes, malos = [], []
for a in archivos:
try:
lotes.append((a, cargar_lote(a)))
except ValueError as ex:
malos.append(str(ex))
ids = [lote["lote"] for _, lote in lotes]
if len(ids) != len(set(ids)):
raise ValueError("Hay dos lotes con el mismo «lote».")
@ -390,6 +423,9 @@ def correr(rutas, aplicar, silencioso=False):
tareas, _ = leer_tareas()
por_aplicar = errores = 0
di = (lambda *a: None) if silencioso else print
for m in malos:
di("✗ " + m + "\n → este lote no se aplica hasta corregirlo (un lote nuevo); los demás sí.")
errores += len(malos)
if not lotes:
di("No hay lotes en " + ", ".join(rutas) + ".")
for archivo, lote in lotes:
@ -473,7 +509,7 @@ def correr(rutas, aplicar, silencioso=False):
def foto():
tareas, equipo = leer_tareas()
out = {"tomada": ahora(), "api": API, "tareas": tareas, "equipo": equipo}
out = {"tomada": ahora(), "api": API, "tareas": tareas, "equipo": equipo, "sitios": list(SITIOS_INFO)}
st, h = llamar("GET", "/api/hoy", LECTOR)
out["enfoque"] = h.get("enfoque") if st == 200 else None
st, a = llamar("GET", "/api/actividad?dias=14&limite=150", LECTOR)
@ -492,6 +528,8 @@ def resumen(ruta):
apodo = {"Leopoldo": "Leo"} # como en el sitio (Kanban.nombreCorto)
nombre = lambda i: (lambda n: apodo.get(n, n))(((eq.get(i) or {}).get("nombre") or i or "—").split(" ")[0])
estado_de = {t.get("id"): t.get("estado") for t in tareas}
sitios_info = F.get("sitios") or []
sit_corto = {x["id"]: x.get("corto") or x["id"] for x in sitios_info}
abiertos = [t for t in tareas if t.get("estado") != "hecho"]
vencido = lambda t: bool(t.get("fecha_limite")) and t.get("estado") != "hecho" and t["fecha_limite"] < hoy
cuenta = {e: sum(1 for t in tareas if t.get("estado") == e) for e in ESTADOS}
@ -515,6 +553,8 @@ def resumen(ruta):
espera = [x for x in (t.get("bloqueada_por") or []) if estado_de.get(x) != "hecho"]
if espera:
bits.append("espera a " + ", ".join(espera))
if sit_corto and t.get("sitios"):
bits.append("sitio: " + ", ".join(sit_corto.get(x, x) for x in t["sitios"]))
return f"- {t.get('titulo')} — `{t.get('id')}` · " + " · ".join(bits)
orden = lambda t: (ESTADOS.index(t.get("estado")) if t.get("estado") in ESTADOS else 9, t.get("fecha_limite") or "9999")
@ -525,6 +565,14 @@ def resumen(ruta):
o += ["## Abiertos por persona", ""]
for p in sorted(personas, key=lambda x: (-len(personas[x]), x)):
o += [f"### {nombre(p)} ({len(personas[p])})", ""] + [linea(t) for t in sorted(personas[p], key=orden)] + [""]
if sitios_info:
o += ["## Abiertos por sitio", ""]
for x in sitios_info:
lst = [t for t in abiertos if x["id"] in (t.get("sitios") or [])]
bl = sum(1 for t in lst if t.get("estado") == "bloqueado")
o += [f"### {x.get('nombre') or x['id']} ({len(lst)}" + (f", {bl} bloqueado{'s' if bl > 1 else ''}" if bl else "") + ")", ""]
o += [linea(t) for t in sorted(lst, key=orden)] or ["- (nada abierto)"]
o += [""]
hace14 = (datetime.date.fromisoformat(hoy) - datetime.timedelta(days=14)).isoformat()
hechos = sorted((t for t in tareas if t.get("estado") == "hecho" and str(t.get("modificado_en") or "") >= hace14),
key=lambda t: str(t.get("modificado_en")), reverse=True)
@ -565,8 +613,8 @@ def main():
rutas = a.rutas or [aqui]
if a.accion == "pendientes":
n, err = correr(rutas, aplicar=False, silencioso=True)
print(f"Tablero: {n} cambio(s) de lotes por aplicar → ./scripts/tablero.sh revisar" if n
else "Tablero: no hay lotes por aplicar.")
print((f"Tablero: {n} cambio(s) de lotes por aplicar" if n else "Tablero: no hay lotes por aplicar")
+ (f"; {err} con errores" if err else "") + (" → ./scripts/tablero.sh revisar" if n or err else "."))
return 3 if err else 0
n, err = correr(rutas, aplicar=(a.accion == "aplicar"))
return 3 if err else 0

View File

@ -23,6 +23,8 @@ Arriba, lo que pasó en el tablero de pendientes y en las actas en los últimos
### Octubre de 2026
**22 oct**
: [Juntas: standup y quick wins](tableros/juntas.md): **modo crisis, semáforo en rojo** (sin pago este mes y compras detenidas; TIMSA bloqueado por el trámite del vehículo; EIT espera material de Manzanillo; ICAVE con mal clima). El tablero ahora tiene **sitio** por pendiente (ICAVE, TIMSA, EIT, LCT, Querétaro) con filtro y vista *Por sitio*, y la pista **Gestión**. Las juntas se graban con Gemini y Claude pasa los acuerdos al tablero con un lote.
: [Re-verificación de las 13:30](hardware/auditoria-final-22oct.md#2-que-se-reviso-metodo-ag): Leo rellenó las zonas, guardó y regeneró los paquetes a las 13:00 → **P-1 cerrado** (relleno guardado = fresco, 0.000 mm²; mismo cobre, mismo paquete, BOM/CPL iguales; DRC igual). El veredicto no cambia; falta el commit. Presentación e informe reemitidos con la re-verificación; `verify_mac.sh` + `gcmp_b.py` para repetirla en 5 min después de cualquier cambio.
: [Estado de la v6, rev. 1.2: T39, el bloque A por script](hardware/v6-estado-21oct.md#6-t39-22-oct-el-bloque-a-por-script): D34 entre U25 y L22, con el cátodo a 3.52 mm del pin SW y un lazo de 56.5 mm² con C149. In1 e In2 sin pistas bajo la fuente y MH8 libre. DRC: 44 errores (≈ 33 de MH4 y 11 encimes de margen), 0 sin conectar. Falta el push de T39; lote `2026-10-22_t39-v6`.
: [Plan de fines de semana de la v6, rev. 1.4](hardware/v6-plan-fines-de-semana.md#31-fuente-u25-bloque-a): el bloque A ya quedó; el sábado 24 empieza con Rig1, el `/24V` sin montar y el bloque B.
: [Auditoría final de producción de V5.6 y V5.7](hardware/auditoria-final-22oct.md): **GO al piloto de 5 + 5 hoy** con tres condiciones de pedido (vías *Epoxy Filled & Capped* + rayos X en U6, rotación/polaridad en el visor de JLC, D10 → SMBJ33CA); **NO-GO para las 108/322** hasta que el piloto pase el banco con 0.7.0. Paquetes `HBOX_v5_Rev6_PROD` / `HBOX_v5_Rev7_PROD` verificados contra una regeneración con relleno fresco; 30 hallazgos, 16 riesgos, 6 easy wins; lote `2026-10-22_auditoria-final`. [PDF, 33 pp.](hardware/2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_ES_v1.0.pdf) · [presentación, 19 láminas](hardware/2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_Presentacion_ES_v1.0.pdf).

View File

@ -1,9 +1,24 @@
{
"_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. Este archivo es parte del cierre de cada tarea (hbox-docs/CLAUDE.md, «Cierre de una tarea»).",
"_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. Este archivo es parte del cierre de cada tarea (hbox-docs/CLAUDE.md, «Cierre de una tarea»). Semáforo (22-oct): «semaforo» con color rojo/amarillo/verde, titulo, texto, causas (admite **negritas** y [ligas](/ruta/)), destraba y actualizado; cada sitio y la oficina llevan «estado» (rojo/amarillo/verde), «nota» y «meta».",
"actualizado": "2026-10-22",
"fase": {
"nombre": "Pre-producción",
"texto": "V5.6 y V5.7 auditadas: GO al piloto de 5 + 5 (pedido hoy); la v6 en layout"
"nombre": "Pre-producción · modo crisis",
"texto": "semáforo rojo: sin pago este mes y compras detenidas; V5.6 y V5.7 auditadas (GO al piloto de 5 + 5); la v6 en layout"
},
"semaforo": {
"color": "rojo",
"titulo": "Rojo — modo crisis",
"texto": "Sin pago este mes y tres sitios frenados. La meta inmediata es tener 1–2 unidades instaladas por sitio.",
"causas": [
"**Finanzas:** no hemos recibido pago este mes; operamos muy ajustados y sin compras significativas.",
"**TIMSA (Manzanillo):** bloqueado por un trámite administrativo del vehículo. [Pendientes de TIMSA](/tableros/pendientes/?sitio=timsa)",
"**EIT (Ensenada):** esperando el material que se envió desde Manzanillo. [Pendientes de EIT](/tableros/pendientes/?sitio=eit)",
"**ICAVE (Veracruz):** mal clima; ventanas cortas en campo y fallas que hay que investigar ahí. [Pendientes de ICAVE](/tableros/pendientes/?sitio=icave)",
"**Software:** falta el retranslator de Wialon hacia nuestro servidor para comparar en el monitor el RTK contra el GPS legado, y hay desconexiones súbitas que investigar de inmediato."
],
"destraba": "Cuando paguemos a proveedores se destraban la mayoría de las tareas.",
"actualizado": "2026-10-22",
"quien": "Leo"
},
"pedido_nota": "primero los pilotos: 5 V5.6 y 5 V5.7 (pedido del 22-oct, 3 condiciones), luego 20 v6",
"gestion": {
@ -121,8 +136,8 @@
"después del banco"
]
],
"detalle": "Auditoría final del 22-oct (Claude, a petición de Leo): el paquete HBOX_v5_Rev6_PROD es idéntico a una regeneración con relleno fresco (0.000 mm²), DRC solo con los 3 de MOD1, 0 sin conectar, paridad limpia, RS1 Kelvin; BOM y CPL cuadran. GO al piloto de 5 con tres condiciones de pedido: vías Epoxy Filled & Capped + rayos X en U6, rotación/polaridad en el visor de JLC, D10 → SMBJ33CA. NO-GO para las 108 hasta el banco con 0.7.0.",
"siguiente": "Leo: OK a D-22-1 → pedido de 5 + 5 hoy con la lista de §7 (BOM D10, ENIG, vías tapadas, rayos X en U6, visor) → P-1: abrir, B, guardar, commit de kicad/ y production/ → 0.7.0-pre antes de que lleguen (≈ 29-oct) → banco del piloto (8 criterios) → las 108, con SD-15B.",
"detalle": "Auditoría final del 22-oct (Claude, a petición de Leo): el paquete HBOX_v5_Rev6_PROD es idéntico a una regeneración con relleno fresco (0.000 mm²), DRC solo con los 3 de MOD1, 0 sin conectar, paridad limpia, RS1 Kelvin; BOM y CPL cuadran. GO al piloto de 5 con tres condiciones de pedido: vías Epoxy Filled & Capped + rayos X en U6, rotación/polaridad en el visor de JLC, D10 → SMBJ33CA. NO-GO para las 108 hasta el banco con 0.7.0. Re-verificada a las 13:30 sobre los archivos de las 13:00: P-1 cerrado, mismo cobre.",
"siguiente": "Leo: OK a D-22-1 → pedido de 5 + 5 hoy con la lista de §7 (BOM D10, ENIG, vías tapadas, rayos X en U6, visor) → P-1 hecho a las 13:00 (re-verificado): commit de kicad/ y production/ → 0.7.0-pre antes de que lleguen (≈ 29-oct) → banco del piloto (8 criterios) → las 108, con SD-15B.",
"quien": "Leo (D-22-1, pedido, P-1) · Claude (auditoría hecha; 0.7.0 con Leo) · Jesús (banco del piloto) · Aldair (pedido e inventario)",
"docs": [
[
@ -207,8 +222,8 @@
"después del banco"
]
],
"detalle": "Auditoría final del 22-oct: paquete HBOX_v5_Rev7_PROD verificado igual que el de la V5.6. La fuente U25 (TPS54360B, 5.32 V / 600 kHz) cubre 9–32 V sin la SD-15B; C149/C150 se quedan (D-21-2 se sostiene) y el timbre se mide en banco. D10 → SMBJ33CA es obligatorio: con el arnés al revés el SMBJ33A abre F2, que es SMD. GO a los 5 prototipos con las mismas condiciones; NO-GO para las 322 hasta el banco.",
"siguiente": "Mismo pedido que la V5.6 (hoy) → P-1 → prototipos en banco: 9/12/24/32 V con 2.8 A, polaridad invertida, U25 < 110 °C y D34 < 100 °C a 32 V, cámara térmica en las vías de entrada a 9 V, ignición a 12 V por ADC, timbre de SW/VIN (D-21-2) → pedido de 327.",
"detalle": "Auditoría final del 22-oct: paquete HBOX_v5_Rev7_PROD verificado igual que el de la V5.6. La fuente U25 (TPS54360B, 5.32 V / 600 kHz) cubre 9–32 V sin la SD-15B; C149/C150 se quedan (D-21-2 se sostiene) y el timbre se mide en banco. D10 → SMBJ33CA es obligatorio: con el arnés al revés el SMBJ33A abre F2, que es SMD. GO a los 5 prototipos con las mismas condiciones; NO-GO para las 322 hasta el banco. Re-verificada a las 13:30: P-1 cerrado, mismo cobre.",
"siguiente": "Mismo pedido que la V5.6 (hoy) → commit (P-1 ya está en el archivo) → prototipos en banco: 9/12/24/32 V con 2.8 A, polaridad invertida, U25 < 110 °C y D34 < 100 °C a 32 V, cámara térmica en las vías de entrada a 9 V, ignición a 12 V por ADC, timbre de SW/VIN (D-21-2) → pedido de 327.",
"quien": "Leo (pedido) · Claude (auditoría hecha; procedimiento de banco) · Jesús (prototipos en banco)",
"docs": [
[
@ -397,14 +412,15 @@
}
],
"enfoque_inicial": {
"titulo": "Esta semana: pedido del piloto de V5.6 y V5.7 (hoy) y 0.7.0-pre antes de que lleguen; la v6 empieza por D34.",
"titulo": "Modo crisis: destrabar pago, trámites y material para tener 1–2 unidades instaladas por sitio.",
"puntos": [
"Auditoría final (22-oct): GO al piloto de 5 + 5 con tres condiciones de pedido — vías Epoxy Filled & Capped + rayos X en U6, rotación/polaridad en el visor de JLC, D10 → SMBJ33CA. NO-GO para las 108/322 hasta el banco con 0.7.0.",
"Leo: OK a D-22-1, pedido de 5 + 5 (≈ 40 min con la lista de §7), P-1 (abrir, B, guardar, commit) y OK a D-21-2 y D-21-3.",
"Firmware: board_v56.h ya existe; 0.7.0-pre (módem, NTC, LFP, ignición por ADC) en 1 día, antes de que lleguen las tarjetas (≈ 29-oct).",
"v6: T25, T28 y T38 van en el mismo push. Michelle empieza por D34 junto a U25 y por dejar libre MH8."
"**Finanzas:** sin pago recibido este mes, las compras están detenidas; al pagar a proveedores se destraban la mayoría de las tareas.",
"**Sitios:** TIMSA bloqueado por el trámite del vehículo; EIT espera el material de Manzanillo; ICAVE con mal clima y fallas por investigar en campo; LCT por confirmar.",
"**Software:** retranslator de Wialon hacia nuestro servidor (RTK contra GPS legado) y desconexiones súbitas: atención inmediata.",
"**Piloto V5.6 + V5.7:** GO de la auditoría final del 22-oct; 0.7.0-pre antes de que lleguen las tarjetas.",
"**Juntas:** standup y revisión de quick wins grabadas con Gemini; Claude pasa los acuerdos al tablero con un lote ([cómo](/tableros/juntas/))."
],
"nota": "Texto inicial tomado de la [auditoría final del 22-oct](/hardware/auditoria-final-22oct/) y del [estado de la v6](/hardware/v6-estado-21oct/). Leo y Adriana lo cambian con «Editar»."
"nota": "Texto inicial del 22-oct (modo crisis, Leo) y de la [auditoría final](/hardware/auditoria-final-22oct/). Leo y Adriana lo cambian con «Editar»."
},
"sitios": [
{
@ -414,7 +430,9 @@
"x": 68.27,
"y": 73.55,
"etiqueta": "der",
"nota": "se compara con el CalAmp"
"nota": "mal clima: ventanas cortas en campo y fallas por investigar · se compara con el CalAmp",
"estado": "amarillo",
"meta": "1–2 unidades instaladas"
},
{
"id": "lzc",
@ -422,7 +440,10 @@
"ciudad": "Lázaro Cárdenas",
"x": 49.6,
"y": 80.34,
"etiqueta": "der"
"etiqueta": "der",
"estado": "amarillo",
"nota": "estado por confirmar en la junta del 22-oct",
"meta": "1–2 unidades instaladas"
},
{
"id": "eit",
@ -430,7 +451,10 @@
"ciudad": "Ensenada",
"x": 9.34,
"y": 7.92,
"etiqueta": "der"
"etiqueta": "der",
"estado": "rojo",
"nota": "esperando el material que se envió desde Manzanillo",
"meta": "1–2 unidades instaladas"
},
{
"id": "timsa",
@ -438,13 +462,19 @@
"ciudad": "Manzanillo",
"x": 43.06,
"y": 74.67,
"etiqueta": "izq"
"etiqueta": "izq",
"estado": "rojo",
"nota": "bloqueado por un trámite administrativo del vehículo",
"meta": "1–2 unidades instaladas"
}
],
"oficina": {
"nombre": "Helgen",
"ciudad": "Querétaro",
"x": 55.08,
"y": 67.1
"y": 67.1,
"id": "qro",
"estado": "amarillo",
"nota": "sin compras hasta recibir el pago"
}
}
}

View File

@ -10,7 +10,7 @@ Estado de cada versión de la EinarBoard al **22-oct-2026**, con sus documentos
- **V5.6 y V5.7 — paquetes verificados.** `HBOX_v5_Rev6_PROD` y `HBOX_v5_Rev7_PROD` son idénticos a una regeneración con relleno fresco (0.000 mm² de diferencia); DRC solo con los 3 de MOD1, 0 sin conectar, paridad solo PWR_FLAG, RS1 Kelvin en las dos; BOM y CPL cuadran con el PCB. Todos los puertos funcionan en hardware; la V5.7 va de 9 a 32 V sin la SD-15B; el apagón del módem de la V5.5 desapareció por diseño.
- **Condiciones del pedido (no tocan el cobre):** (1) vías *Epoxy Filled & Capped* + rayos X en U6 (3 vías dentro de la pasta del QFN); (2) rotación y polaridad en el visor de JLC (U6, U7, U25, U26, L22, Q7, U16, D34, D10, electrolíticos: huellas easyeda2kicad nunca fabricadas); (3) BOM: D10 SMBJ33A → SMBJ33CA.
- **NO-GO para las 108 / 322** hasta que el piloto pase el banco con el firmware 0.7.0: hoy ningún firmware enciende el módem de estas tarjetas. `board_v56.h` ya existe; 0.7.0-pre (módem, NTC, LFP, ignición por ADC) es 1 día y va antes de que lleguen las tarjetas.
- **P-1, Leo, 5 min:** los `.kicad_pcb` del repo traen el relleno viejo (el Gerber sí lleva el fresco): abrir, B, guardar, commit. Regla D-22-3: todo script T que toque cobre termina con rellenado en el GUI.
- **P-1 cerrado a las 13:00:** Leo rellenó, guardó y regeneró los paquetes; re-verificado a las 13:30 (relleno guardado = fresco, 0.000 mm²; mismo cobre y mismo paquete que en la mañana; BOM/CPL iguales). Falta el commit de `kicad/` y `production/`. Regla D-22-3: todo script T que toque cobre termina con rellenado en el GUI ([re-verificación](auditoria-final-22oct.md#2-que-se-reviso-metodo-ag)).
- **v6:** T39 (22-oct, Claude) hizo el bloque A con la propuesta α. D34 quedó entre U25 y L22, In1/In2 sin pistas bajo la fuente y MH8 libre. DRC: 44 errores (≈ 33 de MH4 y 11 encimes de margen), 0 sin conectar. Falta el push de T39 y MH4 (T40) → [estado de la v6, §6](v6-estado-21oct.md#6-t39-22-oct-el-bloque-a-por-script).
[**▶ Auditoría final (veredicto, hallazgos, riesgos)**](auditoria-final-22oct.md) · [PDF completo, 33 pp.](2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_ES_v1.0.pdf) · [presentación, 19 láminas](2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_Presentacion_ES_v1.0.pdf) · [protocolo](auditoria-final-protocolo.md). Antes: [auditoría del 21-oct (pasadas 4 a 6)](v56-v57-auditoria-21oct.md) · [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.4)](v6-plan-fines-de-semana.md).
@ -18,7 +18,7 @@ Estado de cada versión de la EinarBoard al **22-oct-2026**, con sus documentos
| 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> | **Auditoría final del 22-oct: GO al piloto** con tres condiciones de pedido (vías tapadas + rayos X en U6, visor de JLC, D10 → SMBJ33CA). Paquete `HBOX_v5_Rev6_PROD` verificado = cobre con relleno fresco; DRC y paridad limpios. Pendiente P-1 (relleno en el repo, 5 min) ([auditoría final](auditoria-final-22oct.md)). | 113 (piloto de 5 primero, se pide hoy) | `hbox-watchdog-v5` | 0.7.0: `board_v56.h` listo; 0.7.0-pre por escribir (1 día) |
| <span class="badge-v56">V5.6</span> | **Auditoría final del 22-oct: GO al piloto** con tres condiciones de pedido (vías tapadas + rayos X en U6, visor de JLC, D10 → SMBJ33CA). Paquete `HBOX_v5_Rev6_PROD` verificado = cobre con relleno fresco; DRC y paridad limpios. P-1 cerrado a las 13:00 (relleno fresco en el repo, re-verificado; falta el commit) ([auditoría final](auditoria-final-22oct.md)). | 113 (piloto de 5 primero, se pide hoy) | `hbox-watchdog-v5` | 0.7.0: `board_v56.h` listo; 0.7.0-pre por escribir (1 día) |
| <span class="badge-v57">V5.7</span> | **Auditoría final del 22-oct: GO a los 5 prototipos** con las mismas condiciones. Fuente U25 a 5.32 V / 600 kHz: 9–32 V sin la SD-15B; C149/C150 se quedan (D-21-2) y el timbre se mide en banco; D10 → SMBJ33CA es obligatorio aquí (con el arnés al revés abre F2, que es SMD) ([auditoría final](auditoria-final-22oct.md)). | 327 (5 prototipos primero, se piden hoy) | `hbox-watchdog-v57` (convive con v5) | el mismo 0.7.0 (mismos pines de MCU) |
| <span class="badge-v6">v6</span> | Revisada el 22-oct (`b74b96f` + T39): **NO LISTA**. T28 cerró 2 de los 8 bloqueantes de la [auditoría v6](v6-auditoria-produccion-16oct.md), T25 regresó los barrenos, T38 corrigió courtyards, MH6 y el `/3V3` bajo los barrenos, y **T39 hizo el bloque A** (D34 entre U25 y L22, MH8 libre). Faltan MH4 y los bloques B a H ([estado, §6](v6-estado-21oct.md#6-t39-22-oct-el-bloque-a-por-script)). 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 |

View File

@ -9,7 +9,7 @@ hide:
<!--
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).
- Fase, semáforo, ruta a producción, fechas y sitios (con su color): 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).
@ -47,6 +47,8 @@ hide:
</nav>
</div>
<section class="in-semaforo" id="in-semaforo" aria-labelledby="sem-h" hidden></section>
<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">

View File

@ -259,3 +259,45 @@
@media print {
.hg-inicio .in-atajos, .hg-inicio .in-pend-acc, .hg-inicio .in-editar { display: none !important; }
}
/* ---------- semáforo del proyecto (22-oct) ---------- */
.hg-inicio .in-semaforo { display: flex; gap: 18px; align-items: flex-start; margin-top: 22px; padding: 16px 20px 16px 18px;
border: 1px solid var(--hg-linea); border-radius: 14px; background: var(--hg-plano); }
.hg-inicio .in-semaforo.sem-rojo { background: var(--hg-rojo-bg); border-color: var(--hg-rojo-linea); box-shadow: inset 5px 0 0 var(--hg-rojo); }
.hg-inicio .in-semaforo.sem-amarillo { background: var(--hg-ambar-bg); border-color: color-mix(in srgb, var(--hg-ambar) 45%, transparent); box-shadow: inset 5px 0 0 var(--hg-ambar); }
.hg-inicio .in-semaforo.sem-verde { background: var(--hg-verde-bg); border-color: color-mix(in srgb, var(--hg-verde) 45%, transparent); box-shadow: inset 5px 0 0 var(--hg-verde); }
.hg-inicio .in-sem-luz { flex: none; display: flex; flex-direction: column; gap: 6px; padding: 8px 7px; border-radius: 12px; background: #121826; border: 1px solid #2A3348; margin-left: 4px; }
.hg-inicio .in-sem-luz span { width: 18px; height: 18px; border-radius: 50%; background: #2B3447; }
.hg-inicio .in-sem-luz .l-rojo.on { background: #FF4D4D; box-shadow: 0 0 12px 2px rgba(255, 77, 77, .65); }
.hg-inicio .in-sem-luz .l-amarillo.on { background: #F5C35C; box-shadow: 0 0 12px 2px rgba(245, 195, 92, .55); }
.hg-inicio .in-sem-luz .l-verde.on { background: #45D6A0; box-shadow: 0 0 12px 2px rgba(69, 214, 160, .5); }
.hg-inicio .in-sem-c { min-width: 0; flex: 1; }
.hg-inicio .in-sem-cab { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px; font-size: 13px; font-weight: 600; color: var(--hg-texto-2); }
.md-typeset .hg-inicio .in-semaforo h2 { margin: 4px 0 6px; font-size: 24px; line-height: 1.2; font-weight: 700; color: var(--hg-texto-fuerte); border: 0; padding: 0; }
.md-typeset .hg-inicio .in-semaforo.sem-rojo h2 { color: var(--hg-rojo); }
.md-typeset .hg-inicio .in-semaforo.sem-amarillo h2 { color: var(--hg-ambar); }
.md-typeset .hg-inicio .in-semaforo.sem-verde h2 { color: var(--hg-verde); }
.md-typeset .hg-inicio .in-sem-texto { margin: 0 0 8px; font-size: 15px; color: var(--hg-texto); }
.md-typeset .hg-inicio ul.in-sem-causas { margin: 0 0 8px 1.15em; padding: 0; }
.md-typeset .hg-inicio .in-sem-causas li { margin: 0 0 5px; font-size: 14.5px; line-height: 1.5; color: var(--hg-texto-suave); }
.md-typeset .hg-inicio .in-sem-causas li b, .md-typeset .hg-inicio .in-sem-destraba b { color: var(--hg-texto-fuerte); }
.md-typeset .hg-inicio .in-sem-destraba { margin: 6px 0 0; font-size: 14.5px; color: var(--hg-texto); }
/* sitios con su color */
.hg-inicio .in-pin { text-decoration: none; }
.hg-inicio .in-pin.est-rojo .in-pin-p { background: var(--hg-rojo); box-shadow: 0 0 0 4px color-mix(in srgb, var(--hg-rojo) 28%, transparent); }
.hg-inicio .in-pin.est-amarillo .in-pin-p { background: var(--hg-ambar); box-shadow: 0 0 0 4px color-mix(in srgb, var(--hg-ambar) 26%, transparent); }
.hg-inicio .in-pin.est-verde .in-pin-p { background: var(--hg-verde); box-shadow: 0 0 0 4px color-mix(in srgb, var(--hg-verde) 26%, transparent); }
.hg-inicio .in-pin.oficina.est-amarillo .in-pin-p { background: var(--hg-plano-2); border-color: var(--hg-ambar); }
.hg-inicio .in-pin.oficina.est-rojo .in-pin-p { background: var(--hg-plano-2); border-color: var(--hg-rojo); }
.hg-inicio a.in-pin:hover .in-pin-t { color: var(--hg-liga-hover); }
.hg-inicio .in-sit-luz { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--hg-texto-3); margin-right: 7px; vertical-align: 1px; }
.hg-inicio .in-sitios li.est-rojo .in-sit-luz { background: var(--hg-rojo); }
.hg-inicio .in-sitios li.est-amarillo .in-sit-luz { background: var(--hg-ambar); }
.hg-inicio .in-sitios li.est-verde .in-sit-luz { background: var(--hg-verde); }
.hg-inicio .in-sit-meta { flex-basis: 100%; font-size: 12.5px; color: var(--hg-texto-3); }
@media (max-width: 640px) {
.hg-inicio .in-semaforo { gap: 12px; padding: 14px; }
.hg-inicio .in-sem-luz { padding: 6px 5px; gap: 5px; }
.hg-inicio .in-sem-luz span { width: 14px; height: 14px; }
.md-typeset .hg-inicio .in-semaforo h2 { font-size: 20px; }
}

View File

@ -1,6 +1,6 @@
/* 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);
- docs/datos/proyecto.json: fase, semáforo, ruta a producción, próximas fechas, sitios con su color (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;
@ -56,6 +56,27 @@
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; };
// ---------------------------------------------------------------- semáforo del proyecto (22-oct)
const COLOR = { rojo: 'Rojo', amarillo: 'Amarillo', verde: 'Verde' };
function pintarSemaforo(P) {
const S = P.semaforo, sec = $('in-semaforo');
if (!sec || !S || !COLOR[S.color]) return;
sec.className = 'in-semaforo sem-' + S.color;
sec.innerHTML = '';
const luz = el('div', { class: 'in-sem-luz', role: 'img', 'aria-label': 'Semáforo en ' + COLOR[S.color].toLowerCase() });
['rojo', 'amarillo', 'verde'].forEach(c => luz.append(el('span', { class: 'l-' + c + (c === S.color ? ' on' : '') })));
const c = el('div', { class: 'in-sem-c' });
const cab = el('div', { class: 'in-sem-cab' });
cab.append(el('span', { text: 'Semáforo del proyecto' }));
if (S.actualizado) cab.append(el('span', { class: 'in-gris', text: (S.quien ? S.quien + ', ' : '') + fechaLarga(S.actualizado) }));
c.append(cab, el('h2', { id: 'sem-h', text: S.titulo || COLOR[S.color] }));
if (S.texto) c.append(el('p', { class: 'in-sem-texto', html: enLinea(S.texto) }));
if ((S.causas || []).length) { const ul = el('ul', { class: 'in-sem-causas' }); S.causas.forEach(x => ul.append(el('li', { html: enLinea(x) }))); c.append(ul); }
if (S.destraba) c.append(el('p', { class: 'in-sem-destraba', html: '<b>Qué lo destraba:</b> ' + enLinea(S.destraba) }));
sec.append(luz, c);
sec.hidden = false;
}
// ---------------------------------------------------------------- arriba: fase, pedido, hito, equipo
function pintarFase(P) {
const d = $('in-fase'); d.innerHTML = '';
@ -226,24 +247,40 @@
cont.innerHTML = '';
cont.append(document.importNode(mapa, true));
} catch (err) { cont.classList.add('sin-mapa'); }
// cada sitio con su color (rojo / amarillo / verde) y liga a sus pendientes (22-oct)
const aPend = id => '/tableros/pendientes/?sitio=' + encodeURIComponent(id);
const est = s => COLOR[s.estado] ? ' est-' + s.estado : '';
const pin = (s, oficina) => {
const p = el('span', { class: 'in-pin' + (oficina ? ' oficina' : '') + ' et-' + (s.etiqueta || 'der'), style: 'left:' + s.x + '%;top:' + s.y + '%' });
const p = el(s.id ? 'a' : 'span', { class: 'in-pin' + (oficina ? ' oficina' : '') + est(s) + ' et-' + (s.etiqueta || 'der'), style: 'left:' + s.x + '%;top:' + s.y + '%' });
if (s.id) { p.setAttribute('href', aPend(s.id)); p.setAttribute('title', s.nombre + ' · ' + s.ciudad + (COLOR[s.estado] ? ' — ' + COLOR[s.estado].toLowerCase() + (s.nota ? ': ' + s.nota : '') : '') + '. Ver sus pendientes'); }
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 = '';
const fila = (s, oficina) => {
const li = el('li', { class: est(s).trim() });
const a = el('a', { href: s.id ? aPend(s.id) : '#', title: 'Pendientes de ' + s.nombre });
a.append(el('span', { class: 'in-sit-luz', 'aria-hidden': 'true' }), el('b', { text: oficina ? s.ciudad : s.nombre }), ' ' + (oficina ? 'fabricación e ingeniería' : s.ciudad));
if (COLOR[s.estado]) a.append(el('span', { class: 'vh', text: ' (' + COLOR[s.estado].toLowerCase() + ')' }));
const estado = el('span', { class: 'in-gris', text: s.nota || '' });
li.append(a, estado);
if (s.meta) li.append(el('span', { class: 'in-sit-meta', text: 'Meta: ' + s.meta }));
ul.append(li);
return estado;
};
(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));
const estado = fila(s);
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'];
const partes = [];
if (s.nota) partes.push(s.nota);
partes.push(n ? plural(n, 'unidad con actas', 'unidades con actas') + ', ' + (inst ? plural(inst, 'instalada', 'instaladas') : 'ninguna instalada') : 'sin unidades registradas');
estado.textContent = partes.join(' · ');
}).catch(() => { estado.textContent = s.nota || ''; });
});
if (P.oficina && P.oficina.id) fila(P.oficina, true);
}
// ---------------------------------------------------------------- arranque
@ -254,6 +291,7 @@
try { P = await json('/datos/proyecto.json'); }
catch (err) { falla($('in-ruta'), 'el estado del proyecto', err); return; }
pintarSemaforo(P);
pintarFase(P);
pintarHito(P);
pintarFechas(P, []);

View File

@ -92,6 +92,11 @@
--hg-p-mecanica: #E9B865;
--hg-p-compras: #C6A8FF;
--hg-p-campo: #FF9E70;
--hg-p-gestion: #F08BC6;
/* semáforo del proyecto y de cada sitio (22-oct) */
--hg-rojo: #FF5F5F; --hg-rojo-bg: #3D171D; --hg-rojo-linea: #8F2F38;
--hg-ambar: #F5C35C; --hg-ambar-bg: #3A2F1A;
--hg-verde: #45D6A0; --hg-verde-bg: #11372C;
/* versiones de tarjeta: etiqueta de serigrafía (texto y borde) */
--hg-v55: #4FD8BE; --hg-v55-borde: rgba(79, 216, 190, 0.42);
@ -157,6 +162,10 @@ html[data-tema="claro"],
--hg-p-mecanica: #A26B0A;
--hg-p-compras: #7A4FD0;
--hg-p-campo: #C2531F;
--hg-p-gestion: #B0336F;
--hg-rojo: #C01A1A; --hg-rojo-bg: #FDE7E7; --hg-rojo-linea: #E9A3A3;
--hg-ambar: #A15F00; --hg-ambar-bg: #FFF0CF;
--hg-verde: #12805A; --hg-verde-bg: #E0F4EC;
--hg-v55: #13806F; --hg-v55-borde: rgba(19, 128, 111, 0.45);
--hg-v56: #3552A8; --hg-v56-borde: rgba(53, 82, 168, 0.45);

View File

@ -10,12 +10,13 @@ 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; «Por persona», una fila por responsable |
| 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, Campo y Gestión; **sitio** (ICAVE, TIMSA, EIT, LCT, Querétaro) con filtro y vista «Por sitio»; 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 |
| Qué **material** hay y qué falta | [Inventario](inventario.md) | Existencias, mínimos y cada movimiento con quién y por qué |
| Qué hay **en cada puerto** | [Sitios](sitio.md) | Unidades instaladas, envíos, diagnósticos, daños y contactos de ICAVE, LCT, EIT y TIMSA |
| Qué se acordó en las **juntas** (standup y quick wins) | [Juntas](juntas.md) | La agenda fija, la cadencia y el registro de cada junta con su lote |
| **Quién es quién** | [Equipo](equipo.md) | Miembros, área, acceso al sitio y pendientes abiertos y vencidos de cada quien. El administrador crea aquí los accesos |
| **Todo sobre una serie** | [Expediente de unidad](unidad.md) | Cada acta, foto y log de una unidad en una línea de tiempo, de la tarjeta al sitio |
| Las **visitas de diagnóstico** | [Visitas de campo](campo.md) | Todas las actas de diagnóstico en campo y en banco, con fotos |
@ -28,6 +29,7 @@ Las páginas que abres para saber **dónde está el proyecto hoy**: qué pendien
4. Las cantidades del plan de producción (versión → objetivo → fecha) las edita un administrador en el propio tablero; los conteos reales salen de las actas.
5. Los pendientes sustituyen al *Task Tracker* de Notion, que queda como histórico (sus tareas se importaron una vez).
6. Claude también mueve pendientes: sus cambios llegan en **lotes** que Leo revisa y aplica, y se ven en la Actividad con su nombre y una nota que dice por qué. Toda tarea termina en el tablero, sea de quien sea ([el sitio es el centro de mando](../guia-ingeniero.md#el-sitio-es-el-centro-de-mando)).
7. **Sitio** (22-oct): cada pendiente dice dónde pasa — ICAVE, TIMSA, EIT, LCT o Querétaro (fabricación e ingeniería); puede ser más de uno. En [Pendientes](pendientes.md) filtra con los botones de sitio (la dirección lo guarda: `?sitio=eit`) o abre la vista **Por sitio**: una fila por sitio. Pista **Gestión**: finanzas, trámites y juntas. Las [juntas](juntas.md) (standup y quick wins) terminan en el tablero.
Las direcciones viejas (`tableros/kanban.html`, `unidad.html?serie=…`, `sitios/sitio.html?id=…` y las demás) siguen sirviendo: llevan solas a la página nueva.

View File

@ -37,6 +37,7 @@ 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>22-oct (13:24)</b> — <b>Modo crisis: semáforo rojo</b> (Leo). Sin pago recibido este mes → operación muy ajustada y sin compras significativas; TIMSA bloqueado por un trámite del vehículo; EIT esperando material de Manzanillo; ICAVE con mal clima y fallas por investigar en campo; software: el retranslator de Wialon hacia nuestro servidor (para comparar el RTK contra el GPS legado) y las desconexiones súbitas, que se investigan de inmediato. Meta: <b>1–2 unidades instaladas por sitio</b>. El tablero se separa <b>por sitio</b> (filtro y vista «Por sitio»; pista Gestión) y las <b>juntas</b> (standup y quick wins, grabadas con Gemini) llegan al tablero como lotes. <a href="/tableros/juntas/">Juntas</a> · <a href="/tableros/pendientes/?vista=sitio">Pendientes por sitio</a></li>
<li><b>22-oct (13:30)</b> — <b>v6: T39, el bloque A por script</b> (Leo: «haz tú los cambios de la v6, es la prioridad»). <b>D-21-3 queda tomada:</b> se aplicó la propuesta α.
<ul>
<li>D34 en (181.08, 121.75), con el cátodo a 3.52 mm del pin SW; L22 en (192.18, 122.5); R53/R54 abajo de JP2.</li>
@ -45,6 +46,7 @@ hide:
</ul>
<b>Regla nueva:</b> en la v6 no se agregan zonas de F.Cu con prioridad sobre la tierra sin revisar el DRC de toda la tarjeta. KiCad 10 cambia el relleno de toda la tierra, y con T39 salían 2 alivios térmicos rotos lejos del bloque.
Queda por revisar la pared de la HBOX junto a L22: el cobre está a 0.53 mm de la orilla. Falta el push de Leo. <a href="/hardware/v6-estado-21oct/#6-t39-22-oct-el-bloque-a-por-script">T39</a></li>
<li><b>22-oct (13:30)</b> — <b>P-1 cerrado; veredicto confirmado sobre los archivos de las 13:00.</b> Leo rellenó las zonas, guardó los dos tableros y regeneró los paquetes (V5.6 <code>338ee14b</code>, V5.7 <code>85baca51</code>). Re-verificación con el mismo método A: relleno guardado = relleno fresco (0.000 mm² en las dos), paquete = archivo, cobre y 11 capas + taladros idénticos a los auditados en la mañana, BOM y CPL con el mismo SHA (D10 sigue como SMBJ33A: condición 3 viva), DRC igual. Falta el commit de <code>kicad/</code> y <code>production/</code>. Herramienta: <code>Auditoria-Final-V56-V57/scripts/verify_mac.sh</code> + <code>gcmp_b.py</code>, 5 min después de cualquier cambio (los quick wins van en una sesión paralela). <a href="/hardware/auditoria-final-22oct/#2-que-se-reviso-metodo-ag">Re-verificación</a></li>
<li><b>22-oct (11:20)</b> — <b>Auditoría final de producción de V5.6 y V5.7</b> (paquetes <code>HBOX_v5_Rev6_PROD</code> / <code>HBOX_v5_Rev7_PROD</code>, hecha por Claude a petición de Leo): <b>GO al piloto de 5 + 5 hoy</b>, con tres condiciones de pedido que no tocan el cobre — <b>D-22-1</b> (decide Leo): vías <i>Epoxy Filled &amp; Capped</i> + rayos X en U6; rotación y polaridad en el visor de JLC (U6, U7, U25, U26, L22, Q7, U16, D34, D10, electrolíticos); <b>D-22-2:</b> D10 SMBJ33A → SMBJ33CA en el BOM (con el arnés al revés el unidireccional abre F2). <b>NO-GO para las 108 / 322</b> hasta que el piloto pase el banco con 0.7.0: hoy ningún firmware enciende el módem de estas tarjetas (<code>board_v56.h</code> ya existe; 0.7.0-pre es 1 día). Los Gerber son idénticos a una regeneración con relleno fresco (0.000 mm²), pero los <code>.kicad_pcb</code> del repo traen el relleno viejo → <b>D-22-3, regla:</b> todo script T que toque cobre termina con rellenado en el GUI antes del commit (P-1, Leo, 5 min). D-21-2 se sostiene (E-9): C149/C150 se quedan y el timbre se mide en banco. Para las 108/322: THVD1410DR y TRS3232E por SP3485 y MAX3232 (P-5). <a href="/hardware/auditoria-final-22oct/">Auditoría final</a> · <a href="/hardware/2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_ES_v1.0.pdf">PDF</a> · <a href="/hardware/auditoria-final-protocolo/">Protocolo</a></li>
<li><b>21-oct (20:45)</b> — <b>v6: T38 aplicado por Claude.</b> Courtyards reales en las 11 huellas de easyeda2kicad: el DRC no veía encimes en ellas, y salió uno real, C153 debajo de L22. MH6 libre. Regla nueva de barrenos: el plano <code>/3V3</code> de B.Cu quedaba desnudo bajo los separadores de los 5 barrenos; con insertos o separadores metálicos, el tornillo lo une a GND. <b>D-21-3, propuesta (falta el OK de Leo):</b> el bloque A se hace con D34 entre U25 y L22, y L22 al este (propuesta α). Metas corregidas: cátodo ≤ 3.5 mm, L22.1 ≤ 9 mm, nodo SW ≤ 10 mm² fuera de los pads, lazo ≤ 60 mm². En V5.6 y V5.7 salen 2 y 4 avisos de biblioteca: es solo el courtyard, no actualicen esas huellas antes del Gerber. <a href="/hardware/v6-estado-21oct/#5-t38-21-oct-noche-courtyards-mh6-y-regla-de-barrenos">T38</a> · <a href="/hardware/v6-estado-21oct/#31-bloque-a-propuesta-corrige-la-rev-10">Propuesta α</a></li>
<li><b>21-oct (20:00)</b> — <b>El sitio es el centro de mando</b> (regla de Leo): toda tarea del proyecto termina aquí, con sus pendientes movidos, sus documentos al día y, si algo del sitio estorbó, una mejora. Los cambios de Claude al tablero llegan en <b>lotes</b> que Leo revisa y aplica (<code>./scripts/tablero.sh</code>) y se ven en la Actividad como cualquier otro cambio. <a href="/guia-ingeniero/#el-sitio-es-el-centro-de-mando">Cómo se cierra una tarea</a></li>

61
docs/tableros/juntas.md Normal file
View File

@ -0,0 +1,61 @@
---
title: "Juntas"
description: "Standup y revisión de quick wins: cómo se hacen, qué se revisa y cómo llegan los acuerdos al tablero."
date: 2026-10-22
---
# Juntas: standup y quick wins
Las juntas del proyecto existen para una sola cosa: que **el tablero diga la verdad**. Lo que se acuerda en una junta
no queda en el chat ni en la memoria de alguien: termina como pendientes, con responsable y fecha, en el
[tablero](pendientes.md). Regla del sitio: **si no está en el sitio, no pasó**.
!!! warning "22-oct: modo crisis, semáforo en rojo"
Sin pago recibido este mes (compras detenidas), TIMSA bloqueado por un trámite del vehículo, EIT esperando material de
Manzanillo e ICAVE con mal clima. Mientras dure, cada junta empieza por lo que destraba: dinero, trámites y material.
El semáforo y sus causas están en la [página de inicio](../index.md).
## Cómo funciona
1. **Se graba.** Leo graba la junta con Gemini (notas y transcripción).
2. **Claude la lee.** Leo le pasa la transcripción o las notas a Claude.
3. **Claude escribe un lote.** Un archivo con los cambios al tablero: pendientes nuevos, los que se cierran, los que se
mueven, cada uno con su sitio, responsable, fecha y una nota que dice qué se acordó.
4. **Leo lo revisa y lo aplica.** `./scripts/tablero.sh revisar` muestra qué va a cambiar; `aplicar` lo hace.
5. **Queda en la Actividad.** Todos ven en el inicio y en la [Bitácora](../bitacora.md) qué cambió y por qué.
Si en la junta alguien mueve un pendiente a mano, mejor: el lote no lo vuelve a tocar.
## Agenda fija
| # | Tema | Pregunta | Dónde se ve |
|---|---|---|---|
| 1 | **Semáforo** | ¿Sigue en rojo? ¿Qué lo cambia? | Inicio → semáforo |
| 2 | **Por sitio** | ICAVE · TIMSA · EIT · LCT · Querétaro: ¿qué avanzó, qué está bloqueado? Meta: 1–2 unidades instaladas por sitio | [Pendientes](pendientes.md) → filtro por sitio o vista **Por sitio** |
| 3 | **Bloqueos** | ¿Quién destraba cada bloqueado y para cuándo? | Columna **Bloqueado** |
| 4 | **Quick wins** | ¿Qué se puede cerrar esta semana sin dinero ni material nuevo? | Etiqueta `quick-win` |
| 5 | **Acuerdos** | Cada acuerdo con responsable y fecha | El lote de la junta |
## Etiquetas y pistas
- **`crisis`**: lo que hay que destrabar para salir del rojo.
- **`quick-win`**: se puede cerrar en días, sin compras.
- Pista **Gestión**: finanzas, trámites y juntas (desde el 22-oct).
- Cada pendiente lleva su **sitio** (uno o varios): ICAVE, TIMSA, EIT, LCT o Querétaro (fabricación e ingeniería).
Así cada equipo filtra solo lo suyo: [ICAVE](pendientes.md?sitio=icave) · [TIMSA](pendientes.md?sitio=timsa) ·
[EIT](pendientes.md?sitio=eit) · [LCT](pendientes.md?sitio=lzc) · [Querétaro](pendientes.md?sitio=qro).
## Cadencia
*Propuesta; se acuerda en la junta del 22-oct.*
| Junta | Cuándo | Duración | Quién |
|---|---|---|---|
| **Standup** | 2 o 3 veces por semana | 15 min | todo el equipo; cada sitio dice qué hizo, qué sigue y qué lo frena |
| **Revisión de quick wins y semáforo** | una vez por semana | 30–45 min | Leo, Adriana y responsables de sitio |
## Registro
| Fecha | Junta | Lote | Acuerdos |
|---|---|---|---|
| 22-oct | Arranque del modo crisis | `2026-10-22_crisis` | Semáforo rojo; tablero por sitio; meta 1–2 unidades por sitio |

View File

@ -9,7 +9,7 @@
botonNuevoEn: elemento, // opcional: dónde va el botón «Nuevo pendiente» (p. ej. el encabezado)
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)
vistas: true, // botones «Tablero · Por persona · Por sitio» (?vista=persona|sitio 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í)
@ -28,9 +28,17 @@
Estilos: tableros/tablero.css (todo bajo .tb). Sin emoji: íconos de línea (Kanban.icono) y
avatares con iniciales (Kanban.avatar), con el mismo color por persona en todo el sitio. */
(function () {
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo' };
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo', gestion: 'Gestión' };
const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']];
const VERSIONES = ['—', 'V5.5', 'V5.6', 'V5.7', 'v6'];
// Sitio de cada pendiente (22-oct): [id, corto, chip del filtro, nombre]. Sin sitio = Querétaro.
// El backend manda su lista en GET /api/tareas (sitios); esta es la de respaldo.
let SITIOS = [['icave', 'ICAVE', 'ICAVE', 'ICAVE · Veracruz'], ['timsa', 'TIMSA', 'TIMSA', 'TIMSA · Manzanillo'], ['eit', 'EIT', 'EIT', 'EIT · Ensenada'],
['lzc', 'LCT', 'LCT', 'LCT · Lázaro Cárdenas'], ['qro', 'QRO', 'Querétaro', 'Querétaro · fabricación e ingeniería']];
const sitiosDe = t => (t && t.sitios && t.sitios.length) ? t.sitios : ['qro'];
const SIT = id => SITIOS.find(x => x[0] === id) || [id, id, id, id];
// en la dirección también se aceptan los nombres de siempre (?sitio=lct, ?sitio=ensenada)
const ALIAS_SITIO = { lct: 'lzc', veracruz: 'icave', manzanillo: 'timsa', ensenada: 'eit', queretaro: 'qro', 'querétaro': 'qro', oficina: 'qro' };
// Lista de respaldo si /api/equipo no responde; lo normal es que el equipo venga del servidor.
const RESPONSABLES = ['leo', 'adriana', 'michelle', 'jesus', 'alan', 'daniel', 'aldair', 'gibran', 'claude'];
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
@ -61,7 +69,8 @@
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',
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'
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',
sitio: 'M12 20.5s-6.5-5.4-6.5-10.3a6.5 6.5 0 0 1 13 0c0 4.9-6.5 10.3-6.5 10.3zM12 7.7a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8'
};
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).
@ -157,9 +166,10 @@
opts = opts || {};
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 filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1',
sitios: new Set((q0.get('sitio') || '').split(',').map(x => x.trim().toLowerCase()).filter(Boolean).map(x => ALIAS_SITIO[x] || x)) };
const vistasOn = !!opts.vistas;
let vista = vistasOn && (q0.get('vista') === 'persona' || (!q0.get('vista') && opts.vista === 'persona')) ? 'persona' : 'tablero';
let vista = !vistasOn ? 'tablero' : (['persona', 'sitio'].includes(q0.get('vista')) ? q0.get('vista') : (!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;
@ -168,16 +178,17 @@
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);
history.replaceState(null, '', u.pathname + u.search.replace(/%2C/gi, ',') + u.hash); // ?sitio=eit,timsa legible
}
// 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 (vista !== 'tablero') u.set('vista', vista);
if (filtro.sitios.size) u.set('sitio', [...filtro.sitios].join(','));
if (opts.pista) u.set('pista', opts.pista);
Object.entries(extra || {}).forEach(([k, v]) => v && u.set(k, v));
const q = u.toString();
const q = u.toString().replace(/%2C/gi, ',');
return (opts.enlaceMas || '/tableros/pendientes/') + (q ? '?' + q : '');
}
@ -194,16 +205,30 @@
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' });
const selVista = el('div', { class: 'kb-vistas', role: 'group', 'aria-label': 'Vista' });
[['tablero', 'Tablero', 'columnas'], ['persona', 'Por persona', 'equipo']].forEach(([k, n, ic]) => {
[['tablero', 'Tablero', 'columnas'], ['persona', 'Por persona', 'equipo'], ['sitio', 'Por sitio', 'sitio']].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(); });
b.addEventListener('click', () => { if (vista === k) return; vista = k; ponURL('vista', k === 'tablero' ? null : k); pintar(); });
selVista.append(b);
});
// filtro por sitio (22-oct): varios a la vez; viaja en la dirección (?sitio=eit,timsa)
const grpSitios = el('div', { class: 'kb-sitios', role: 'group', 'aria-label': 'Sitio' });
function pintarChipsSitio() {
grpSitios.innerHTML = '';
grpSitios.append(el('span', { class: 'kb-sitios-i', title: 'Sitio', html: icono('sitio') }));
const todos = el('button', { type: 'button', 'aria-pressed': String(!filtro.sitios.size), text: 'Todos', title: 'Todos los sitios' });
todos.addEventListener('click', () => { filtro.sitios.clear(); ponURL('sitio', null); pintar(); });
grpSitios.append(todos);
SITIOS.forEach(([id, , chip, nombre]) => {
const b = el('button', { type: 'button', 'data-sitio': id, title: nombre, 'aria-pressed': String(filtro.sitios.has(id)), text: chip });
b.addEventListener('click', () => { if (filtro.sitios.has(id)) filtro.sitios.delete(id); else filtro.sitios.add(id); ponURL('sitio', filtro.sitios.size ? [...filtro.sitios].join(',') : null); pintar(); });
grpSitios.append(b);
});
}
const modo = opts.filtros === undefined ? true : opts.filtros;
if (modo) {
if (vistasOn) barra.append(selVista);
barra.append(btnMios);
if (modo !== 'min') { if (!opts.pista) barra.append(selPista); barra.append(selResp, selVer, selHechas, buscar); }
if (modo !== 'min') { barra.append(grpSitios); if (!opts.pista) barra.append(selPista); barra.append(selResp, selVer, selHechas, buscar); }
if (opts.botonNuevoEn) opts.botonNuevoEn.append(btnNuevo); else barra.append(btnNuevo);
if (opts.barraEn) {
// la página ya puso ahí sus propios elementos (p. ej. una liga): las vistas y «Solo los míos»
@ -259,6 +284,7 @@
else if (!String(t.responsable || '').toLowerCase().includes(f)) return false;
}
if (filtro.version && t.version_tarjeta !== filtro.version) return false;
if (filtro.sitios.size && !sitiosDe(t).some(x => filtro.sitios.has(x))) return false;
if (t.estado === 'hecho') {
if (filtro.hechas === 'ninguna') return false;
if (filtro.hechas === 'recientes' && (t.modificado_en || '').slice(0, 10) < corte) return false;
@ -278,7 +304,10 @@
const sup = el('div', { class: 'sup' });
if (!opts.pista) sup.append(el('span', { class: 'pista-p' }), el('span', { text: PISTAS[t.pista] || t.pista }));
if (t.version_tarjeta && t.version_tarjeta !== '—') sup.append(el('span', { class: (BADGE[t.version_tarjeta] || 'chip') + ' ver-t', text: t.version_tarjeta }));
if (sup.childNodes.length) c.append(sup);
const sb = el('span', { class: 'sits' });
sitiosDe(t).forEach(id => sb.append(el('span', { class: 'sit-b s-' + id, title: SIT(id)[3], text: SIT(id)[1] })));
sup.append(sb);
c.append(sup);
// título (abre el detalle)
const tit = el('div', { class: 'tit', text: t.titulo, role: 'button', tabindex: '0' });
tit.addEventListener('click', () => abrir(t));
@ -315,9 +344,11 @@
porId = {}; tareas.forEach(t => porId[t.id] = t);
const vis = visibles();
selVista.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vista === vista)));
kb.hidden = vista === 'persona';
per.hidden = vista !== 'persona';
pintarChipsSitio();
kb.hidden = vista !== 'tablero';
per.hidden = vista === 'tablero';
if (vista === 'persona') pintarPersonas(vis);
else if (vista === 'sitio') pintarSitios(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));
@ -344,7 +375,7 @@
}
// ---------- 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 filtrosActivos() { return !!((filtro.pista && !opts.pista) || filtro.responsable || filtro.version || filtro.buscar.trim() || filtro.mios || filtro.sitios.size); }
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 }));
@ -422,10 +453,49 @@
}
}
// ---------- vista por sitio (22-oct): una fila por sitio, una columna por estado ----------
function pintarSitios(vis) {
per.innerHTML = '';
const cab = el('div', { class: 'kb-p-fila kb-p-cab', 'aria-hidden': 'true' });
cab.append(el('span', { text: 'Sitio' }));
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);
const N = opts.limitePersona || 3;
const lista = filtro.sitios.size ? SITIOS.filter(x => filtro.sitios.has(x[0])) : SITIOS;
lista.forEach(([id, corto, , nombre]) => {
const ts = vis.filter(t => sitiosDe(t).includes(id));
const fila = el('div', { class: 'kb-p-fila kb-s-fila s-' + id });
const quien = el('div', { class: 'kb-p-quien' });
quien.append(el('span', { class: 'kb-s-pin', html: icono('sitio') }));
const ab = ts.filter(t => t.estado !== 'hecho').length, bl = ts.filter(t => t.estado === 'bloqueado').length, ve = ts.filter(t => t.vencida).length;
const txt = el('span', { class: 'kb-p-nom' });
txt.append(el('b', { text: nombre.split(' · ')[0] }));
if (nombre.includes(' · ')) txt.append(el('span', { class: 'kb-p-rol', text: nombre.split(' · ').slice(1).join(' · ') }));
txt.append(el('span', { class: 'kb-p-carga' + (bl ? ' mal' : (ve ? ' aviso' : '')), text: ab ? ab + (ab === 1 ? ' abierto' : ' abiertos') + (bl ? ', ' + bl + (bl === 1 ? ' bloqueado' : ' bloqueados') : '') + (ve ? ', ' + ve + (ve === 1 ? ' vencido' : ' vencidos') : '') : 'Nada abierto' }));
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 = ts.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 = 'sitio:' + id + '|' + 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({ sitio: id }), 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);
});
}
async function recargar() {
try {
const j = await api('/api/tareas');
tareas = j.tareas || []; admin = !!j.admin; user = j.user || '';
if (Array.isArray(j.sitios) && j.sitios.length) SITIOS = j.sitios.map(x => [x.id, x.corto || x.id, x.id === 'qro' ? 'Querétaro' : (x.corto || x.id), x.nombre || x.id]);
usarEquipo(j.equipo);
pintar();
if (opts.onCargado) opts.onCargado(tareas, { admin, user });
@ -456,7 +526,8 @@
function campo(lbl, input) { const d = el('div'); d.append(el('label', { text: lbl }), input); return d; }
function abrir(t, estadoInicial) {
const nuevo = !t;
t = t || { titulo: '', descripcion: '', pista: opts.pista || 'produccion', estado: estadoInicial || 'por-hacer', responsable: '', fecha_limite: null, enlaces: [], bloqueada_por: [], etiquetas: [], version_tarjeta: '—' };
t = t || { titulo: '', descripcion: '', pista: opts.pista || 'produccion', estado: estadoInicial || 'por-hacer', responsable: '', fecha_limite: null, enlaces: [], bloqueada_por: [], etiquetas: [], version_tarjeta: '—',
sitios: filtro.sitios.size ? [...filtro.sitios] : ['qro'] };
dlg.innerHTML = '';
const cab = el('div', { class: 'dcab' });
cab.append(el('h2', { text: nuevo ? 'Nuevo pendiente' : 'Pendiente' }));
@ -482,10 +553,20 @@
const leerResp = () => [...fResp.querySelectorAll('input:checked')].map(c => c.value).concat(fLibre.value.split(',').map(x => x.trim()).filter(Boolean)).join(', ');
const fFecha = el('input', { type: 'date', value: t.fecha_limite || '' });
const fVer = el('select'); VERSIONES.forEach(v => fVer.append(el('option', { value: v, text: v === '—' ? 'No aplica' : v }))); fVer.value = t.version_tarjeta || '—';
const fEtq = el('input', { type: 'text', value: (t.etiquetas || []).join(', '), placeholder: 'layout, pedido 570, bloqueante' });
const fEtq = el('input', { type: 'text', value: (t.etiquetas || []).join(', '), placeholder: 'layout, crisis, quick-win' });
// sitio: dónde pasa el trabajo (uno o varios); nuevo = el del filtro o Querétaro
const ySit = sitiosDe(t);
const fSit = el('div', { class: 'chips' });
SITIOS.forEach(([id, , , nombre]) => {
const l = el('label', { class: 'chip-sel' + (ySit.includes(id) ? ' on' : '') });
const ck = el('input', { type: 'checkbox', value: id }); ck.checked = ySit.includes(id);
ck.addEventListener('change', () => l.classList.toggle('on', ck.checked));
l.append(ck, el('span', { class: 'kb-s-pin', html: icono('sitio') }), document.createTextNode(nombre)); fSit.append(l);
});
cuerpo.append(campo('Título', fTit), campo('Descripción', fDesc));
const g1 = el('div', { class: 'dos-col' }); g1.append(campo('Pista', fPista), campo('Estado', fEst), campo('Fecha límite', fFecha), campo('Versión de tarjeta', fVer), campo('Etiquetas (separadas por coma)', fEtq)); cuerpo.append(g1);
cuerpo.append(campo('Sitio (dónde pasa; puede ser más de uno)', fSit));
const cResp = campo('Responsable (obligatorio; puede ser más de uno)', fResp); cResp.append(fLibre); cuerpo.append(cResp);
// enlaces
@ -532,15 +613,20 @@
const d = { titulo: fTit.value.trim(), descripcion: fDesc.value.trim(), pista: fPista.value, estado: fEst.value, responsable: leerResp(),
fecha_limite: fFecha.value || null, version_tarjeta: fVer.value, etiquetas: fEtq.value.split(',').map(s => s.trim()).filter(Boolean),
enlaces: [...enlBox.querySelectorAll('.fila')].map(f => ({ titulo: f.children[0].value.trim(), url: f.children[1].value.trim() })).filter(e => e.url),
bloqueada_por: [...lst.querySelectorAll('input:checked')].map(c => c.value) };
bloqueada_por: [...lst.querySelectorAll('input:checked')].map(c => c.value),
sitios: [...fSit.querySelectorAll('input:checked')].map(c => c.value) };
if (!d.titulo) { m.className = 'msg err'; m.textContent = 'Escribe el título.'; return; }
if (!d.sitios.length) { m.className = 'msg err'; m.textContent = 'Elige al menos un sitio (Querétaro si es de fabricación o ingeniería).'; fSit.scrollIntoView({ block: 'center' }); return; }
if (!d.responsable) { m.className = 'msg err'; m.textContent = 'Elige al menos un responsable del equipo (o escribe uno en texto libre).'; fResp.scrollIntoView({ block: 'center' }); return; }
guardar.disabled = true;
try {
let r;
if (nuevo) r = await api('/api/tareas', json('POST', d));
else {
const cambios = {}; Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(t[k] == null ? (Array.isArray(d[k]) ? [] : (k === 'fecha_limite' ? null : '')) : t[k])) cambios[k] = d[k]; });
const cambios = {}; Object.keys(d).forEach(k => {
if (k === 'sitios') { if ([...d.sitios].sort().join() !== [...sitiosDe(t)].sort().join()) cambios.sitios = d.sitios; return; }
if (JSON.stringify(d[k]) !== JSON.stringify(t[k] == null ? (Array.isArray(d[k]) ? [] : (k === 'fecha_limite' ? null : '')) : t[k])) cambios[k] = d[k];
});
if (Object.keys(cambios).length) r = await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', cambios));
}
dlg.close(); mensaje((nuevo ? 'Pendiente creado.' : 'Cambios guardados.') + (r && r.aviso ? ' Aviso: ' + r.aviso : ''), !(r && r.aviso)); recargar();
@ -569,5 +655,5 @@
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, nombreCorto, avatar, avatares, icono, fechaDe, fechaCorta, get equipo() { return EQUIPO; } };
window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, get SITIOS() { return SITIOS; }, sitiosDe, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, nombreCorto, avatar, avatares, icono, fechaDe, fechaCorta, get equipo() { return EQUIPO; } };
})();

View File

@ -8,7 +8,7 @@ hide:
<div class="tb tb-pendientes">
<header class="tb-cab">
<div class="tb-cab__t"><h1>Pendientes</h1><span class="meta" id="meta">Ingeniería, producción, mecánica, compras y campo</span></div>
<div class="tb-cab__t"><h1>Pendientes</h1><span class="meta" id="meta">Ingeniería, producción, mecánica, compras, campo y gestión · por sitio</span></div>
<div class="tb-cab__acc"><span id="acciones-kb"></span></div>
</header>
@ -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>Sitio</b>: cada pendiente dice dónde pasa (ICAVE, TIMSA, EIT, LCT o Querétaro). Los botones de sitio dejan solo los de tu sitio (puedes elegir varios); <b>Por sitio</b> muestra una fila por sitio. Al crear un pendiente con un sitio filtrado, ese sitio ya viene marcado. La dirección (<code>?sitio=eit</code>) se puede compartir.</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>

View File

@ -312,6 +312,7 @@
.tb .p-mecanica .pista-p, .tb .pista-p.p-mecanica { background: var(--hg-p-mecanica); }
.tb .p-compras .pista-p, .tb .pista-p.p-compras { background: var(--hg-p-compras); }
.tb .p-campo .pista-p, .tb .pista-p.p-campo { background: var(--hg-p-campo); }
.tb .p-gestion .pista-p, .tb .pista-p.p-gestion { background: var(--hg-p-gestion); }
.tb .card .tit { font-weight: 600; font-size: 14.5px; line-height: 1.36; color: var(--hg-texto); cursor: pointer; overflow-wrap: anywhere; }
.tb .card .tit:hover { color: var(--hg-liga-hover); }
.tb .card .tit a { color: inherit; text-decoration: none; }
@ -323,13 +324,14 @@
.tb .card .enl a span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb .card .etq { display: flex; flex-wrap: wrap; gap: 4px; }
.tb .chip { display: inline-block; font-size: 11.5px; font-weight: 500; padding: 1px 7px; border-radius: 5px; background: var(--hg-pend-bg); color: var(--hg-texto-2); white-space: nowrap; }
.tb .chip.p-ingenieria, .tb .chip.p-produccion, .tb .chip.p-mecanica, .tb .chip.p-compras, .tb .chip.p-campo { background: none; padding: 0; color: var(--hg-texto-2); display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; }
.tb .chip.p-ingenieria::before, .tb .chip.p-produccion::before, .tb .chip.p-mecanica::before, .tb .chip.p-compras::before, .tb .chip.p-campo::before { content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--hg-texto-3); }
.tb .chip.p-ingenieria, .tb .chip.p-produccion, .tb .chip.p-mecanica, .tb .chip.p-compras, .tb .chip.p-campo, .tb .chip.p-gestion { background: none; padding: 0; color: var(--hg-texto-2); display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; }
.tb .chip.p-ingenieria::before, .tb .chip.p-produccion::before, .tb .chip.p-mecanica::before, .tb .chip.p-compras::before, .tb .chip.p-campo::before, .tb .chip.p-gestion::before { content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--hg-texto-3); }
.tb .chip.p-ingenieria::before { background: var(--hg-p-ingenieria); }
.tb .chip.p-produccion::before { background: var(--hg-p-produccion); }
.tb .chip.p-mecanica::before { background: var(--hg-p-mecanica); }
.tb .chip.p-compras::before { background: var(--hg-p-compras); }
.tb .chip.p-campo::before { background: var(--hg-p-campo); }
.tb .chip.p-gestion::before { background: var(--hg-p-gestion); }
.tb .card .pie { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.tb .card .fecha { font-size: 12.5px; color: var(--hg-texto-2); white-space: nowrap; }
.tb .card .fecha.hoy { color: var(--hg-acento); font-weight: 600; }
@ -670,3 +672,21 @@
.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; }
}
/* ---------- sitio de cada pendiente (22-oct) ---------- */
.tb .kb-sitios { display: inline-flex; align-items: center; gap: 2px; padding: 3px; border: 1px solid var(--hg-linea-2); border-radius: 10px; background: var(--hg-plano-2); flex-wrap: wrap; }
.tb .kb-sitios-i { display: inline-flex; padding: 0 4px 0 5px; color: var(--hg-texto-3); }
.tb .kb-sitios-i .i { width: 15px; height: 15px; }
.tb .kb-sitios button { background: transparent; border: 0; color: var(--hg-texto-2); font-size: 13px; font-weight: 500; padding: 4px 9px; border-radius: 7px; }
.tb .kb-sitios button[aria-pressed="true"] { background: var(--hg-elevado); color: var(--hg-texto-fuerte); font-weight: 600; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--hg-curso) 45%, transparent); }
.tb .kb-sitios button:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
.tb .card .sup .sits { display: inline-flex; gap: 3px; flex-wrap: wrap; }
.tb .card .sup .ver-t + .sits { margin-left: 4px; }
.tb .card .sup .sits:first-child, .tb .card .sup > span:not(.ver-t) + .sits { margin-left: auto; }
.tb .sit-b { display: inline-block; font-size: 10.5px; font-weight: 600; letter-spacing: .03em; line-height: 1.5; padding: 0 5px; border-radius: 4px;
border: 1px solid var(--hg-linea-2); color: var(--hg-texto-2); background: transparent; white-space: nowrap; }
.tb .sit-b:not(.s-qro) { border-color: color-mix(in srgb, var(--hg-p-campo) 55%, transparent); color: var(--hg-texto-fuerte); }
.tb .kb-s-pin { display: inline-flex; color: var(--hg-texto-3); }
.tb .kb-s-pin .i { width: 16px; height: 16px; }
.tb .kb-p-quien .kb-s-pin { width: 30px; height: 30px; align-items: center; justify-content: center; border-radius: 50%; background: var(--hg-plano-2); flex: none; }
.tb .chip-sel .kb-s-pin .i { width: 13px; height: 13px; }

View File

@ -10,6 +10,7 @@ ICONOS = {
"index.md": "helgen/inicio",
"tableros/pendientes.md": "helgen/pendientes",
"bitacora.md": "helgen/bitacora",
"tableros/juntas.md": "helgen/fecha",
# Tableros
"tableros/index.md": "helgen/pendientes",
"tableros/ingenieria.md": "helgen/ingenieria",

View File

@ -84,6 +84,7 @@ markdown_extensions:
nav:
- Inicio: index.md
- Pendientes: tableros/pendientes.md
- Juntas: tableros/juntas.md
- Bitácora: bitacora.md
- Tableros: