Instalaciones por sitio, acta Prueba en sitio y lote 2026-10-22_instalaciones

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
This commit is contained in:
Leopoldo Rodríguez 2026-10-22 15:58:55 -06:00
parent 0e828510a9
commit 7cbf9556c3
30 changed files with 1412 additions and 68 deletions

View File

@ -28,6 +28,7 @@ Adriana, Michelle, Jesús and the field technicians work from it; Claude closes
| **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` |
| **Instalaciones** (per-site installation board, 22-oct) | `docs/tableros/instalaciones.md` + `.js`/`.css`; data: field actas via `GET /api/instalaciones` (`backend/instalaciones.py`, hooked at the end of `app.py`) | automatic | stages envío → recepción → instalación → **prueba en sitio** (acta `prueba-sitio`; only a passed test counts for the 1–2 per-site goal) → en operación (7 days after the test, no failure) + «con falla»; colour, nota and meta per site from `proyecto.json`; forms accept `?serie=&sitio=&<clave>=` prefill (`acta.js`) |
| **Ú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 |
| **Bitácora → Documentos por fecha** | `docs/bitacora.md` | Claude or Leo | newest first under the month: `**DD oct**` + `: [Title](path.md#anchor): what it decides or changes` |
| **Documentos recientes** (home) | automatic from `date:` front matter (`hooks/documentos.py`) | — | give every new page a `date:`; pages under «Historia» are left out |

View File

@ -353,6 +353,36 @@ curl -s -u leo:PASS 'https://drogheda.helgen.online/api/tareas?sitio=eit' | head
| `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 |
## A7 · 22-oct: tablero de Instalaciones por sitio y prueba en sitio
Qué cambia: página nueva **Instalaciones** (`/tableros/instalaciones/`, pestañas ICAVE · TIMSA · EIT · LCT): una tarjeta por
unidad con su vehículo, sus actas y su etapa (enviada → recibida → instalada → probada en sitio → en operación, más «con
falla») contra la meta de cada sitio. Acta nueva **Prueba en sitio** (`formularios/prueba-sitio.html`, tipo `prueba-sitio`).
El acta de instalación pide además 12/24 V, los tipos Reach Stacker y Empty Handler y el nombre en el monitor (H00xx). Los
formularios aceptan `?serie=…&sitio=…` para abrir la siguiente acta ya llena (`acta.js`).
Backend: `backend/instalaciones.py` (nuevo; lo engancha el final de `app.py`) da `GET /api/instalaciones[?sitio=eit]` y vuelve
repetible `prueba-sitio` **sin tocar** `ACTAS_REPETIBLES` del servicio. `deploy.sh` ya copia el archivo nuevo.
**Cambió `backend/app.py`: reinicia el servicio.** Sin reiniciar, la página dice «falta reiniciar el servicio actas-hbox» y
la segunda prueba en sitio de una misma unidad daría «Ya existe un acta».
```bash
cd ~/Documents/Claude/Projects/Drogheda/hbox-docs
mkdocs build --strict
git add -A && git commit -m "Tablero de Instalaciones por sitio y acta de prueba en sitio" && git push
cd .. && git add hbox-docs && git commit -m "hbox-docs: tablero de Instalaciones" && git push && cd hbox-docs # sube el submódulo
./scripts/deploy.sh # sitio + backend (rsync, ya con instalaciones.py)
ssh leo@git.helgen.online 'sudo systemctl restart actas-hbox'
curl -s -u leo:PASS 'https://drogheda.helgen.online/api/instalaciones?sitio=icave' | head -c 300 # «"ok": true»
```
| Síntoma | Causa | Arreglo |
|---|---|---|
| «No se pudo leer el tablero de instalaciones (HTTP 404)» | servicio sin reiniciar, o falta `backend/instalaciones.py` en el servidor | `deploy.sh` y el `restart`; si falta el archivo, `journalctl -u actas-hbox` dice «instalaciones.py no se cargó» |
| Una unidad instalada no aparece | no tiene acta, o su acta no trae el sitio ni el nombre del puerto | llenar el acta de instalación con el sitio y la **fecha real** |
| La unidad sale en otro sitio | cuenta en el sitio de su acta más reciente | revisar el sitio de esa acta en el expediente |
---
## B · Verificación después de desplegar

View File

@ -2086,5 +2086,17 @@ def actividad():
return jsonify(ok=True, desde=desde, total=len(eventos), eventos=eventos[:limite], equipo=equipo)
# ------------------------------------------------------------------ tablero de Instalaciones (22-oct)
# backend/instalaciones.py: GET /api/instalaciones y el acta repetible «prueba-sitio». Si el archivo
# falta o falla, el resto del servicio arranca igual (el tablero avisa que no hay datos).
try:
import importlib.util as _ilu
_spec = _ilu.spec_from_file_location("instalaciones", os.path.join(os.path.dirname(os.path.abspath(__file__)), "instalaciones.py"))
_inst = _ilu.module_from_spec(_spec)
_spec.loader.exec_module(_inst)
_inst.registrar(app, globals())
except Exception as _e: # noqa: BLE001
app.logger.error("instalaciones.py no se cargó: %s", _e)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081")))

263
backend/instalaciones.py Normal file
View File

@ -0,0 +1,263 @@
"""Tablero de Instalaciones por sitio (22-oct-2026).
Lo engancha app.py al final con registrar(app, globals()); vive aparte para no crecer app.py.
GET /api/instalaciones[?sitio=eit] (solo lee)
Una ficha por unidad en cada sitio, armada con las actas de campo: envío → recepción →
instalación → prueba en sitio → en operación, más «con falla». Trae el vehículo (tipo,
número económico, voltaje), el nombre en el monitor (H00xx), fechas, técnico y sus actas.
Las series PRUEBA no cuentan. ?sitio= acepta el id (icave, timsa, eit, lzc) o un alias (lct).
Acta nueva «prueba-sitio» (formularios/prueba-sitio.html): la prueba después de instalar.
registrar() la vuelve repetible (como las demás de campo) y la suma a los tipos que lista
la página de cada sitio, sin tocar ACTAS_REPETIBLES en actas-hbox.service.
Reglas de las etapas (la última acta manda; se ordenan por la fecha que anotó el técnico):
- Enviada: acta de envío «Enviada» a ese sitio. Un envío nuevo empieza otro ciclo.
- Recibida en sitio: recepción «Lista para entrar» o «Entra con observaciones».
- Instalada: acta de instalación que no dice «No se pudo instalar». Una instalación nueva pide
otra prueba.
- Probada en sitio: SOLO con el acta «Prueba en sitio» aprobada (resultado OK).
- En operación: probada hace DIAS_OPERACION días o más, sin falla registrada después.
- Con falla (aparte de la columna): instalación sin reportar o FALLA, prueba no aprobada,
diagnóstico con falla, unidad desmontada o reemplazada, recepción rechazada, o marca de daño.
Cada serie se cuenta en un solo sitio: el de su acta más reciente.
"""
import datetime
import os
from flask import jsonify, request
TIPO_PRUEBA = "prueba-sitio"
TIPOS = ("envio", "recepcion", "instalacion-campo", TIPO_PRUEBA, "diagnostico-campo", "dano")
DIAS_OPERACION = 7
ORDEN = ("icave", "timsa", "eit", "lzc") # el orden de las pestañas (lo pidió Leo)
ETAPAS = ("enviada", "recibida", "instalada", "probada", "operacion")
def _resp(d, clave):
"""Respuesta de un campo del acta (d['respuestas'][clave]['val']) como texto."""
r = d.get("respuestas")
v = r.get(clave) if isinstance(r, dict) else None
v = v.get("val") if isinstance(v, dict) else None
return str(v).strip() if isinstance(v, str) else ""
def _dia(d):
f = str(d.get("fecha") or "")
if len(f) == 10 and f[4] == "-" and f[7] == "-":
return f
return str(d.get("_recibido") or "")[:10]
def _evento(tipo, fn, d, sitio, modo):
ev = {"tipo": tipo, "archivo": fn, "serie": d.get("serie"), "sitio": sitio, "coincidencia": modo,
"fecha": _dia(d), "recibido": str(d.get("_recibido") or ""), "tecnico": str(d.get("tecnico") or "")[:80],
"resultado": str(d.get("resultado") or ""), "accion": str(d.get("accion") or "")[:160],
"motivo": str(d.get("motivo") or "")[:160], "clasificacion": str(d.get("clasificacion") or "")[:120],
"maquina": str(d.get("maquina_id") or d.get("maquina") or "").strip()[:60],
"id_monitor": str(d.get("id_monitor") or _resp(d, "id_monitor")).strip().upper()[:20],
"fotos": len(d.get("fotos") or [])}
if tipo in ("instalacion-campo", TIPO_PRUEBA):
ev["tipo_vehiculo"] = (str(d.get("clasificacion") or "") or _resp(d, "tipo_vehiculo"))[:60]
ev["voltaje"] = (str(d.get("sistema_v") or "") or _resp(d, "sistema_v"))[:20]
ev["placas"] = _resp(d, "placas")[:40]
if tipo == "diagnostico-campo":
ev["repuesto"] = _resp(d, "unidad_repuesto")[:40]
if tipo == TIPO_PRUEBA:
ev["gga"] = _resp(d, "gga")[:40]
return ev
def _falla(ev, motivo):
return {"motivo": motivo, "tipo": ev["tipo"], "fecha": ev["fecha"], "archivo": ev["archivo"]}
def _ficha(serie, eventos, marca, hoy):
"""Estado de una serie en su sitio a partir de sus actas ahí (viejas → nuevas)."""
etapa, falla, instalada, retirada, sin_inst = None, None, False, False, False
fechas = {}
prueba = None
destino = ""
inicio = 0 # un envío nuevo empieza otro ciclo
for i, ev in enumerate(eventos):
t, acc, res = ev["tipo"], ev["accion"], ev["resultado"]
if t == "envio":
if acc.startswith("Lista pero detenida"):
continue
etapa, falla, instalada, retirada, sin_inst, prueba = "enviada", None, False, False, False, None
fechas, inicio = {"enviada": ev["fecha"]}, i
destino = ev["maquina"]
elif t == "recepcion":
destino = ev["maquina"] or destino
mala = acc.startswith(("No entra", "Rechazada")) or res == "FALLA"
if etapa in (None, "enviada", "recibida"):
etapa = "recibida"
fechas["recibida"] = ev["fecha"]
falla = _falla(ev, acc or "La recepción no pasó la inspección") if mala else None
elif mala: # ya estaba instalada: una recepción mala solo marca la falla
falla = _falla(ev, acc or "La recepción no pasó la inspección")
elif t == "instalacion-campo":
if acc.startswith("No se pudo instalar"):
falla = _falla(ev, "No se pudo instalar")
continue
etapa, instalada, retirada, sin_inst, prueba = "instalada", True, False, False, None
fechas["instalada"] = ev["fecha"]
fechas.pop("probada", None)
if acc.startswith("Instalada, sin reportar"):
falla = _falla(ev, "Instalada, sin reportar todavía")
elif res == "FALLA":
falla = _falla(ev, "Instalación con falla")
else:
falla = None
elif t == TIPO_PRUEBA:
if etapa not in ("instalada", "probada"):
sin_inst, instalada, retirada = True, True, False
fechas.setdefault("instalada", "")
if res == "OK" and not acc.startswith("No aprobada"):
etapa, falla, prueba = "probada", None, ev
fechas["probada"] = ev["fecha"]
else:
etapa = etapa if etapa == "probada" else "instalada"
falla = _falla(ev, "Prueba en sitio no aprobada")
elif t == "diagnostico-campo":
if acc.startswith("Desmontada"):
falla, instalada, retirada = _falla(ev, acc), False, True
elif acc.startswith("Reemplazada"):
falla = _falla(ev, "Reemplazada por otra unidad" + (f" ({ev.get('repuesto')})" if ev.get("repuesto") else ""))
instalada, retirada = False, True
elif res == "FALLA" or acc.startswith("Instalada, pero sin resolver"):
falla = _falla(ev, ev["motivo"] or acc or "Diagnóstico con falla")
if etapa is None:
etapa, instalada, sin_inst = "instalada", True, True
elif res == "OK":
falla = None
if etapa is None:
etapa, instalada, sin_inst = "instalada", True, True
fechas.setdefault("instalada", "")
if marca and marca.get("marca"):
a = marca.get("acta") or {}
m = "Dada de baja" if marca["marca"] == "BAJA" else "Marcada como dañada"
if a.get("clasificacion"):
m += ": " + str(a["clasificacion"])[:80]
falla = {"motivo": m, "tipo": "dano", "fecha": a.get("fecha") or "", "archivo": a.get("archivo") or ""}
if etapa is None and not falla:
return None
operacion_desde = None
if prueba and prueba.get("fecha"):
try:
operacion_desde = (datetime.date.fromisoformat(prueba["fecha"])
+ datetime.timedelta(days=DIAS_OPERACION)).isoformat()
except ValueError:
operacion_desde = None
if etapa == "probada" and not falla and operacion_desde and operacion_desde <= hoy:
etapa = "operacion"
# vehículo: el último que anotó una instalación, una prueba o un diagnóstico (de este ciclo)
veh = {"numero": "", "tipo": "", "voltaje": "", "placas": "", "acta": ""}
monitor = ""
for ev in eventos[inicio:]:
if ev["id_monitor"]:
monitor = ev["id_monitor"]
if ev["tipo"] in ("instalacion-campo", TIPO_PRUEBA, "diagnostico-campo") and ev["maquina"]:
veh["numero"], veh["acta"] = ev["maquina"], ev["tipo"]
for k in ("tipo_vehiculo", "voltaje", "placas"):
if ev.get(k):
veh["tipo" if k == "tipo_vehiculo" else k] = ev[k]
ultima = eventos[-1] if eventos else None
return {
"serie": serie,
"etapa": etapa,
"columna": "falla" if falla else etapa,
"falla": falla,
"instalada": bool(instalada and not retirada),
"retirada": retirada,
"sin_acta_instalacion": sin_inst,
"probada": etapa in ("probada", "operacion") and not falla,
"vehiculo": veh if (veh["numero"] or veh["tipo"]) else None,
"vehiculo_destino": destino or None,
"id_monitor": monitor or None,
"fechas": fechas,
"operacion_desde": operacion_desde,
"marca": (marca or {}).get("marca"),
"tecnico": ultima["tecnico"] if ultima else "",
"ultima": {k: ultima[k] for k in ("tipo", "fecha", "accion", "resultado")} if ultima else None,
"actas": [{k: ev[k] for k in ("tipo", "archivo", "fecha", "tecnico", "resultado", "accion")}
for ev in reversed(eventos)],
}
def registrar(app, g):
"""g = globals() de app.py: se usan sus funciones (lectura de actas y sitios) sin copiarlas."""
g["TIPOS_REPETIBLES"].add(TIPO_PRUEBA)
if TIPO_PRUEBA not in g["TIPOS_SITIO"]:
g["TIPOS_SITIO"] = tuple(g["TIPOS_SITIO"]) + (TIPO_PRUEBA,)
def _orden(s):
sid = s.get("id", "")
return (ORDEN.index(sid) if sid in ORDEN else len(ORDEN), s.get("orden", 50), s.get("nombre", ""))
@app.get("/api/instalaciones")
def instalaciones():
slug, leer = g["slug"], g["_leer_json"]
sitios = [s for s in g["_listar_json"](g["SITIOS_DIR"]) if s.get("activo", True) and s.get("id")]
sitios.sort(key=_orden)
ids = [s["id"] for s in sitios]
pedido = slug(request.args.get("sitio"), 30).lower()
pedido = g.get("ALIAS_SITIO", {}).get(pedido, pedido)
if pedido and pedido not in ids:
return jsonify(ok=False, error=f"No existe el sitio {pedido}."), 404
en_sitio = g["_acta_en_sitio"]
por_serie = {}
for tipo in TIPOS:
carpeta = os.path.join(g["BASE"], tipo)
if not os.path.isdir(carpeta):
continue
for fn in sorted(os.listdir(carpeta)):
if not fn.endswith(".json"):
continue
d = leer(os.path.join(carpeta, fn))
if not isinstance(d, dict) or d.get("_prueba"):
continue
serie = str(d.get("serie") or "")
if not serie or "PRUEBA" in serie.upper():
continue
donde, modo = None, None
for s in sitios: # primero el sitio elegido en el acta; luego por nombre
m = en_sitio(d, s)
if m == "sitio":
donde, modo = s["id"], m
break
if m and not donde:
donde, modo = s["id"], m
if donde:
por_serie.setdefault(serie, []).append(_evento(tipo, fn, d, donde, modo))
hoy = datetime.date.today().isoformat()
unidades = []
for serie, evs in por_serie.items():
evs.sort(key=lambda e: (e["fecha"], e["recibido"], e["archivo"])) # el archivo lleva microsegundos
sitio = evs[-1]["sitio"] # cuenta en el sitio de su acta más reciente
u = _ficha(serie, [e for e in evs if e["sitio"] == sitio], g["estado_serie"](serie), hoy)
if u:
u["sitio"] = sitio
u["coincidencia"] = "sitio" if any(e["coincidencia"] == "sitio" for e in evs if e["sitio"] == sitio) else "texto"
unidades.append(u)
resumen = {}
for sid in ids:
us = [u for u in unidades if u["sitio"] == sid]
r = {c: sum(1 for u in us if u["columna"] == c) for c in ETAPAS + ("falla",)}
r.update(unidades=len(us), instaladas=sum(1 for u in us if u["instalada"]),
probadas=sum(1 for u in us if u["probada"]),
sin_vehiculo=sum(1 for u in us if u["instalada"] and not (u["vehiculo"] or {}).get("numero")),
sin_prueba=sum(1 for u in us if u["instalada"] and u["etapa"] == "instalada" and not u["falla"]))
resumen[sid] = r
orden_col = {c: i for i, c in enumerate(("falla",) + ETAPAS)}
unidades.sort(key=lambda u: (ids.index(u["sitio"]), orden_col.get(u["columna"], 9), u["id_monitor"] or u["serie"]))
if pedido:
unidades = [u for u in unidades if u["sitio"] == pedido]
return jsonify(ok=True, generado=g["ahora"](), hoy=hoy, sitio=pedido or None, dias_operacion=DIAS_OPERACION,
sitios=[{k: s.get(k) for k in ("id", "nombre", "cliente", "ubicacion", "alias")} for s in sitios],
resumen=resumen, unidades=unidades, total=len(unidades))

View File

@ -0,0 +1,83 @@
{
"lote": "2026-10-22_instalaciones",
"autor": "claude",
"fecha": "2026-10-22",
"motivo": "Tablero de Instalaciones por sitio y acta «Prueba en sitio» (22-oct): cada unidad con su vehículo, su estado y su prueba; para la meta de 1–2 unidades por sitio solo cuentan las probadas",
"fuente": "/tableros/instalaciones/",
"cambios": [
{
"id": "t-prueba-en-sitio",
"poner": {
"descripcion": "22-oct: había metas de instalación pero no había prueba. Cada unidad instalada pasa la prueba en sitio y se registra con el acta «Prueba en sitio»: unidad y vehículo, fix RTK visto en el monitor, reporte en el monitor y en Wialon, posición coherente en el mapa, respaldo en batería si aplica, y fotos. Solo una prueba aprobada cuenta para la meta de 1–2 unidades por sitio; si pasan 7 días sin fallas, la unidad queda «En operación». Todo se ve en el tablero de Instalaciones.",
"enlaces": [
{ "titulo": "Acta de prueba en sitio", "url": "/formularios/prueba-sitio.html" },
{ "titulo": "Instalaciones por sitio", "url": "/tableros/instalaciones/" }
]
},
"nota": "Ya está el acta «Prueba en sitio» (Formularios → Campo): se llena en la máquina con alguien viendo el monitor. El tablero de Instalaciones la cuenta para la meta del sitio."
},
{
"id": "t-timsa-instalar",
"poner": {
"titulo": "TIMSA (Manzanillo): instalar y probar 1–2 unidades",
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas y probadas 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 acta de prueba en sitio (t-prueba-en-sitio); sin la prueba aprobada no cuenta para la meta. 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?sitio=timsa" },
{ "titulo": "Acta de prueba en sitio", "url": "/formularios/prueba-sitio.html?sitio=timsa" },
{ "titulo": "Instalaciones de TIMSA", "url": "/tableros/instalaciones/?sitio=timsa" }
]
},
"nota": "La meta es instalada y probada. Ligas a las dos actas, ya con TIMSA elegido, y a las instalaciones del sitio."
},
{
"id": "t-eit-instalar",
"poner": {
"titulo": "EIT (Ensenada): instalar y probar 1–2 unidades",
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas y probadas 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 acta de prueba en sitio (t-prueba-en-sitio); sin la prueba aprobada no cuenta para la meta. 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?sitio=eit" },
{ "titulo": "Acta de prueba en sitio", "url": "/formularios/prueba-sitio.html?sitio=eit" },
{ "titulo": "Instalaciones de EIT", "url": "/tableros/instalaciones/?sitio=eit" }
]
},
"nota": "La meta es instalada y probada. Ligas a las dos actas, ya con EIT elegido, y a las instalaciones del sitio."
},
{
"id": "t-icave-unidades",
"poner": {
"titulo": "ICAVE (Veracruz): confirmar 1–2 unidades instaladas y probadas",
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas y probadas por sitio. En ICAVE ya hay unidades: registrar el acta de instalación de cada una con su vehículo (t-registrar-instaladas) y hacerles la prueba en sitio; las que no reporten bien en el monitor o en Wialon van a la lista de t-icave-campo.",
"enlaces": [
{ "titulo": "Acta de instalación", "url": "/formularios/instalacion.html?sitio=icave" },
{ "titulo": "Acta de prueba en sitio", "url": "/formularios/prueba-sitio.html?sitio=icave" },
{ "titulo": "Instalaciones de ICAVE", "url": "/tableros/instalaciones/?sitio=icave" }
]
},
"nota": "«Reportando bien» ahora se demuestra con la prueba en sitio. Ligas a las dos actas, ya con ICAVE elegido, y a las instalaciones del sitio."
},
{
"id": "t-lct-instalar",
"poner": {
"titulo": "LCT (Lázaro Cárdenas): instalar y probar 1–2 unidades — estado por confirmar en la junta",
"descripcion": "Meta inmediata: 1 o 2 unidades instaladas y probadas por sitio. El estado de LCT no se revisó el 22-oct: confirmar en la junta qué hay en sitio y qué falta. Cada unidad lleva su acta de instalación con el vehículo y su acta de prueba en sitio (t-prueba-en-sitio).",
"enlaces": [
{ "titulo": "Checklist de puesta en marcha", "url": "/procedimientos/v5-checklist-puesta-en-marcha/" },
{ "titulo": "Acta de instalación", "url": "/formularios/instalacion.html?sitio=lzc" },
{ "titulo": "Acta de prueba en sitio", "url": "/formularios/prueba-sitio.html?sitio=lzc" },
{ "titulo": "Instalaciones de LCT", "url": "/tableros/instalaciones/?sitio=lzc" }
]
},
"nota": "La meta es instalada y probada. Ligas a las dos actas, ya con LCT elegido, y a las instalaciones del sitio."
},
{
"id": "t-acta-instalacion",
"nota": "22-oct: el acta ya pide el número económico, el tipo de vehículo (con Reach Stacker y Empty Handler), si es de 12 o 24 V y el nombre en el monitor, y lleva a la prueba en sitio. Falta probarla en una instalación real."
},
{
"id": "t-registrar-instaladas",
"nota": "Las que ya estaban instaladas se registran con la fecha real de instalación; después, cada una lleva su prueba en sitio. En el tablero de Instalaciones salen como «sin vehículo» o «sin prueba» mientras falte algo."
}
]
}

View File

@ -0,0 +1,80 @@
{
"lote": "2026-10-22_t40-v6",
"autor": "claude",
"motivo": "T40 aplicado en la v6: MH4 libre (D23/D24 fuera del barreno, /SIM_CLK por In1, /SIM_RST arriba de D25, /NTC alrededor del barreno, vías de GND de las TVS y de U19.92/94) y D-22-4 para Leo",
"fuente": "/hardware/v6-estado-21oct/#7-t40-22-oct-tarde-mh4-libre",
"cambios": [
{
"id": "t-v6-barrenos",
"poner": {
"estado": "hecho",
"titulo": "v6: liberar MH4 (MH6 con T38, MH8 con T39 y MH4 con T40)"
},
"nota": "22-oct, tarde: T40 (Claude) liberó MH4. D23 bajo U19.31, D24 junto a R129/R133, /SIM_CLK por In1, /SIM_RST arriba de D25 y /NTC a 0.95 mm de la orilla. DRC: 11 errores (los de margen), 0 sin conectar. Las TVS junto a J14 pasan a D-22-4."
},
{
"id": "t-v6-t39-push",
"poner": {
"estado": "hecho"
},
"nota": "Leo hizo el commit de T39 (d5f5002) y el push el 22-oct. T40 tiene su propio pendiente de push (t-v6-t40-push)."
},
{
"id": "t-v6-cierre-pedido",
"nota": "22-oct, tarde: T40 liberó MH4. DRC de la v6: 11 errores, los encimes de courtyard de margen; 0 sin conectar, paridad igual. Quedan 4 de los 8 bloqueantes del 16-oct y D-22-4 (las TVS de la SIM junto a J14)."
},
{
"crear": {
"id": "t-v6-t40-push",
"titulo": "v6: commit y push de T40 (MH4); Michelle no abre la v6 hasta el push",
"pista": "ingenieria",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "v6",
"fecha_limite": "2026-10-23",
"descripcion": "T40 cambió hbox-watchdog-v6.kicad_pcb (ya rellenado con kicad-cli y el proyecto; idéntico a la copia probada, a273d44c) y agregó kicad/tools/t40_v6_mh4.py. Primero el commit del submódulo kicad y su push; después el del repo principal (docs, figura, CLAUDE.md) con el submódulo al día. Los comandos están en la respuesta de Claude del 22-oct. Después, Michelle hace git pull --ff-only antes de abrir la v6.",
"enlaces": [
{
"titulo": "Estado de la v6, §7 (T40)",
"url": "/hardware/v6-estado-21oct/#7-t40-22-oct-tarde-mh4-libre"
}
],
"etiquetas": [
"git"
]
},
"si_no_hay": [
"push de T40"
]
},
{
"crear": {
"id": "d-22-4-v6-tvs-sim",
"titulo": "D-22-4: TVS de la SIM de la v6 — (a) se quedan del lado del módem o (b) junto a J14, en /SIMB_* — decide Leo",
"pista": "ingenieria",
"estado": "por-hacer",
"responsable": "leo",
"version_tarjeta": "v6",
"fecha_limite": "2026-10-24",
"descripcion": "T40 sacó D23–D25 de MH4, pero del lado del módem de las resistencias de 22 Ω: una descarga en el zócalo recorre 27–41 mm de pista y la resistencia antes de llegar a la TVS. El plan (L-38, por B-09) las pedía junto a J14, y el 28-sep se dejó fuera el arreglo de ESD de /SIM_VDD contando con eso. (a) Se quedan así: no cuesta nada y el módem queda protegido. (b) Recomendada por Claude: D23–D25 pasan a /SIMB_DATA, /SIMB_CLK y /SIMB_RST en el esquemático, pegadas a los pines de J14 y cada una con su vía de GND. Son las mismas piezas; lo hace Claude como T41 (esquemático y tarjeta por script) o Michelle el sábado 24.",
"enlaces": [
{
"titulo": "Estado de la v6, §7.2 (D-22-4)",
"url": "/hardware/v6-estado-21oct/#72-las-tvs-de-la-sim-se-quedan-del-lado-del-modem-d-22-4"
},
{
"titulo": "Plan de fines de semana, §3.4",
"url": "/hardware/v6-plan-fines-de-semana/#34-barrenos-mh4-y-mh6-1-h"
}
],
"etiquetas": [
"decisión",
"esd"
]
},
"si_no_hay": [
"D-22-4"
]
}
]
}

View File

@ -42,6 +42,7 @@ El acta de ensamble no deja elegir un módulo sin aprobar, sin integración o ya
| Acta | Qué registra | Procedimiento |
|---|---|---|
| [Instalación en campo](../formularios/instalacion.html) | Una por unidad y vehículo: sitio, equipo, alimentación 24 V, antenas, RTK FIX, señal 4G, fotos | [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md) · [Sitios](../sitios/index.md) |
| [Prueba en sitio](../formularios/prueba-sitio.html) | Después de instalar, una por unidad: fix RTK en el monitor, reporte en el monitor y en Wialon, posición, respaldo en batería, fotos. Solo con ella la unidad cuenta para la meta del sitio | [Instalaciones](../tableros/instalaciones.md) |
| [Diagnóstico en campo](../formularios/diagnostico.html) | Una por visita; muestra las visitas anteriores y admite fotos desde el teléfono | [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) · [Tablero de campo](../tableros/campo.md) |
| [Envío](../formularios/envio.html) · [Recepción](../formularios/recepcion.html) | El tramo entre el banco y el puerto | [Ciclo de vida](../procedimientos/ciclo-de-vida.md) |
| [Daño o incidente](../formularios/dano.html) | Cualquier serie; la marca en todos los formularios. «Reparada» la levanta, «Baja» la hace permanente | [Ciclo de vida](../procedimientos/ciclo-de-vida.md#dano-o-incidente) |

View File

@ -23,6 +23,9 @@ Arriba, lo que pasó en el tablero de pendientes y en las actas en los últimos
### Octubre de 2026
**22 oct**
: [Instalaciones por sitio](tableros/instalaciones.md): tablero nuevo, una tarjeta por unidad con **en qué vehículo está** (número económico, tipo, 12/24 V), sus actas y su etapa: enviada → recibida → instalada → **probada en sitio** → en operación, más «con falla». Acta nueva [Prueba en sitio](formularios/prueba-sitio.html): solo con ella aprobada una unidad cuenta para la meta de 1–2 por sitio.
: [Estado de la v6, rev. 1.3: T40, MH4 libre](hardware/v6-estado-21oct.md#7-t40-22-oct-tarde-mh4-libre): D23 y D24 fuera del barreno, `/SIM_CLK` por In1, `/SIM_RST` por arriba de D25 y `/NTC` alrededor del barreno; las tres TVS de la SIM y los pines 92/94 de U19 con vía de GND propia. DRC: **11 errores** (los encimes de margen), 0 sin conectar. Leo decide D-22-4: las TVS siguen del lado del módem y Claude recomienda pasarlas junto a J14. Falta el push de T40; lote `2026-10-22_t40-v6`.
: [Plan de fines de semana de la v6, rev. 1.5](hardware/v6-plan-fines-de-semana.md#34-barrenos-mh4-y-mh6-1-h): MH4 ya quedó; de los barrenos solo queda D-22-4.
: [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`.

View File

@ -22,6 +22,7 @@ Cada sitio muestra sus unidades instaladas y sus actas de instalación y diagnó
| Situación | Procedimiento | Acta |
|---|---|---|
| **Instalar en el puerto** (gabinete montado, 24 V, antenas, RTK FIX) | [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md) — imprimir, una hoja por unidad | [Instalación en campo](../formularios/instalacion.html) — sitio, unidad, vehículo, antena, alimentación, RTK FIX, fotos |
| **Probar la unidad recién instalada** (monitor, Wialon, posición, batería) | En el acta; el avance por sitio está en [Instalaciones](../tableros/instalaciones.md) | [Prueba en sitio](../formularios/prueba-sitio.html) — solo con ella cuenta para la meta del sitio |
| **Falla en la máquina** | [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](../formularios/diagnostico.html) — una por visita, con fotos desde el teléfono |
| Sale una unidad del banco | [Ciclo de vida §6](../procedimientos/ciclo-de-vida.md#6-envio) | [Envío](../formularios/envio.html) |
| Llega una unidad al puerto o al taller | [Ciclo de vida §7](../procedimientos/ciclo-de-vida.md#7-recepcion-e-inspeccion) | [Recepción e inspección](../formularios/recepcion.html) |

View File

@ -267,8 +267,8 @@
],
[
"bloqueado",
"5 bloqueantes",
"T39: bloque A y MH8 listos; faltan MH4 y los bloques B a H"
"4 bloqueantes",
"T39 y T40: bloque A y los 5 barrenos listos; faltan los bloques B a H"
],
[
"falta",
@ -296,16 +296,16 @@
"después de G2"
]
],
"detalle": "No lista (22-oct): T25, T28 y T38 ya están en HBox_PCB (b74b96f). T39 (Claude, 22-oct) hizo el bloque A con la propuesta α: D34 entre U25 y L22 (cátodo a 3.52 mm del pin SW, lazo de 56.5 mm² con C149, antes ≈ 115), 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. Quedan 5 de los 8 bloqueantes (MH4, 5 V por capas internas, cargador U6, RF por In1, vías de tierra).",
"siguiente": "Leo: commit y push de T39; Michelle hace git pull --ff-only antes de abrir la v6. Claude evalúa MH4 (T40). Sáb 24: Michelle revisa el bloque A, hace Rig1 y el /24V sin montar, MH4 si T40 no lo resolvió, y sigue con el bloque B. Dom 25: bloques C, D y E (módem, GNSS y U12); en la noche, re-auditoría de Claude. Sáb 31: bloques F, G y H1 (pares, puertos serie e In1). Dom 1-nov: H2 y DRC final; en la tarde, re-auditoría final y Gerber (G1). Lun 2-nov: pedido del piloto de 20. Firmware los fines de semana del 7–8 y del 14–15 nov; banco del piloto el 21–22 nov (G2). No habrá v6.1.",
"detalle": "No lista (22-oct): T25, T28, T38 y T39 ya están en HBox_PCB (d5f5002). T39 (Claude) hizo el bloque A con la propuesta α: D34 entre U25 y L22 (cátodo a 3.52 mm del pin SW, lazo de 56.5 mm² con C149, antes ≈ 115), In1 e In2 sin pistas bajo la fuente y MH8 libre. T40 (Claude, 22-oct, tarde) dejó libre MH4: D23 y D24 fuera del barreno, /SIM_CLK por In1, /SIM_RST por arriba de D25, /NTC alrededor del barreno y vías de GND para las tres TVS de la SIM. DRC: 11 errores (los encimes de margen), 0 sin conectar. Quedan 4 de los 8 bloqueantes (5 V por capas internas, cargador U6, RF por In1, vías de tierra) y D-22-4 (las TVS de la SIM junto a J14).",
"siguiente": "Leo: commit y push de T40, y D-22-4 (Claude recomienda pasar las TVS de la SIM a /SIMB_*, junto a J14); Michelle hace git pull --ff-only antes de abrir la v6. Sáb 24: Michelle revisa el bloque A y MH4, hace Rig1 y el /24V sin montar, las TVS junto a J14 si Leo elige (b), y sigue con el bloque B. Dom 25: bloques C, D y E (módem, GNSS y U12); en la noche, re-auditoría de Claude. Sáb 31: bloques F, G y H1 (pares, puertos serie e In1). Dom 1-nov: H2 y DRC final; en la tarde, re-auditoría final y Gerber (G1). Lun 2-nov: pedido del piloto de 20. Firmware los fines de semana del 7–8 y del 14–15 nov; banco del piloto el 21–22 nov (G2). No habrá v6.1.",
"quien": "Michelle (layout) · Leo (scripts y pedido) · Claude (re-auditoría y Gerber) · Aldair (pedido) · Jesús (banco)",
"docs": [
[
"Estado de la v6 (22-oct, T39)",
"Estado de la v6 (22-oct, T39 y T40)",
"/hardware/v6-estado-21oct/"
],
[
"Plan de fines de semana (rev. 1.4)",
"Plan de fines de semana (rev. 1.5)",
"/hardware/v6-plan-fines-de-semana/"
],
[
@ -432,7 +432,7 @@
"etiqueta": "der",
"nota": "mal clima: ventanas cortas en campo y fallas por investigar · se compara con el CalAmp",
"estado": "amarillo",
"meta": "1–2 unidades instaladas"
"meta": "1–2 unidades instaladas y probadas"
},
{
"id": "lzc",
@ -443,7 +443,7 @@
"etiqueta": "der",
"estado": "amarillo",
"nota": "estado por confirmar en la junta del 22-oct",
"meta": "1–2 unidades instaladas"
"meta": "1–2 unidades instaladas y probadas"
},
{
"id": "eit",
@ -454,7 +454,7 @@
"etiqueta": "der",
"estado": "rojo",
"nota": "esperando el material que se envió desde Manzanillo",
"meta": "1–2 unidades instaladas"
"meta": "1–2 unidades instaladas y probadas"
},
{
"id": "timsa",
@ -465,7 +465,7 @@
"etiqueta": "izq",
"estado": "rojo",
"nota": "bloqueado por un trámite administrativo del vehículo",
"meta": "1–2 unidades instaladas"
"meta": "1–2 unidades instaladas y probadas"
}
],
"oficina": {

View File

@ -20,6 +20,8 @@
item.tipo = 'sitio' → lista de sitios (puertos/terminales) desde /api/sitios; con
guardaEn:'sitio' el slug queda en la raíz del acta y la página
del sitio la encuentra (sitios/sitio.html?id=)
Precarga (22-oct): formulario.html?serie=…&sitio=…&<clave>=… elige la unidad y llena los campos
vacíos (lo usa el tablero de Instalaciones para abrir la siguiente acta).
Modos de serie:
(a) simple: cfg.prefijo + cfg.series (cantidad) [+ cfg.desde, cfg.pad] → "RTK-005"
@ -94,6 +96,7 @@
const tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' });
const flota = Array.isArray(cfg.unidades) && cfg.unidades.length > 0;
let ensambladas = null; // promesa: unidades HBOX completas (para la precarga)
const serieOtra = $('input', { type: 'text', id: 'f_serie_otra', placeholder: 'Escribe la etiqueta de la unidad tal como viene en la caja' });
const fichaUnidad = $('div', { class: 'ficha-unidad' });
const armar = $('div', { class: 'armar-hbox' }); const armarSel = {};
@ -107,7 +110,7 @@
grupos[g].forEach(u => og.append($('option', { value: u.serie, text: u.etiqueta || (u.serie + (u.maquina ? ' · ' + u.maquina : '')) })));
serieSel.append(og);
});
if (!cfg.sinEnsambladas) cargarEnsambladas();
if (!cfg.sinEnsambladas) ensambladas = cargarEnsambladas();
if (!cfg.sinEnsambladas) serieSel.insertBefore($('option', { value: '__HBOX__', text: 'Unidad HBOX completa: elegir sus 3 piezas' }), serieSel.children[1] || null);
serieSel.append($('option', { value: '__OTRA__', text: 'Otra unidad (no está en la lista)' }));
serieSel.append($('option', { value: 'CAMPO-PRUEBA', text: 'PRUEBA (no cuenta)' }));
@ -607,7 +610,7 @@
try {
const r = await fetch(`/api/historial?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(s)}`, { cache: 'no-store' });
const j = await r.json();
if (!j.total) { hist.innerHTML = '🆕 <b>Primera visita</b> registrada para esta unidad.'; return; }
if (!j.total) { hist.innerHTML = '<b>Primera visita</b> registrada para esta unidad.'; return; }
hist.innerHTML = '';
hist.append($('div', { class: 'hist-t', html: '<b>' + j.total + ' visita(s) anterior(es)</b> a esta unidad — léelas antes de empezar:' }));
j.visitas.slice(0, 6).forEach(v => {
@ -716,7 +719,7 @@
}
// ---------------- cargar una serie ----------------
async function loadSerie() {
async function cargarSerie() {
msg.className = 'msg'; locked = false;
if (flota) {
serieOtra.style.display = serieSel.value === '__OTRA__' ? '' : 'none';
@ -749,6 +752,53 @@
}
}
}
// ---------------- precarga desde la dirección (22-oct) ----------------
// El tablero de Instalaciones abre la siguiente acta ya con la unidad, el sitio y el vehículo:
// ?serie=HBOX5-…&sitio=eit&maquina_id=TP-114 (cualquier campo por su clave). Solo llena lo vacío:
// no pisa lo escrito ni el borrador guardado, y nunca marca casillas, fotos ni el resultado.
const pre = new URLSearchParams(location.search);
const preSerie = String(pre.get('serie') || '').trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '-').slice(0, 24);
async function precargarSerie() {
if (!preSerie || !flota) return;
const hay = () => Array.from(serieSel.options).some(o => o.value === preSerie);
if (!hay() && ensambladas) { try { await ensambladas; } catch (e) {} }
if (hay()) serieSel.value = preSerie;
else { serieSel.value = '__OTRA__'; serieOtra.value = preSerie; }
}
function precargarCampos() {
if (locked || (preSerie && serieActual() !== preSerie)) return;
let n = preSerie && flota ? 1 : 0;
pre.forEach((v, k) => {
if (k === 'serie' || !/^[a-z][a-z0-9_]{0,39}$/.test(k)) return;
v = String(v || '').trim().slice(0, 200);
const els = Array.from(fields()).filter(e => e.dataset.id === k);
if (!v || !els.length) return;
if (els[0].type === 'radio') {
if (els.some(e => e.checked)) return;
const r = els.find(e => e.value === v);
if (r) { r.checked = true; n++; }
return;
}
const el = els[0];
if (el.disabled || ['checkbox', 'hidden', 'file'].includes(el.type)) return;
if (el.tagName === 'SELECT') {
if (el.value) return;
if (Array.from(el.options).some(o => o.value === v)) el.value = v; else el.dataset.pend = v;
} else {
if (el.value && el.value !== el.dataset.def) return;
el.value = v;
}
n++;
});
if (!n) return;
aplicarCondiciones();
if (banner.className === 'msg') {
banner.className = 'msg ok';
banner.textContent = 'Datos precargados desde el tablero de Instalaciones. Revisa que la unidad, el sitio y el vehículo sean los correctos antes de firmar.';
}
}
async function loadSerie() { await cargarSerie(); precargarCampos(); }
wrap.addEventListener('input', () => { aplicarCondiciones(); guardarLocal(); });
wrap.addEventListener('change', aplicarCondiciones);
serieSel.addEventListener('change', loadSerie);
@ -833,6 +883,7 @@
if (!tecnico.value) tecnico.value = nombreUsuario;
}
} catch (e) {}
await precargarSerie();
loadSerie();
})();
}

View File

@ -36,12 +36,14 @@ Acta.render({
textoOK: 'INSTALADA Y REPORTANDO',
intro: '<details class="como" open><summary>Cómo se llena</summary><ol>'
+ '<li>Elige el <b>sitio</b> y la <b>unidad</b> (la etiqueta de la caja, <code>HBOX5-Exx-Axxx-Rxxxx</code>). Si la unidad trae aviso de daño, no la instales.</li>'
+ '<li>Anota el vehículo <b>como lo nombra el puerto</b> (FL-JUN-060, RT-ZPMC-20): así lo buscan después.</li>'
+ '<li>Anota el vehículo: su <b>número económico como lo nombra el puerto</b> (FL-JUN-060, RT-ZPMC-20), su tipo y si es de 12 V o 24 V. Así lo buscan después y así sale en el <a href="/tableros/instalaciones/">tablero de Instalaciones</a>.</li>'
+ '<li>Marca cada punto del checklist <b>después</b> de hacerlo, no antes. Mide el voltaje con la máquina encendida.</li>'
+ '<li>Espera el <b>RTK FIX</b> con la antena ya fija en su lugar definitivo y anota cuánto tardó.</li>'
+ '<li>Confirma con alguien que vea el monitor que la posición aparece. Sin confirmación, el resultado es FALLA.</li>'
+ '<li>Fotos: gabinete montado, antena, interior con cables sujetos. Si no subieron por señal, reintenta antes de irte.</li>'
+ '<li>Resultado: <b>INSTALADA Y REPORTANDO</b> solo si todo quedó. Si no, FALLA y explica qué faltó; se vuelve con otra acta.</li>'
+ '<li>Después va la <b><a href="prueba-sitio.html">prueba en sitio</a></b>: sin ella la unidad no cuenta para la meta del sitio.</li>'
+ '<li>¿La unidad ya estaba instalada y no tiene acta? Llénala ahora con la <b>fecha real</b> de la instalación; lo que no sepas, anótalo en observaciones.</li>'
+ '</ol></details>',
secciones: [
@ -50,10 +52,14 @@ Acta.render({
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha de la instalación' },
{ tipo:'sitio', clave:'sitio', req:true, guardaEn:'sitio', txt:'Sitio (puerto o terminal)',
ayuda:'Si el sitio no está en la lista, elige «Otro» y escríbelo en observaciones; Leo lo da de alta.' },
{ tipo:'texto', clave:'maquina_id', req:true, guardaEn:'maquina_id', rows:'1', txt:'Vehículo o equipo (ID como lo nombra el puerto)', ph:'FL-JUN-060 · RT-ZPMC-20 · HS-WES-62' },
{ tipo:'texto', clave:'maquina_id', req:true, guardaEn:'maquina_id', rows:'1', txt:'Número económico del vehículo o equipo (como lo nombra el puerto)', ph:'FL-JUN-060 · RT-ZPMC-20 · HS-WES-62' },
{ tipo:'radio', clave:'tipo_vehiculo', req:true, guardaEn:'clasificacion', txt:'Tipo de vehículo o equipo',
opciones:['RTG','Montacargas','Tractoplana','Grúa','Camión / pick-up','Equipo fijo','Otro'] },
{ tipo:'texto', clave:'placas', rows:'1', txt:'Placas o número económico (si aplica)' },
opciones:['RTG','Reach Stacker','Empty Handler','Tractoplana','Montacargas','Grúa','Camión / pick-up','Equipo fijo','Otro'] },
{ tipo:'radio', clave:'sistema_v', req:true, guardaEn:'sistema_v', txt:'Sistema eléctrico del vehículo',
opciones:['24 V','12 V','No se sabe'] },
{ tipo:'nota', txt:'En ICAVE, RTG, Reach Stacker y Empty Handler son de 24 V; Tractoplana y Montacargas, de 12 V. La unidad toma 24 V de la fuente del gabinete en los dos casos; la ignición llega directo del vehículo.' },
{ tipo:'texto', clave:'placas', rows:'1', txt:'Placas (si aplica)' },
{ tipo:'texto', clave:'id_monitor', rows:'1', guardaEn:'id_monitor', txt:'Nombre de la unidad en el monitor (H00xx), si ya lo sabes', ph:'H0013' },
{ tipo:'texto', clave:'acompanante', rows:'1', txt:'Quién autorizó o acompañó (operador, supervisor)', ph:'Abraham (ICAVE), 10:30' },
] },
@ -113,7 +119,7 @@ Acta.render({
items: [
{ tipo:'radio', clave:'accion', req:true, guardaEn:'accion', txt:'Cómo quedó',
opciones:['Instalada y reportando','Instalada, sin reportar todavía (explica)','No se pudo instalar (explica)'],
aviso:'Solo «Instalada y reportando» cuenta como instalación terminada en el tablero y en la página del sitio.' },
aviso:'Solo «Instalada y reportando» cuenta como instalación terminada en el tablero de Instalaciones y en la página del sitio. Para contar en la meta del sitio falta la <b>prueba en sitio</b>.' },
{ tipo:'check', clave:'operador_informado', txt:'Operador o supervisor informado: no desconectar ni abrir el gabinete; a quién llamar.' },
{ tipo:'check', clave:'registro_sitio', txt:'Vehículo y unidad anotados en la hoja de contacto que se queda en el sitio.' },
{ tipo:'texto', clave:'observaciones', rows:'3', txt:'Observaciones (qué faltó, qué cambió respecto al plan)' },

View File

@ -0,0 +1,122 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prueba en sitio — HBOX</title>
<script src="../js/tema.js"></script>
<link rel="stylesheet" href="acta.css">
<style>
details.como{background:var(--hg-plano);border:1px solid var(--hg-linea);border-radius:10px;padding:8px 14px;margin:0 0 12px;font-size:14px;color:var(--hg-texto)}
details.como summary{cursor:pointer;font-weight:700}
details.como ol{margin:8px 0 4px;padding-left:20px} details.como li{margin:3px 0}
</style>
</head>
<body>
<div id="acta"></div>
<script src="flota.js"></script>
<script src="series.js"></script>
<script src="acta.js"></script>
<script>
/* Prueba en sitio (22-oct-2026): el paso que faltaba después de instalar. Una unidad cuenta para la
meta del sitio (1–2 unidades) solo con esta acta aprobada. La lee el tablero de Instalaciones
(GET /api/instalaciones, backend/instalaciones.py). Tipo repetible: cada prueba es un registro. */
const conFix = v => String(v.gga || '').startsWith('4');
const conPos = v => /^[45]/.test(String(v.gga || ''));
const conBat = v => String(v.bateria || '').startsWith('Sí');
const usaWia = v => !String(v.wialon || '').startsWith('No aplica');
Acta.render({
tipo: 'prueba-sitio',
repetible: true,
tecnicoDesdeUsuario: true,
nombres: { alan: 'Alán Basurto', daniel: 'Daniel Martínez', leo: 'Leo Rodríguez' },
unidades: window.SERIES,
etiquetaSerie: 'Unidad HBOX que se prueba',
titulo: 'Prueba en sitio',
subtitulo: 'Después de instalar, una por unidad. Se llena en la máquina con alguien viendo el monitor.',
textoOK: 'APROBADA EN SITIO',
intro: '<details class="como" open><summary>Cómo se llena</summary><ol>'
+ '<li>Va <b>después del acta de instalación</b>, con la máquina encendida y la antena en su lugar definitivo.</li>'
+ '<li>Elige el <b>sitio</b> y la <b>unidad</b>. Anota el número económico del vehículo y el <b>nombre de la unidad en el monitor</b> (H00xx): así la busca quien ve el monitor.</li>'
+ '<li>Alguien con acceso abre el monitor de Helgen (y Wialon, si el sitio lo usa) y te dice lo que ve. Marca cada punto <b>después</b> de verlo.</li>'
+ '<li>Fix: <b>4 = RTK FIX</b>, lo esperado. <b>5 = RTK FLOAT</b>, solo si en 15 min no llega a 4 y explicas por qué (cielo tapado, antena). 1, 2 o 0 no se aprueba.</li>'
+ '<li>Si la unidad lleva batería, prueba el respaldo: con permiso del operador, apaga la máquina y confirma que sigue reportando.</li>'
+ '<li>Resultado <b>APROBADA EN SITIO</b> solo si reporta en el monitor con fix 4 (o 5 explicado) y la posición coincide con el vehículo. Si no, FALLA y explica qué faltó; se repite con otra acta.</li>'
+ '<li>Solo una prueba aprobada cuenta para la meta del sitio. Se ve en el <a href="/tableros/instalaciones/">tablero de Instalaciones</a>.</li>'
+ '</ol></details>',
secciones: [
{ titulo: '0 · Unidad y vehículo',
items: [
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha de la prueba' },
{ tipo:'sitio', clave:'sitio', req:true, guardaEn:'sitio', txt:'Sitio (puerto o terminal)',
ayuda:'Si el sitio no está en la lista, elige «Otro» y escríbelo en observaciones; Leo lo da de alta.' },
{ tipo:'texto', clave:'maquina_id', req:true, guardaEn:'maquina_id', rows:'1', txt:'Número económico del vehículo o equipo (como lo nombra el puerto)', ph:'FL-JUN-060 · RT-ZPMC-20 · HS-WES-62' },
{ tipo:'radio', clave:'tipo_vehiculo', req:true, guardaEn:'clasificacion', txt:'Tipo de vehículo o equipo',
opciones:['RTG','Reach Stacker','Empty Handler','Tractoplana','Montacargas','Grúa','Camión / pick-up','Equipo fijo','Otro'] },
{ tipo:'texto', clave:'id_monitor', req:true, guardaEn:'id_monitor', rows:'1', txt:'Nombre de la unidad en el monitor (H00xx)', ph:'H0013' },
{ tipo:'texto', clave:'quien_ve', req:true, rows:'1', txt:'Quién ve el monitor y Wialon, y desde dónde', ph:'Leo, desde Querétaro, por teléfono' },
] },
{ titulo: '1 · Fix RTK en el monitor',
sub:'Lo que dice el monitor de Helgen para esta unidad (calidad de la posición GGA).',
items: [
{ tipo:'radio', clave:'gga', req:true, txt:'Calidad del fix en el monitor',
opciones:['4 — RTK FIX','5 — RTK FLOAT (explica por qué no llega a 4)','1 o 2 — GPS sin RTK','0 — sin posición o no aparece'],
aviso:'4 es lo esperado. 5 se acepta solo si en 15 minutos no llega a 4 y lo explicas en observaciones.' },
{ tipo:'medida', clave:'t_fix', si:conFix, txt:'Minutos desde que encendió hasta el FIX', esperado:'< 10 min con cielo libre', u:'min' },
{ tipo:'medida', clave:'sats', si:conPos, txt:'Satélites en uso', esperado:'≥ 10', u:'sat' },
{ tipo:'medida', clave:'edad_corr', si:conPos, txt:'Edad de las correcciones, si el monitor la muestra', esperado:'< 5 s', u:'s' },
] },
{ titulo: '2 · Reporte en el monitor y en Wialon',
items: [
{ tipo:'radio', clave:'monitor', req:true, txt:'Reporta en el monitor de Helgen',
opciones:['Sí — posiciones nuevas cada pocos segundos','Sí, con huecos o retraso (explica)','No reporta'] },
{ tipo:'radio', clave:'wialon', req:true, txt:'Reporta en Wialon',
opciones:['Sí — aparece con su vehículo','Sí, pero con otro nombre o vehículo (explica)','No reporta','No aplica — este sitio no usa Wialon'] },
{ tipo:'texto', clave:'hora_monitor', req:true, rows:'1', txt:'Hora en que se vio el último reporte', ph:'11:42' },
] },
{ titulo: '3 · Posición coherente',
items: [
{ tipo:'radio', clave:'posicion', req:true, txt:'La posición en el mapa coincide con dónde está el vehículo',
opciones:['Sí — en su lugar del patio, sin saltos','Con saltos o desplazada (explica cuánto)','No coincide'] },
{ tipo:'radio', clave:'movimiento', txt:'Al mover el vehículo, la traza sigue el recorrido',
opciones:['Sí','Con saltos o cortes (explica)','No se pudo mover el vehículo'] },
{ tipo:'radio', clave:'legado', si:usaWia, txt:'Contra el GPS que ya tenía el vehículo (CalAmp u otro), en Wialon',
opciones:['Coinciden; el RTK más fino','Difieren (explica cuánto)','El vehículo no tiene otro GPS'] },
] },
{ titulo: '4 · Respaldo en batería (si aplica)',
items: [
{ tipo:'radio', clave:'bateria', req:true, txt:'La unidad lleva batería de respaldo',
opciones:['Sí','No — sin batería'] },
{ tipo:'nota', si:conBat, txt:'Solo con permiso del operador: apaga la máquina (o quita la llave) y mira el monitor 5 minutos. Al terminar, enciéndela y confirma que la unidad sigue reportando.' },
{ tipo:'radio', clave:'respaldo', si:conBat, req:true, txt:'Con la máquina apagada, la unidad sigue reportando',
opciones:['Sí — 5 minutos o más','Se corta antes de 5 minutos (explica)','No reporta en batería','No se pudo apagar la máquina'] },
{ tipo:'check', clave:'regreso', si:conBat, txt:'Al volver a encender la máquina, la unidad sigue reportando.' },
] },
{ titulo: '5 · Fotos',
items: [
{ tipo:'foto', clave:'f_monitor', req:true, etiqueta:'monitor', txt:'Pantalla del monitor con la unidad y su fix (foto o captura).' },
{ tipo:'foto', clave:'f_wialon', si:usaWia, etiqueta:'wialon', txt:'Pantalla de Wialon con la unidad.' },
{ tipo:'foto', clave:'f_vehiculo', req:true, etiqueta:'vehiculo', txt:'El vehículo con su número económico a la vista.' },
{ tipo:'archivo', clave:'log', etiqueta:'prueba-sitio', txt:'Captura de consola (opcional)' },
] },
{ titulo: '6 · Cierre',
items: [
{ tipo:'radio', clave:'accion', req:true, guardaEn:'accion', txt:'Cómo quedó',
opciones:['Aprobada — queda en operación','Aprobada con observaciones (explica)','No aprobada — hay que volver (explica)'],
aviso:'Solo una prueba aprobada cuenta para la meta del sitio. Si no se aprobó, marca FALLA abajo: la unidad aparece en «Con falla» en el tablero de Instalaciones.' },
{ tipo:'check', clave:'operador_informado', txt:'Operador o supervisor informado del resultado y de a quién llamar si deja de reportar.' },
{ tipo:'texto', clave:'observaciones', rows:'3', txt:'Observaciones (qué faltó, qué se vio raro, FLOAT explicado)' },
] },
]
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

View File

@ -11,16 +11,16 @@ Estado de cada versión de la EinarBoard al **22-oct-2026**, con sus documentos
- **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 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).
- **v6:** T39 (22-oct, Claude) hizo el bloque A con la propuesta α, y T40 (22-oct, tarde) dejó libre MH4: D23 y D24 fuera del barreno, `/SIM_CLK` por In1, `/SIM_RST` por arriba de D25 y `/NTC` alrededor del barreno. DRC: **11 errores** (los encimes de margen), 0 sin conectar. Falta el push de T40 y que Leo decida D-22-4 (las TVS de la SIM junto a J14) → [estado de la v6, §7](v6-estado-21oct.md#7-t40-22-oct-tarde-mh4-libre).
[**▶ 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).
[**▶ 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.5)](v6-plan-fines-de-semana.md).
| Tarjeta | Estado | Pedido | Proyecto KiCad | Firmware |
|---|---|---|---|---|
| <span class="badge-v55">V5.5</span> | En campo (15 pzas, 22-ago). No se fabrica más. | — | `hbox-watchdog-v5` (tag `v5.5-as-fabbed`) | `hbox-v5-bringup_0.6.0` CAMPO · 0.6.3 banco |
| <span class="badge-v56">V5.6</span> | **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-v6">v6</span> | Revisada el 22-oct (`d5f5002` + T40): **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, **T39 hizo el bloque A** (D34 entre U25 y L22, MH8 libre) y **T40 liberó MH4**. Faltan los bloques B a H y D-22-4 ([estado, §7](v6-estado-21oct.md#7-t40-22-oct-tarde-mh4-libre)). Plan: [fines de semana 24-oct → 22-nov](v6-plan-fines-de-semana.md). | 130 + reemplazos (piloto de 20, pedido previsto el 2-nov) | `hbox-watchdog-v6` | 0.7.x (`board_v6.h` + NTRIP), no existe |
| <span class="badge-v4">V4</span> | En prueba y campo. Sin producción nueva. | — | `hbox-watchdog-v4` | `hbox-tarjetaV4_0.6.0` CAMPO |
| <span class="badge-v3">V3</span> | 3 unidades en campo. Registro congelado. | — | `hbox-watchdog-v3-PROD` (no editar) | `hbox-tarjetaV3_4.0.0` |
@ -132,23 +132,32 @@ DRC: 52 errores, todos conocidos (MH4 28 · MH8 10 · courtyards 14), 0 sin cone
- In1 e In2 sin pistas bajo la fuente: I²C y la toma de 5 V de R121 pasan a B.Cu.
- MH8 libre.
DRC: **44 errores** (≈ 33 de MH4 y 11 encimes de courtyard de margen, que ya estaban), 0 sin conectar, paridad igual. Del bloque A quedan Rig1 y el `/24V` sin montar. Detalle: [**▶ estado de la v6, rev. 1.2 (§6)**](v6-estado-21oct.md#6-t39-22-oct-el-bloque-a-por-script).
DRC: **44 errores** (≈ 33 de MH4 y 11 encimes de courtyard de margen, que ya estaban), 0 sin conectar, paridad igual. Del bloque A quedan Rig1 y el `/24V` sin montar. Detalle: [**▶ estado de la v6, rev. 1.3 (§6)**](v6-estado-21oct.md#6-t39-22-oct-el-bloque-a-por-script).
**22-oct, tarde (T40, Claude): MH4 libre.**
- D23 (TVS de `/SIM_DATA`) debajo de U19.31 y D24 (TVS de `/SIM_CLK`) junto a R129/R133; D25 no se mueve. Las tres tienen vía de GND propia.
- `/SIM_CLK` baja a In1; `/SIM_RST` pasa por F.Cu arriba de D25, a 4.2 mm del centro, hasta una vía nueva.
- `/NTC` (In2) ya no cruza el barreno: pasa a 0.95 mm de su orilla.
- Los pines 92 y 94 de U19 quedaban sin tierra: pista de GND y vía nueva.
DRC: **11 errores**, los encimes de margen; ya no hay errores de barreno, de máscara ni de courtyard con barreno. 0 sin conectar, paridad igual. **D-22-4 (Leo):** las TVS siguen del lado del módem; Claude recomienda pasarlas a `/SIMB_*`, pegadas a J14 (L-38). Detalle: [**▶ estado de la v6, rev. 1.3 (§7)**](v6-estado-21oct.md#7-t40-22-oct-tarde-mh4-libre).
El firmware que sustituye al 4G NTRIP Master no existe. Decisión vigente: **no habrá v6.1**.
| | Documentos |
|---|---|
| Vigentes | [**▶ Estado al 22-oct (T25, T28, T38 y T39)**](v6-estado-21oct.md) · [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.4: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) |
| Vigentes | [**▶ Estado al 22-oct (T25, T28, T38, T39 y T40)**](v6-estado-21oct.md) · [**▶ Auditoría de producción v6 (16-oct)**](v6-auditoria-produccion-16oct.md) · [**▶ Plan de fines de semana (rev. 1.5: 24-oct → 22-nov)**](v6-plan-fines-de-semana.md) · [Re-auditoría 19-oct, §8](re-auditoria-19oct.md#8-v6) · [Plan de corrección y lista de liberación (28-sep, el porqué de cada punto)](v6-plan-correccion.md) · [Esquemático final — resumen de 3 pasadas](v6-cambios-resumen.md) · [Mapa de pines MCU para `board_v6.h`](v6-pinmap.md) · [Plan de cobre y colocación EMC](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) · [Manual v6 (marcador)](../manuales/manual-tarjeta-v6.md) |
| Detalle (28-sep) | [Auditoría completa](v6-auditoria-completa.md) ([inglés](v6-full-audit.md)) · [Pasada 1 — cambios al esquemático](v6-cambios-esquematico.md) · [Pasada 2 — puertos (inglés)](v6-puertos-sin-nueva-version.md) · [Pasada 3 — revisión final (inglés)](v6-revision-final.md) |
| Históricos | [Próximos pasos para ordenar (29-sep)](v6-proximos-pasos.md) — superado por el plan de fines de semana · [Auditoría V5.6 · V5.7 · v6 del 16-oct (mañana), §8](auditoria-produccion-16oct.md#8-v6-que-falta) · [Revisión contra lecciones V5.5](v6-revision-lecciones-v55.md) — superada por la auditoría completa · [Auditoría pre-fab 25-sep](v6-auditoria-prefab.md) ([inglés](v6-prefab-audit.md)) — superada por la del 28-sep |
| Cantidad | La auditoría de septiembre hablaba de **5 prototipos**; el plan del 13-oct son **130 unidades y todos los reemplazos futuros**. Recomendado: piloto de 20 que pase el banco con el firmware 0.7.x antes de las 130. |
**Siguiente paso** ([plan de fines de semana, rev. 1.4](v6-plan-fines-de-semana.md); recorrido una semana el 19-oct):
**Siguiente paso** ([plan de fines de semana, rev. 1.5](v6-plan-fines-de-semana.md); recorrido una semana el 19-oct):
- **Lun 19 o mar 20:** pre-órdenes en JLC (THVD1410DR, TPS7A2033PDBVR, J14). No se recorren.
- **21-oct:** T25, T28 y T38 aplicados; Leo subió T38 el 22-oct (`b74b96f`).
- **22-oct:** T39 (Claude): bloque A con la propuesta α y MH8 libre; falta el push. Claude evalúa MH4 (T40).
- **Sáb 24:** Michelle revisa el bloque A que dejó T39, hace Rig1 y el `/24V`, MH4 si T40 no lo resolvió, y sigue con el bloque B ([estado, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir)).
- **22-oct:** T39 (Claude): bloque A con la propuesta α y MH8 libre (`d5f5002`). En la tarde, T40: MH4 libre; falta el push. Leo decide D-22-4 (TVS de la SIM junto a J14).
- **Sáb 24:** Michelle revisa el bloque A y MH4 que dejaron T39 y T40, hace Rig1 y el `/24V`, las TVS junto a J14 si Leo elige (b) en D-22-4, y sigue con el bloque B ([estado, §3](v6-estado-21oct.md#3-lo-que-tiene-que-corregir-michelle-antes-de-seguir)).
- **Dom 25:** RF del módem, U20, GNSS y U12.
- **Sáb 31 y dom 1-nov:** pares, puertos, In1, vías de tierra (T29), serigrafía y DRC final. P7/P8 se quitan del esquemático (decisión del 9-oct).
- **Lun 2-nov:** pedido del piloto de 20.

View File

@ -1,19 +1,25 @@
---
title: "Estado de la v6 al 22-oct: T25, T28, T38 y T39 (bloque A) aplicados"
tags: [project-drogheda, hardware, hbox, einarboard, v6, auditoria, t25, t28, t38, t39, courtyards, barrenos, u25, bloque-a, layout, michelle]
title: "Estado de la v6 al 22-oct: T25, T28, T38, T39 (bloque A) y T40 (MH4) aplicados"
tags: [project-drogheda, hardware, hbox, einarboard, v6, auditoria, t25, t28, t38, t39, t40, courtyards, barrenos, mh4, sim, esd, u25, bloque-a, layout, michelle]
project: Project Drogheda
author: Helgen Technologies (revisión: Claude; pedido: Leo)
date: 2026-10-22
version: 1.2
status: ACTIVO — HBox_PCB b74b96f (T25, T28 y T38; Leo, 22-oct 09:42) + T39 (Claude, 22-oct) · falta commit y push de T39
version: 1.3
status: ACTIVO — HBox_PCB d5f5002 (T39; Leo, 22-oct) + T40 (Claude, 22-oct, tarde) · falta commit y push de T40
audience: Leo, Michelle (layout)
---
# Estado de la v6 al 22-oct · T25, T28, T38 y T39 aplicados
# Estado de la v6 al 22-oct · T25, T28, T38, T39 y T40 aplicados
**Pregunta (Leo, 21-oct):** ¿T28 ya quedó o hay que correrlo? ¿Cómo va la v6? Y en la noche: trabaja en la v6 real.
**22-oct, 09:40:** «Haz tú los cambios de la v6; es la prioridad».
!!! note "Rev. 1.3 (22-oct, tarde): T40 dejó libre MH4"
- **T40 sacó de MH4 todo lo que no era GND**, por script. D23 quedó debajo del pin 31 de U19 y D24 junto a R129/R133. `/SIM_CLK` va por In1, `/SIM_RST` por arriba de D25 y `/NTC` ya no cruza el barreno. Las tres TVS de la SIM tienen vía de GND propia, y los pines 92 y 94 de U19 también. Detalle en el [§7](#7-t40-22-oct-tarde-mh4-libre).
- **DRC de KiCad 10:** quedan **11 errores**, los encimes de courtyard que son solo de margen. Hay 0 sin conectar y la misma paridad (19 PWR_FLAG). Salieron 37 violaciones y no entró ninguna.
- **Lo que no hace T40:** las TVS se quedan del lado del módem, no junto a J14 como pide L-38 (B-09). Leo decide si se cambian al lado del zócalo (D-22-4, [§7.2](#72-las-tvs-de-la-sim-se-quedan-del-lado-del-modem-d-22-4)).
- **Falta el commit y el push de T40.** Hasta entonces, Michelle no abre la v6.
!!! note "Rev. 1.2 (22-oct): T39, el bloque A, ya está en la v6"
- **T39 aplicó la propuesta α del bloque A por script.** Leo lo pidió el 22-oct como prioridad. D34 quedó entre U25 y L22, L22 al este y R53/R54 junto a U6. El nodo SW y `/VEH5V` van por F.Cu. In1 e In2 ya no llevan pistas bajo la fuente, y MH8 quedó libre. Detalle en el [§6](#6-t39-22-oct-el-bloque-a-por-script).
- **DRC de KiCad 10, con relleno y paridad:**
@ -28,7 +34,8 @@ audience: Leo, Michelle (layout)
**Respuesta corta (22-oct):**
- **T25, T28 y T38** ya están en HBox_PCB: `b74b96f`, que Leo subió el 22-oct a las 09:42.
- **T25, T28 y T38** ya están en HBox_PCB: `b74b96f`, que Leo subió el 22-oct a las 09:42. **T39** también: `d5f5002`.
- **T40 (22-oct, tarde) dejó libre MH4** ([§7](#7-t40-22-oct-tarde-mh4-libre)). Con eso los 5 barrenos quedan libres y el DRC ya no tiene errores de barreno, de máscara ni de courtyard con barreno.
- **T39 (22-oct) hizo el bloque A** con la propuesta α ([§6](#6-t39-22-oct-el-bloque-a-por-script)):
- el cátodo de D34 quedó a 3.52 mm del pin SW (antes 13.0) y L22.1 a 8.79 mm;
- el nodo SW tiene 9.8 mm² de cobre fuera de los pads;
@ -36,11 +43,13 @@ audience: Leo, Michelle (layout)
- MH8 quedó libre y ya no están los 3 encimes reales: C153 y C154 bajo L22, y R53 sobre U25;
- In1 e In2 ya no llevan pistas de señal bajo el nodo SW, L22 ni la entrada de U25.
- **Del bloque A queda poco:** Rig1 a ≤ 3 mm de P1.3 (está a 4.79 mm) y decidir el `/24V` sin montar (pasos 5 y 6 del plan).
- **La v6 todavía no se puede pedir.** Falta MH4, que Claude evalúa como T40, y siguen los bloques B a H del plan.
- **DRC de hoy, con T39:**
- 44 errores: ≈ 33 de MH4 y 11 encimes de courtyard que son solo de margen; esos se excluyen al final;
- **La v6 todavía no se puede pedir.** Siguen los bloques B a H del plan, y D-22-4 (las TVS de la SIM junto a J14).
- **DRC de hoy, con T39 y T40:**
- 11 errores, los encimes de courtyard que son solo de margen; esos se excluyen al final;
- 0 sin conectar y la paridad solo con los 19 PWR_FLAG;
- 254 avisos, 8 menos que antes.
- 249 avisos, 5 menos que con T39.
![v6 con T40: el barreno MH4 libre](img/2026-10-22_Drogheda_v6-T40-MH4_v1.0.png)
![v6 con T39: el bloque A, la fuente U25, por script](img/2026-10-22_Drogheda_v6-T39-Bloque-A_v1.0.png)
@ -48,7 +57,7 @@ audience: Leo, Michelle (layout)
![v6 al 21-oct: la fuente U25 y los barrenos](img/2026-10-21_Drogheda_v6-Estado-U25-Barrenos_v1.0.png)
## 1. T25, T28 y T38: qué cambiaron hoy
## 1. T25, T28, T38, T39 y T40: qué cambiaron
| Script | Qué cambió en `hbox-watchdog-v6` |
|---|---|
@ -56,6 +65,7 @@ audience: Leo, Michelle (layout)
| **T28** | U25 sin el código del AP64350: queda solo LCSC C524806 (TPS54360B) · pasta del EP de U6 al 50 % (6 ventanas de 0.95 × 0.85 mm) · conexión sólida a la zona en 114 pads de tierra de potencia y RF · stackup JLC04161H-7628 con ENIG y control de impedancia · cajetín «EinarBoard v6 (HBOX 6.0)», rev 6.0, y textos de serigrafía · clase RF a 0.35 mm en el `.kicad_pro`. |
| **T38** (21-oct, noche) | Courtyard correcto en U19, U20, U23, U24, U25, U26, D22, D27, J12, J13 y L22, en la tarjeta y en `Drogheda-v6.pretty` · `/3V3` de MH6 por B.Cu, a 4.06 mm del centro · regla nueva en el `.kicad_dru`: en F.Cu y B.Cu, solo GND a menos de 4 mm del centro de un barreno. No toca el `.kicad_pro`. |
| **T39** (22-oct) | Bloque A con la propuesta α: mueve D34, L22, R53 y R54 · nodo SW y `/VEH5V` con pistas anchas de F.Cu · `/I2C_SDA`, `/I2C_SCL` y la toma de 5 V de R121 pasan de In1/In2 a B.Cu · borra 28 tramos y 5 vías (2 de ellas dentro de MH8) y agrega 9 vías y 19 tramos. Solo cambia el `.kicad_pcb`; ver [§6](#6-t39-22-oct-el-bloque-a-por-script). |
| **T40** (22-oct, tarde) | MH4 libre: mueve D23 y D24 · `/SIM_CLK` por In1 · `/SIM_RST` por F.Cu arriba de D25, con vía nueva · `/NTC` (In2) rodea el barreno · vías de GND para D23, D24, D25 y los pines 92/94 de U19 · borra 17 tramos y 2 vías, acorta 2 tramos y agrega 7 vías y 17 tramos. Solo cambia el `.kicad_pcb`; ver [§7](#7-t40-22-oct-tarde-mh4-libre). |
**Cómo se probó.**
@ -64,24 +74,25 @@ audience: Leo, Michelle (layout)
- con los scripts: 22 errores, todos en MH4 y MH8; 0 sin conectar; la paridad igual (19 PWR_FLAG).
- T38: igual, sobre una copia de la v6 ya con T25 y T28. Después se aplicó a la v6 de HBox_PCB y el resultado es idéntico, byte por byte, a la copia probada.
- T39: igual, sobre una copia de `b74b96f`. Después se aplicó a la v6 de HBox_PCB y se rellenó con `kicad-cli` y el proyecto cargado; el archivo es idéntico, byte por byte, a la copia probada (`97ea7e45`).
- T40: igual, sobre una copia de la v6 ya con T39 (`97ea7e45`, la de `d5f5002`). Aplicado y rellenado en HBox_PCB: idéntico a la copia probada (`a273d44c`).
- Correr T28 o T38 otra vez no cambia nada, y T25 se niega si los barrenos ya están.
!!! warning "Antes de que Michelle vuelva a abrir la v6"
T39 (22-oct) cambia `hbox-watchdog-v6.kicad_pcb`, igual que antes T38 cambió el `.kicad_pcb`, el `.kicad_dru` y 8 archivos de `Drogheda-v6.pretty`. Git ya no mezcla archivos de KiCad (`merge=binary`): si Michelle guarda la v6 sobre su copia anterior y la sube, se pierde uno de los dos trabajos. El orden es: **Leo sube → Michelle hace `git pull --ff-only` → abre la v6.**
T40 (22-oct, tarde) cambia `hbox-watchdog-v6.kicad_pcb`, igual que T39 y que antes T38, que cambió el `.kicad_pcb`, el `.kicad_dru` y 8 archivos de `Drogheda-v6.pretty`. Git ya no mezcla archivos de KiCad (`merge=binary`): si Michelle guarda la v6 sobre su copia anterior y la sube, se pierde uno de los dos trabajos. El orden es: **Leo sube → Michelle hace `git pull --ff-only` → abre la v6.**
## 2. Los 8 bloqueantes del 16-oct, hoy
Medido en el cobre con las zonas rellenadas. «19-oct» = `00a9af4`; «hoy» = `b74b96f` (T25, T28 y T38) con T39.
Medido en el cobre con las zonas rellenadas. «19-oct» = `00a9af4`; «hoy» = `d5f5002` (T25, T28, T38 y T39) con T40.
| # | Bloqueante del 16-oct | 19-oct | Hoy | Estado |
|---|---|---|---|---|
| 1 | U25 con el código del AP64350 | igual | solo C524806 | ✅ T28 |
| 2 | Sin barrenos | sin barrenos | 5 barrenos · MH5, MH6, MH7 y MH8 libres (MH6 con T38, MH8 con T39) · **MH4 con piezas y cobre dentro** (≈ 33 errores) | 🟠 liberar MH4 |
| 2 | Sin barrenos | sin barrenos | **5 barrenos libres**: MH6 con T38, MH8 con T39 y MH4 con T40. En las caras, dentro de 4 mm del centro solo queda GND | ✅ T25 + T38–T40 |
| 3 | 5 V por capas internas | `/VEH5V` 12 mm por In1 a 0.5 mm; `/VIN_SW` 22 mm por In2 | `/VEH5V` de L22.2 a R51.1 **de 2 mm por F.Cu** (T39); de R51.1 a RS1 sigue de 1.0 mm, sin vías (meta ≥ 2 mm). La toma de 5 V de R121 ya no va por In1. `/VIN_SW` de RS1 a Q7 **igual**: 22 mm por In2 a 0.5–0.8 mm, con 2 vías; Q7 sigue a 10.6 mm de RS1 | 🟠 mitad |
| 4 | Cargador U6 | `/SW_BAT` y `/BAT_PLUS` de 0.5 mm; 1 vía en el EP | `/SW_BAT` de 0.7–0.75 mm (meta: polígono ≥ 2 mm); `/BAT_PLUS` igual; 1 vía en el EP (meta 9) | ❌ |
| 5 | Fuente U25 | L22 a 13 mm; EP sin vías | **D34 a 3.52 mm del pin SW** ✅ · L22.1 a 8.79 mm ✅ · nodo SW 9.8 mm² fuera de los pads ✅ · lazo por centros 56.5 mm² con C149 (60.8 con C150) ✅ · 6 vías en el EP y 4 en el ánodo de D34 ✅ · C149 a 0.75 mm de VIN ✅ · C153 y C154 fuera de L22 ✅ · faltan Rig1 (4.79 mm de P1.3) y el `/24V` sin montar | ✅ T39 (con las metas del 21-oct) |
| 6 | RF LTE por In1 | 21.9 mm por In1 | igual | ❌ |
| 7 | Vías de GND | 96 (meta ≥ 400) | 116 (T39: 7 nuevas y 2 menos en MH8): 1 bajo U19 y 0 bajo U18; In1 con 1 587 mm de pistas (69 menos) | ❌ |
| 7 | Vías de GND | 96 (meta ≥ 400) | 119 (T39: 7 nuevas y 2 menos en MH8; T40: 4 nuevas y 1 menos): 1 bajo U19 y 0 bajo U18; In1 con 1 595 mm de pistas (T40 le sumó 8 mm de `/SIM_CLK`) | ❌ |
| 8 | Stackup genérico | igual | JLC04161H-7628, ENIG, control de impedancia | ✅ T28 |
**Lo demás** (bloques C a H: módem, GNSS, U12, pares diferenciales, puertos, In1, serigrafía) no cambió desde el 19-oct.
@ -105,7 +116,7 @@ Después del `git pull`. Primero esto y luego el resto de los bloques A y B del
2. **R53/R54 quedaron abajo de JP2.** Están a 3.4 mm de U6.10 y a 6–7 mm de U6.18, no a ≤ 3 mm: más cerca de U6.18 no hay lugar sin mover piezas del bloque B.
3. **El nodo SW y `/VEH5V` son pistas anchas, no zona** ([§6.3](#63-por-que-pistas-y-no-zona)).
**Para Michelle queda:** MH4 (si T40 no lo resuelve), Rig1 y el `/24V` del bloque A, y luego el bloque B del plan.
**Para Michelle queda:** Rig1 y el `/24V` del bloque A, y luego el bloque B del plan. MH4 ya quedó con T40 ([§7](#7-t40-22-oct-tarde-mh4-libre)); si Leo elige la opción (b) de D-22-4, también las TVS de la SIM junto a J14.
![v6 con T38: courtyards reales, la fuente U25 y los barrenos](img/2026-10-21_Drogheda_v6-T38-Courtyards-U25-Barrenos_v1.0.png)
@ -151,10 +162,10 @@ Después del `git pull`. Primero esto y luego el resto de los bloques A y B del
### 3.2 Barrenos
1. **MH8 (195.2, 135.3):** ✅ ya quedó con T39 (22-oct). D34 salió de ahí y se borraron las 2 vías de GND de (194.6, 133.7) y (196.2, 133.7): 0 errores en MH8.
2. **MH4 (185.2, 90.3):** igual que el 16-oct ([plan, §3.4](v6-plan-fines-de-semana.md#34-barrenos-mh4-y-mh6-1-h)). Claude lo evalúa como T40 (22-oct). La regla nueva hace que el DRC ya muestre todo lo que estorba: ≈ 33 errores con KiCad 10. El número cambia un poco de una corrida a otra; ver el [§6.4](#64-drc-con-t39).
- D23, D24 y D25 junto a J14 y sus resistencias de 22 Ω;
- `/NTC` (In2) a ≥ 2.5 mm del centro;
- `/SIM_RST`, `/SIM_CLK`, `/SIM_DATA` y la pista GND fuera del círculo de 4 mm.
2. **MH4 (185.2, 90.3):** ✅ ya quedó con T40 (22-oct, tarde; [§7](#7-t40-22-oct-tarde-mh4-libre)). Lo que pedía el plan:
- `/NTC` (In2) a ≥ 2.5 mm del centro: ✅ pasa a 2.55 mm del centro, 0.95 mm de la orilla del barreno;
- `/SIM_RST`, `/SIM_CLK`, `/SIM_DATA` y la pista GND fuera del círculo de 4 mm: ✅ en F.Cu y B.Cu solo queda GND;
- D23, D24 y D25 junto a J14 y sus resistencias de 22 Ω: **no**. Salieron del barreno, pero se quedan del lado del módem. Lo decide Leo (D-22-4, [§7.2](#72-las-tvs-de-la-sim-se-quedan-del-lado-del-modem-d-22-4)).
3. **MH6 (65.2, 135.3):** ✅ ya quedó con T38.
4. **No mover los barrenos:** MH4–MH6 son los de la placa base de la V5.5, y MH7 y MH8 son las esquinas de la v6.
@ -168,9 +179,9 @@ Commit y push con el bloque en el mensaje.
## 4. Calendario
El paso del sábado 24 a las 8:00 (Leo corre T25 y T28) ya está hecho, y también MH6, MH8 y la colocación del bloque A (T39). Lo demás del [plan de fines de semana](v6-plan-fines-de-semana.md) sigue igual:
El paso del sábado 24 a las 8:00 (Leo corre T25 y T28) ya está hecho, y también MH6, MH8, la colocación del bloque A (T39) y MH4 (T40). Lo demás del [plan de fines de semana](v6-plan-fines-de-semana.md) sigue igual:
- sábado 24: lo que queda del bloque A (Rig1 y `/24V`) y el bloque B;
- sábado 24: lo que queda del bloque A (Rig1 y `/24V`), el bloque B y, si Leo elige la opción (b) de D-22-4, las TVS de la SIM junto a J14;
- domingo 25: bloques C, D y E;
- entre semana: T29;
- lunes 2-nov: pedido del piloto.
@ -362,17 +373,105 @@ KiCad 10.0.4, `pcb drc --schematic-parity --refill-zones --severity-all --all-tr
3. **Rig1 y el `/24V` sin montar** (pasos 5 y 6 del bloque A) siguen como estaban.
4. **Michelle:** al abrir la v6, revisar el bloque A y ajustar lo que no le guste. En ese bloque el DRC ya está limpio.
## 7. T40 (22-oct, tarde): MH4 libre
Script: `kicad/tools/t40_v6_mh4.py`.
- Requiere T39.
- Antes de escribir revisa que ninguna de las piezas y pistas que toca se haya movido.
- Si ya se aplicó, no hace nada.
- Guarda un respaldo `.T40bak`.
![v6 con T40: el barreno MH4 libre](img/2026-10-22_Drogheda_v6-T40-MH4_v1.0.png)
### 7.1 Qué cambió
| Qué | Antes (T39) | Con T40 |
|---|---|---|
| D23 (TVS de `/SIM_DATA`) | (182.7, 90.5): su pad de GND quedaba dentro del barreno y el de `/SIM_DATA` a 3.6 mm del centro | **(179.25, 87.0)**, girado 90°, debajo de U19.31: `/SIM_DATA` llega directo del pin (1.95 mm; antes 7.5 mm de pista) |
| D24 (TVS de `/SIM_CLK`) | (182.7, 88.5), con el barreno dentro de su courtyard | **(188.15, 87.6)**, girado 90°, entre la vía de `/SIM_CLK` y R129/R133 |
| D25 (TVS de `/SIM_RST`) | (182.7, 86.5) | **no se mueve** |
| `/SIM_CLK` | por F.Cu en y = 87.75, a 2.55 mm del centro y dentro de la máscara abierta | baja a **In1** junto a U19.32 y corre en y = 86.7 hasta una vía entre D24 y R129 |
| `/SIM_RST` | por F.Cu en y = 87.25 hasta una vía a 3.94 mm del centro (regla 4.0) | sale de D25 por **F.Cu en y = 85.95**, por arriba de D25 (a 4.2 mm del centro), hasta una vía nueva en (187.05, 85.95); la diagonal de In1 se acorta 0.42 mm |
| `/NTC` (In2) | en y = 89.7: **cruzaba por dentro del barreno** | dobla antes, en (179.8, 87.5), y pasa en y = 87.5: a 0.95 mm de la orilla del barreno |
| Pines 92 y 94 de U19 (GND) | al relleno de F.Cu y a la vía de (183.0, 85.5) | pista de GND de 0.25 mm en y = 85.35, bajo U19.93, y **vía nueva en (186.6, 84.6)** |
| Vías de GND de las TVS | ninguna propia | D23 a 0.8 mm · D24 a 1.1 mm · D25 a 2.3 mm, con pista directa |
| Referencias en serigrafía | D23 y D24 dentro de la máscara de MH4 | D23 a la izquierda de D23, D24 arriba de D24; la de R129 (DNP) pasa a la derecha de R129 para no caer sobre D24 |
**Por qué los pines 92 y 94 necesitan su pista.** `/SIM_RST` tiene que pasar entre la fila de pines de U19 y D25: más abajo entra en los 4 mm de MH4. El canal que queda (0.8 mm) es muy angosto para el relleno de GND, que pide 0.5 mm de alivio, 0.3 mm de holgura y 0.25 mm de cobre. Sin la pista nueva, los pines 92 y 94 se quedan sin tierra. Por eso también se borró la vía de GND de (183.0, 85.5), que estorbaba a `/SIM_RST`.
**Holguras más justas de lo nuevo** (regla 0.15 mm):
- `/SIM_RST` a D25.2: 0.20 mm;
- la pista de GND a U19.93 (BT_ANT, sin conectar): 0.225 mm;
- la vía de GND de D23 a `/CHRG_FAULT` (In1): 0.23 mm;
- `/NTC` a las vías de `/SIM_CLK` y de D25: 0.25 mm.
### 7.2 Las TVS de la SIM se quedan del lado del módem (D-22-4)
El plan (L-38, por B-09 del 28-sep) pide **D23–D25 junto a J14**. T40 no lo hace: las saca del barreno, pero las deja junto a U19 (D23 y D25) y junto a R129/R133 (D24).
- **Por qué no.** En el esquemático, D23–D25 están en `/SIM_DATA`, `/SIM_CLK` y `/SIM_RST`, del lado del módem de las resistencias de 22 Ω. R133 y R134 (las de `/SIM_CLK` y `/SIM_DATA`) están junto a U22, a 13–24 mm de pista de J14, así que esas dos redes no llegan al zócalo. Ponerlas ahí pide una de dos:
- mover también R133 y R134 junto a J14, y volver a rutear las redes;
- o cambiar las TVS a `/SIMB_*` en el esquemático.
- **Qué protege hoy.** Una descarga en el zócalo recorre **27–41 mm de pista**, y pasa por la resistencia de 22 Ω, antes de llegar a la TVS: 28.6 mm en DATA, 27.1 en CLK y 40.6 en RST (antes de T40: 34.1, 32.2 y 41.2). El módem queda protegido, pero el pico entra a la tarjeta y la resistencia lo aguanta completo.
- **Por qué importa.** El 28-sep se dijo que no hacía falta un arreglo de ESD para `/SIM_VDD` porque la SIM se pone una vez, dentro del gabinete, **y** porque D23–D25 iban a quedar junto a J14. La segunda razón ya no se cumple.
**D-22-4 (decide Leo):**
- **(a) Se quedan así.** No cuesta nada más y el módem queda protegido.
- **(b) Recomendada: TVS del lado del zócalo.** D23–D25 pasan a `/SIMB_DATA`, `/SIMB_CLK` y `/SIMB_RST`, pegadas a los pines de J14 y cada una con su vía de GND. Son las mismas piezas: el BOM no cambia. Es un cambio chico de esquemático, más la colocación. Claude lo puede hacer como T41 (esquemático y tarjeta por script), o Michelle el sábado.
Recomiendo (b): la SIM se pone a mano, en el taller o en el sitio, casi siempre sin pulsera antiestática, y un módem dañado es una vuelta al sitio. La TVS tiene que quedar donde entra la descarga.
### 7.3 DRC con T40
| | Con T39 (misma corrida) | Con T40 |
|---|---|---|
| Errores | 43: 13 de holgura de barreno · 17 de máscara · 2 de barreno en courtyard · 11 encimes de margen | **11**: los encimes de margen |
| Sin conectar | 0 | 0 |
| Paridad | 19 PWR_FLAG | 19 PWR_FLAG |
| Avisos | 254 | 249 (5 menos de serigrafía sobre la máscara de MH4) |
KiCad 10.0.4, `pcb drc --schematic-parity --refill-zones --severity-all --all-track-errors`. En esta corrida, la v6 con T39 dio 13 errores de holgura de barreno en MH4. En la del §6.4 dio 14: el número cambia de una corrida a otra.
- **Se van 37 violaciones y no entra ninguna.** Son todas las de MH4: holgura de barreno y regla T38, máscara, barreno en el courtyard de D23 y D24, y la serigrafía de D23 y D24.
- **El relleno solo cambia junto a MH4.** Fuera de una ventana de 29 × 24 mm alrededor del barreno, los rellenos de las 4 capas son idénticos a los de T39.
- **Conectividad.** `/SIM_CLK`, `/SIM_RST`, `/SIM_DATA`, `/SIM_VDD` y `/NTC` siguen en un solo grupo cada una. Los pines 29, 92 y 94 de U19 y los pads de GND de las tres TVS quedan unidos a GND: 0 sin conectar y ningún alivio térmico incompleto.
### 7.4 Cómo se probó y lo que queda a la vista
**Prueba.**
- Copia de la v6 con T39 (`97ea7e45`) en la Mac, con su `.kicad_pro`, `.kicad_dru`, esquemático y bibliotecas. T40 en la copia, y DRC de la copia contra la v6 sin T40.
- Antes de escribir, el script revisa:
- la holgura de todo lo nuevo, movido o acortado contra el cobre de las otras redes;
- en todas las capas, 0.25 mm de la orilla del barreno, y la regla de 4 mm en las caras;
- que dentro de 4 mm de MH4, en las caras, ya no quede nada que no sea GND;
- que los courtyards de D23 y D24 no se encimen con nadie ni encierren un barreno;
- que no se pierdan UUIDs.
- **Aplicación.** Se aplicó a la v6 de HBox_PCB y se rellenó con `kicad-cli`, con el proyecto al lado (regla D-22-3). El archivo es idéntico, byte por byte, a la copia probada (`a273d44c`).
**Lo que queda a la vista:**
1. **D-22-4:** las TVS junto a J14 ([§7.2](#72-las-tvs-de-la-sim-se-quedan-del-lado-del-modem-d-22-4)).
2. **D25 quedó con su vía de GND a 2.3 mm** (antes había una a 1.25 mm, por el relleno). Ahora llega por una pista directa de 0.4 mm. Más cerca no cabe: arriba pasa `/SIM_RST` y abajo, en In1/In2, `/SIM_CLK` y `/NTC`.
3. **`/SIM_CLK` suma 8.7 mm por In1**, que es plano de tierra. Corre entre el GND de F.Cu y el de In2, y encima, en F.Cu, solo hay pistas de la SIM y de GND.
4. **La pista de GND bajo U19.93** (BT_ANT, sin usar) queda a 0.225 mm de su pad, bajo máscara.
## Cómo se hizo
- **Archivos:**
- HBox_PCB `9255b84` (Michelle, 21-oct 18:14) contra `00a9af4` (19-oct).
- Para T39: `b74b96f` (22-oct 09:42).
- Para T40: la v6 con T39 (`97ea7e45`, commit `d5f5002`).
- El esquemático de la v6 no cambió desde el 28-sep (`d58a89ba…`).
- **Scripts:** primero en una copia y luego en los archivos de HBox_PCB. Los respaldos `.T25bak`, `.T28bak`, `.T38bak` y `.T39bak` no se suben.
- **Scripts:** primero en una copia y luego en los archivos de HBox_PCB. Los respaldos `.T25bak`, `.T28bak`, `.T38bak`, `.T39bak` y `.T40bak` no se suben.
- `kicad/tools/t25_mounting_holes_v55.py --set v6`;
- `kicad/tools/t28_v6_mecanicos.py`;
- `kicad/tools/t38_v6_courtyards_mh.py`;
- `kicad/tools/t39_v6_bloque_a.py`.
- `kicad/tools/t39_v6_bloque_a.py`;
- `kicad/tools/t40_v6_mh4.py`.
- **DRC:** KiCad 10.0.4, `pcb drc --schematic-parity --refill-zones --severity-all`, con el `.kicad_dru` del proyecto.
- **Medidas:**
- El script de la auditoría del 16-oct sobre el cobre rellenado: distancias, series de cobre, vías por red y por EP, y lazo por centros de pad.

View File

@ -4,8 +4,8 @@ tags: [project-drogheda, hardware, hbox, einarboard, v6, plan, layout, firmware,
project: Project Drogheda
author: Helgen Technologies (plan: Claude; revisión: Leo)
date: 2026-10-16
version: 1.4
status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00 · 21-oct: T25 y T28 ya aplicados · 21-oct noche: T38 aplicado y metas del bloque A corregidas (§3.1) · 22-oct: T39 hizo la colocación del bloque A y liberó MH8
version: 1.5
status: ACTIVO — rev. 1.1 (19-oct): recorrido una semana; empieza el sábado 24-oct a las 8:00 · 21-oct: T25 y T28 ya aplicados · 21-oct noche: T38 aplicado y metas del bloque A corregidas (§3.1) · 22-oct: T39 hizo la colocación del bloque A y liberó MH8 · 22-oct, tarde: T40 liberó MH4
audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana (PM)
---
@ -35,6 +35,14 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
Del §3.1 solo quedan Rig1 (paso 5) y el `/24V` sin montar (paso 6). Del §3.2, el paso 6 quedó a medias: R53/R54 ya están junto a U6, pero a 6–7 mm de U6.18. El sábado 24 empieza con eso y con el bloque B.
!!! success "Rev. 1.5 (22-oct, tarde): T40 liberó MH4"
**T40 sacó de MH4 todo lo que no era GND** ([estado de la v6, §7](v6-estado-21oct.md#7-t40-22-oct-tarde-mh4-libre)). Con eso, los 5 barrenos están libres y el DRC de la v6 solo tiene los 11 encimes de courtyard que son de margen.
- D23 quedó debajo de U19.31 y D24 junto a R129/R133. D25 no se movió, y las tres tienen vía de GND propia.
- `/SIM_CLK` va por In1, `/SIM_RST` por arriba de D25 y `/NTC` rodea el barreno.
**Lo que no hace:** llevar D23–D25 junto a J14 (§3.4, por B-09/L-38). Leo decide (D-22-4): (a) se quedan del lado del módem, o (b) se pasan a `/SIMB_*`, pegadas al zócalo. Claude recomienda (b).
**Dos metas:**
| Meta | Qué significa | Fecha objetivo |
@ -59,7 +67,7 @@ audience: Leo, Michelle (layout), Jesús (banco), Aldair (fabricación), Adriana
| Día | Quién | Qué | Listo cuando |
|---|---|---|---|
| **Sáb 24-oct, 8:00** | Leo | Correr T25 `--set v6` + T28 y subir (§2). **Ya se corrieron el 21-oct: falta el push** | push hecho; Michelle avisada |
| **Sáb 24-oct** | Michelle | **Bloques A + B:** el camino de la energía (U25, RS1, Q7, U6, L6) + liberar MH4 y MH6 (§3) | criterios del §3.5 |
| **Sáb 24-oct** | Michelle | **Bloques A + B:** el camino de la energía (U25, RS1, Q7, U6, L6). MH4, MH6 y MH8 ya quedaron (T38–T40); las TVS de la SIM junto a J14 si Leo elige (b) en D-22-4 (§3) | criterios del §3.5 |
| **Dom 25-oct** | Michelle | **Bloque C:** módem (RF LTE, U20, VBAT). **Bloque D:** GNSS. **Bloque E:** U12 (§4) | criterios del §4.4 |
| **Dom 25-oct, noche** | Claude | Re-auditoría del commit del domingo; lista corta para el lunes | lista enviada |
| **Lun 26 – vie 30** | Leo / Adriana | Decisiones D-v6-1…6 y cotización (§5). Las **pre-órdenes en JLC** no esperan: lun 19 o mar 20-oct | pre-órdenes pagadas |
@ -165,10 +173,10 @@ Qué cambia T28: U25 sin el código del AP64350 · pasta del EP de U6 al 50 % ·
![Barrenos: qué hay que mover en MH4 y MH6](img/2026-10-16_Drogheda_v6-Barrenos_v1.0.png)
1. **MH4 (185.2, 90.3):**
- Llevar **D23, D24 y D25** (ESD de la SIM) junto a J14 y sus resistencias de 22 Ω (R131–R134): ahí deben ir de todos modos (B-09) y así salen del barreno.
- Desviar `/NTC` (In2) a **≥ 2.5 mm del centro**.
- Sacar `/SIM_CLK`, `/SIM_RST` y la pista GND del **círculo de 4 mm**.
1. **MH4 (185.2, 90.3):** ✅ con T40 (22-oct, tarde; [estado, §7](v6-estado-21oct.md#7-t40-22-oct-tarde-mh4-libre)).
- Desviar `/NTC` (In2) a **≥ 2.5 mm del centro**: ✅ a 2.55 mm, 0.95 mm de la orilla del barreno.
- Sacar `/SIM_CLK`, `/SIM_RST` y la pista GND del **círculo de 4 mm**: ✅ en las caras solo queda GND.
- Llevar **D23, D24 y D25** (ESD de la SIM) junto a J14 y sus resistencias de 22 Ω (B-09): **queda abierto.** T40 las sacó del barreno, pero del lado del módem. Leo decide D-22-4 ([estado, §7.2](v6-estado-21oct.md#72-las-tvs-de-la-sim-se-quedan-del-lado-del-modem-d-22-4)): (a) se quedan así, o (b) se pasan a `/SIMB_*`, pegadas a J14, con su vía de GND (recomendada; T41 o Michelle el sábado).
2. **MH6 (65.2, 135.3):** ✅ ya quedó con T38 (21-oct, noche): `/3V3` por B.Cu, a 4.06 mm del centro. Si la rondana no cabe, correr H4 1 mm al este.
3. **MH8 (195.2, 135.3):** ✅ con T39 (22-oct): D34 salió de ahí y se borraron las 2 vías de GND de (194.6, 133.7) y (196.2, 133.7).
4. **Regla nueva (T38):** el DRC ya marca cualquier cobre que no sea GND a menos de 4 mm del centro de un barreno, en F.Cu y B.Cu. En MH4 marca también `/SIM_DATA`.
@ -176,7 +184,7 @@ Qué cambia T28: U25 sin el código del AP64350 · pasta del EP de U6 al 50 % ·
### 3.5 Cierre del sábado
- [ ] DRC: **0 errores** y 0 sin conectar. Los de MH8 y el encime de C153 con L22 ya se fueron con T39; faltan los ≈ 33 de MH4. Solo quedan los encimes de courtyard que son de margen: se excluyen al final ([estado, §3.3](v6-estado-21oct.md#33-cierre)).
- [ ] DRC: **0 errores** y 0 sin conectar. Los de MH8 y el encime de C153 con L22 ya se fueron con T39, y los de MH4 con T40. Solo quedan los encimes de courtyard que son de margen: se excluyen al final ([estado, §3.3](v6-estado-21oct.md#33-cierre)).
- [ ] Ningún aviso de ancho de pista en `/VEH5V`, `/VIN_SW`, `/MAIN5V`, `/SW_BAT`, `/BAT_PLUS`, `/VIN_PROT`.
- [ ] Nada de potencia en In1/In2 entre L22, RS1, Q7, U6, L6 y P3.
- [ ] Vías: EP de U6 = 9, EP de U25 = 6, D34 ≥ 4.

View File

@ -266,18 +266,33 @@
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 }));
if (s.meta) li.append(el('a', { class: 'in-sit-meta', href: '/tableros/instalaciones/?sitio=' + encodeURIComponent(s.id), title: 'Instalaciones de ' + s.nombre, text: 'Meta: ' + s.meta + ' · ver instalaciones' }));
ul.append(li);
return estado;
};
(P.sitios || []).forEach(s => {
const estado = fila(s);
json('/api/sitio/unidades?id=' + encodeURIComponent(s.id)).then(j => {
// Conteos por sitio: los mismos del tablero de Instalaciones (solo las probadas cuentan para la meta).
// Si el servicio todavía no tiene /api/instalaciones (falta reiniciar actas-hbox), se usa el conteo anterior.
const porSitio = json('/api/instalaciones').then(j => j.resumen || {}).catch(() => null);
const conteoAnterior = (s, estado) => json('/api/sitio/unidades?id=' + encodeURIComponent(s.id)).then(j => {
const n = j.total_unidades || 0, inst = (j.unidades || []).filter(u => u.instalada).length;
const partes = [];
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(' · ');
});
(P.sitios || []).forEach(s => {
const estado = fila(s);
porSitio.then(R => {
if (!R) return conteoAnterior(s, estado);
const r = R[s.id] || {};
const partes = [];
if (s.nota) partes.push(s.nota);
if (r.unidades) {
let t = plural(r.instaladas || 0, 'instalada', 'instaladas') + ', ' + plural(r.probadas || 0, 'probada', 'probadas');
if (r.falla) t += ', ' + plural(r.falla, 'con falla', 'con falla');
partes.push(t);
} else partes.push('sin unidades registradas');
estado.textContent = partes.join(' · ');
}).catch(() => { estado.textContent = s.nota || ''; });
});
if (P.oficina && P.oficina.id) fila(P.oficina, true);

View File

@ -10,7 +10,7 @@
'prueba-v5': 'tarjeta V5', 'prueba-v4': 'tarjeta V4', 'modulo-ard': 'ArduSimple', 'modulo-4g': 'módem 4G',
'integracion': 'integración', 'ensamble': 'ensamble', 'reflasheo': 'reflasheo', 'prueba-ethernet-serie': 'Ethernet y serie',
'envio': 'envío', 'recepcion': 'recepción', 'instalacion-campo': 'instalación', 'diagnostico-campo': 'diagnóstico en campo',
'diagnostico-banco': 'diagnóstico en banco', 'dano': 'daño', 'prueba-bateria-modem': 'prueba de batería del módem'
'diagnostico-banco': 'diagnóstico en banco', 'dano': 'daño', 'prueba-bateria-modem': 'prueba de batería del módem', 'prueba-sitio': 'prueba en sitio'
};
const CAMPOS = { titulo: 'el título', descripcion: 'la descripción', pista: 'la pista', responsable: 'el responsable',
fecha_limite: 'la fecha', version_tarjeta: 'la tarjeta', enlaces: 'las ligas', etiquetas: 'las etiquetas',

View File

@ -15,6 +15,7 @@ Las páginas que abres para saber **dónde está el proyecto hoy**: qué pendien
| **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é |
| En qué **vehículo** está cada unidad y si ya pasó la **prueba en sitio** | [Instalaciones](instalaciones.md) | Una tarjeta por unidad en cada sitio: enviada, recibida, instalada, probada en sitio y en operación, más «con falla»; vehículo (número económico, tipo, 12/24 V), sus actas y el botón de la siguiente acta. Solo las probadas cuentan para la meta de 1–2 por sitio |
| 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 |

View File

@ -37,6 +37,9 @@ 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 (14:30)</b> — <b>Instalaciones por sitio y prueba en sitio</b> (D-22-5; Leo: hay 3 o 4 unidades instaladas sin saber en qué vehículo, y metas de instalación sin prueba). Una unidad cuenta para la meta de 1–2 por sitio <b>solo con el acta «Prueba en sitio» aprobada</b>: fix RTK 4 (o 5 explicado) en el monitor, reporte en el monitor y en Wialon, posición coherente, respaldo en batería si aplica y fotos. Pasa a «en operación» 7 días después de la prueba si no se registra una falla (los 7 días los propone Claude; se cambian en <code>backend/instalaciones.py</code>). El acta de instalación registra el vehículo: número económico, tipo (RTG, Reach Stacker, Empty Handler, Tractoplana, Montacargas…) y 12/24 V. <a href="/tableros/instalaciones/">Instalaciones</a> · <a href="/formularios/prueba-sitio.html">Prueba en sitio</a></li>
<li><b>22-oct (14:10)</b> — <b>v6: T40, MH4 libre</b> (Claude). D23 quedó debajo de U19.31 y D24 junto a R129/R133. <code>/SIM_CLK</code> va por In1, <code>/SIM_RST</code> por arriba de D25 y <code>/NTC</code> alrededor del barreno; antes lo cruzaba por In2. Las tres TVS de la SIM tienen vía de GND propia. Los pines 92 y 94 de U19 también, porque el canal de <code>/SIM_RST</code> los dejaba sin relleno. DRC: <b>11 errores</b>, los encimes de margen; 0 sin conectar.
<b>D-22-4 (decide Leo):</b> las TVS siguen del lado del módem, a 27–41 mm de pista del zócalo. Claude recomienda (b): pasarlas a <code>/SIMB_*</code>, pegadas a J14 y cada una con su vía de GND. Son las mismas piezas; lo hace T41 o Michelle el sábado. Falta el push de Leo. <a href="/hardware/v6-estado-21oct/#7-t40-22-oct-tarde-mh4-libre">T40</a> · <a href="/hardware/v6-estado-21oct/#72-las-tvs-de-la-sim-se-quedan-del-lado-del-modem-d-22-4">D-22-4</a></li>
<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>

View File

@ -0,0 +1,150 @@
/* ==========================================================================
Tablero de Instalaciones por sitio (22-oct-2026)
tableros/instalaciones.md + instalaciones.js · datos: GET /api/instalaciones
Todo bajo .tb-ins; colores de los tokens --hg-* (claro y oscuro salen solos).
El color de cada sitio es su semáforo de la página de inicio (proyecto.json).
========================================================================== */
.tb-ins { --ins-c: var(--hg-linea-2); --ins-bg: var(--hg-pend-bg); }
.tb-ins .est-rojo { --ins-c: var(--hg-rojo); --ins-bg: var(--hg-rojo-bg); }
.tb-ins .est-amarillo { --ins-c: var(--hg-ambar); --ins-bg: var(--hg-ambar-bg); }
.tb-ins .est-verde { --ins-c: var(--hg-verde); --ins-bg: var(--hg-verde-bg); }
/* ---- Pestañas de sitio (mismo control que el filtro de sitio de Pendientes) ---- */
.tb-ins .ins-tabs { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px; padding: 3px; margin: 0 0 16px;
border: 1px solid var(--hg-linea-2); border-radius: 10px; background: var(--hg-plano-2); }
.tb-ins .ins-tabs button { background: transparent; border: 0; color: var(--hg-texto-2); font-size: 13.5px; font-weight: 500;
padding: 5px 11px; border-radius: 7px; gap: 7px; }
.tb-ins .ins-tabs 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-ins .ins-tabs button:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
.tb-ins .ins-tabs .n { font-weight: 400; color: var(--hg-texto-3); font-variant-numeric: tabular-nums; }
.tb-ins .luz { display: inline-block; width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--ins-c); }
/* ---- Cifras de arriba ------------------------------------------------------ */
.tb-ins .ins-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin: 0 0 16px; }
.tb-ins .ins-kpi { background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 12px; padding: 11px 14px; }
.tb-ins .ins-kpi .n { font-family: var(--hg-fuente-titulo); font-size: 28px; font-weight: 600; line-height: 1; color: var(--hg-texto-fuerte); font-variant-numeric: tabular-nums; }
.tb-ins .ins-kpi .l { color: var(--hg-texto-2); font-size: 13px; margin-top: 6px; line-height: 1.3; }
.tb-ins .ins-kpi.bien .n { color: var(--hg-verde); }
.tb-ins .ins-kpi.mal .n { color: var(--hg-bloqueado); }
.tb-ins .ins-kpi.aviso .n { color: var(--hg-aviso); }
/* ---- Tarjeta de sitio (resumen y cabecera de cada sitio) -------------------- */
.tb-ins .ins-sitios { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin: 0 0 18px; }
@media (min-width: 1560px) { .tb-ins .ins-sitios { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 860px) { .tb-ins .ins-sitios { grid-template-columns: 1fr; } }
.tb-ins .ins-sitio { container-type: inline-size; display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 14px 16px 14px 18px;
background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 12px; box-shadow: inset 4px 0 0 var(--ins-c); }
.tb-ins .ins-sitio.cab { margin: 0 0 16px; }
.tb-ins .ins-sitio-t { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.md-typeset .tb-ins .ins-sitio-t h2 { margin: 0; font-size: 21px; line-height: 1.15; }
.md-typeset .tb-ins .ins-sitio.cab .ins-sitio-t h2 { font-size: 24px; }
.tb-ins .ins-sitio-t .ciudad { color: var(--hg-texto-2); font-size: 14px; }
.tb-ins .ins-est { display: inline-flex; align-items: center; gap: 6px; margin-left: auto; font-size: 12px; font-weight: 600;
padding: 2px 9px; border-radius: 6px; background: var(--ins-bg); color: var(--ins-c); white-space: nowrap; }
.tb-ins .ins-nota { margin: -4px 0 0; font-size: 13.5px; line-height: 1.45; color: var(--hg-texto-2); }
/* meta: una casilla por unidad de la meta */
.tb-ins .ins-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 13.5px; color: var(--hg-texto-2); }
.tb-ins .ins-meta b { color: var(--hg-texto-fuerte); font-weight: 600; }
.tb-ins .ins-casillas { display: inline-flex; gap: 4px; }
.tb-ins .ins-casillas span { width: 26px; height: 12px; border-radius: 4px; border: 1.5px dashed var(--hg-linea-2); background: transparent; }
.tb-ins .ins-casillas span.prob { border: 1.5px solid var(--hg-verde); background: var(--hg-verde); }
.tb-ins .ins-casillas span.inst { border: 1.5px solid var(--hg-ambar); background: var(--hg-ambar-bg); }
.tb-ins .ins-logro { font-weight: 600; font-size: 12.5px; padding: 1px 8px; border-radius: 6px; background: var(--hg-pend-bg); color: var(--hg-texto-2); }
.tb-ins .ins-logro.si { background: var(--hg-verde-bg); color: var(--hg-verde); }
/* conteo por etapa (mini embudo) */
.tb-ins .ins-embudo { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 0; border: 1px solid var(--hg-linea); border-radius: 9px; overflow: hidden; }
.tb-ins .ins-embudo div { padding: 6px 4px 5px; text-align: center; border-left: 1px solid var(--hg-linea); background: var(--hg-plano-2); min-width: 0; }
.tb-ins .ins-embudo div:first-child { border-left: 0; }
.tb-ins .ins-embudo b { display: block; font-size: 17px; line-height: 1.1; font-weight: 600; color: var(--hg-texto-fuerte); font-variant-numeric: tabular-nums; }
.tb-ins .ins-embudo span { display: block; font-size: 10.5px; line-height: 1.25; color: var(--hg-texto-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-ins .ins-embudo .cero b { color: var(--hg-texto-3); font-weight: 400; }
.tb-ins .ins-embudo .falla:not(.cero) { background: var(--hg-bloqueado-bg); }
.tb-ins .ins-embudo .falla:not(.cero) b { color: var(--hg-bloqueado); }
/* tarjeta angosta (cuatro columnas o teléfono): dos filas de tres */
@container (max-width: 430px) {
.tb-ins .ins-embudo { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tb-ins .ins-embudo div:nth-child(4) { border-left: 0; }
.tb-ins .ins-embudo div:nth-child(n+4) { border-top: 1px solid var(--hg-linea); }
}
.tb-ins .ins-acc { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.tb-ins .ins-acc .boton, .tb-ins .ins-acc button { font-size: 13px; padding: 6px 11px; }
.tb-ins .ins-acc a.liga { display: inline-flex; align-items: center; gap: 6px; font-size: 13.5px; color: var(--hg-liga); text-decoration: none; }
.tb-ins .ins-acc a.liga:hover { color: var(--hg-liga-hover); text-decoration: underline; }
.tb-ins .ins-vacio { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 10px 12px; border: 1px dashed var(--hg-linea-2);
border-radius: 9px; font-size: 13.5px; color: var(--hg-texto-2); }
/* ---- Columnas de una unidad: Enviada → … → En operación --------------------- */
.tb-ins .ins-kb { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; align-items: start; }
@media (max-width: 1240px) { .tb-ins .ins-kb { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 720px) { .tb-ins .ins-kb { grid-template-columns: 1fr; gap: 16px; } }
.tb-ins .ins-col { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.md-typeset .tb-ins .ins-col h3 { display: flex; align-items: center; gap: 8px; margin: 0; padding: 0 2px 8px; border-bottom: 1px solid var(--hg-linea);
font-family: var(--hg-fuente); font-size: 14px; font-weight: 600; color: var(--hg-texto); }
.tb-ins .ins-col h3 .n { font-weight: 400; font-size: 13px; color: var(--hg-texto-3); font-variant-numeric: tabular-nums; }
.tb-ins .ins-col h3 svg { width: 16px; height: 16px; flex: none; }
.tb-ins .ins-col .hueco { border: 1px dashed var(--hg-linea); border-radius: 9px; padding: 9px; text-align: center; font-size: 12.5px; color: var(--hg-texto-3); }
/* carril «Con falla» */
.tb-ins .ins-carril { margin: 0 0 16px; padding: 12px 14px 14px; border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--hg-bloqueado) 42%, var(--hg-linea));
background: color-mix(in srgb, var(--hg-bloqueado-bg) 55%, transparent); }
.md-typeset .tb-ins .ins-carril h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; font-family: var(--hg-fuente); font-size: 14px; font-weight: 600; color: var(--hg-bloqueado); }
.tb-ins .ins-carril h3 .n { font-weight: 400; color: var(--hg-texto-2); }
.tb-ins h3 svg { width: 16px; height: 16px; flex: none; }
.tb-ins .ins-carril-g { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 10px; }
/* ---- Tarjeta de una unidad -------------------------------------------------- */
.tb-ins .ins-card { display: flex; flex-direction: column; gap: 7px; min-width: 0; padding: 11px 12px 12px;
background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 10px; font-size: 13.5px; line-height: 1.4; }
.tb-ins .ins-card:hover { border-color: var(--hg-linea-2); }
.tb-ins .ins-card.con-falla { border-color: color-mix(in srgb, var(--hg-bloqueado) 50%, var(--hg-linea)); }
.tb-ins .ins-card .cab { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.tb-ins .ins-card .id { font-weight: 700; font-size: 15px; color: var(--hg-texto-fuerte); text-decoration: none; overflow-wrap: anywhere; }
.tb-ins .ins-card a.id:hover { color: var(--hg-liga-hover); }
.tb-ins .ins-card .sitio-b { margin-left: auto; font-size: 10.5px; font-weight: 600; letter-spacing: .03em; padding: 0 5px; border-radius: 4px;
border: 1px solid var(--hg-linea-2); color: var(--hg-texto-2); white-space: nowrap; }
.tb-ins .ins-card .serie { margin-top: -5px; font-family: var(--hg-fuente-mono); font-size: 11.5px; color: var(--hg-texto-3); overflow-wrap: anywhere; }
.tb-ins .ins-card .fila { display: flex; align-items: flex-start; gap: 7px; color: var(--hg-texto-2); min-width: 0; }
.tb-ins .ins-card .fila .i { margin-top: 2px; }
.tb-ins .ins-card .fila b { color: var(--hg-texto); font-weight: 600; }
.tb-ins .ins-card .veh { color: var(--hg-texto); }
.tb-ins .ins-card .v-chip { font-size: 11px; font-weight: 600; padding: 0 5px; border-radius: 4px; background: var(--hg-pend-bg); color: var(--hg-texto-2); white-space: nowrap; }
.tb-ins .ins-card .sin-veh { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; padding: 2px 8px; border-radius: 6px;
background: var(--hg-aviso-bg); color: var(--hg-aviso); font-weight: 600; font-size: 12.5px; }
.tb-ins .ins-card .aviso { color: var(--hg-aviso); }
.tb-ins .ins-card .falla { color: var(--hg-bloqueado); font-weight: 600; }
.tb-ins .ins-card .actas { display: flex; flex-wrap: wrap; gap: 4px; }
.tb-ins .ins-card .actas a { display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-weight: 500; padding: 1px 7px; border-radius: 5px;
background: var(--hg-hecho-bg); color: var(--hg-hecho); text-decoration: none; white-space: nowrap; }
.tb-ins .ins-card .actas a.falla { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); }
.tb-ins .ins-card .actas a:hover { text-decoration: underline; }
.tb-ins .ins-card .actas .i { width: 12px; height: 12px; }
.tb-ins .ins-card .acc { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; margin-top: 2px; }
.tb-ins .ins-card .acc .boton { font-size: 13px; padding: 6px 10px; }
.tb-ins .ins-card .acc a.liga { font-size: 12.5px; color: var(--hg-liga); text-decoration: none; }
.tb-ins .ins-card .acc a.liga:hover { text-decoration: underline; }
/* ---- Qué falta (resumen) ---------------------------------------------------- */
.tb-ins .ins-falta { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; }
.tb-ins .ins-falta section { background: var(--hg-plano-2); border: 1px solid var(--hg-linea); border-radius: 12px; padding: 12px 14px; min-width: 0; }
.md-typeset .tb-ins .ins-falta h3 { display: flex; align-items: center; gap: 8px; margin: 0 0 8px; font-family: var(--hg-fuente); font-size: 14.5px; font-weight: 600; }
.tb-ins .ins-falta h3 .n { font-weight: 400; color: var(--hg-texto-3); }
.tb-ins .ins-falta ul { list-style: none; margin: 0; padding: 0; }
.md-typeset .tb-ins .ins-falta li { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: 0; padding: 8px 0; border-top: 1px solid var(--hg-linea-suave); font-size: 13.5px; }
.md-typeset .tb-ins .ins-falta li:first-child { border-top: 0; padding-top: 2px; }
.tb-ins .ins-falta li .q { flex: 1 1 180px; min-width: 0; color: var(--hg-texto-2); }
.tb-ins .ins-falta li .q b { color: var(--hg-texto); }
.tb-ins .ins-falta li .boton { font-size: 12.5px; padding: 5px 9px; }
.tb-ins .ins-falta .nada { font-size: 13.5px; color: var(--hg-texto-3); }
@media (max-width: 600px) {
.tb-ins .ins-tabs { display: flex; }
.tb-ins .ins-tabs button { flex: 1 1 auto; justify-content: center; }
.tb-ins .ins-sitio { padding: 12px 13px 12px 15px; }
.tb-ins .ins-col .hueco { display: none; }
}

View File

@ -0,0 +1,362 @@
/* Tablero de Instalaciones por sitio (22-oct-2026).
Una tarjeta por unidad en cada sitio: Enviada → Recibida en sitio → Instalada → Probada en sitio →
En operación, y aparte el carril «Con falla». Todo sale de las actas (GET /api/instalaciones,
backend/instalaciones.py); aquí no se captura nada. El color, la meta y la nota de cada sitio vienen
de docs/datos/proyecto.json (los mismos de la página de inicio).
La dirección guarda el sitio: ?sitio=eit (sin sitio = resumen de todos; ?sitio=lct también sirve).
Los botones abren la siguiente acta con la unidad, el sitio y el vehículo ya puestos (acta.js, precarga). */
(function () {
const raiz = document.getElementById('ins');
if (!raiz) return;
const $ = id => document.getElementById(id);
const el = (t, a, ...k) => {
const e = document.createElement(t);
if (a) for (const p in a) { if (p === 'text') e.textContent = a[p]; else if (p === 'html') e.innerHTML = a[p]; else if (a[p] != null) e.setAttribute(p, a[p]); }
k.forEach(c => c != null && e.append(typeof c === 'string' ? document.createTextNode(c) : c));
return e;
};
const ETAPAS = [['enviada', 'Enviada'], ['recibida', 'Recibida en sitio'], ['instalada', 'Instalada'], ['probada', 'Probada en sitio'], ['operacion', 'En operación']];
const NOM_ETAPA = Object.fromEntries(ETAPAS);
const CORTO = { enviada: 'Enviada', recibida: 'Recibida', instalada: 'Instalada', probada: 'Probada', operacion: 'Operación', falla: 'Con falla' };
const ORDEN = ['icave', 'timsa', 'eit', 'lzc']; // el orden que pidió Leo
const ALIAS = { lct: 'lzc', lazarocardenas: 'lzc', 'lazaro-cardenas': 'lzc', veracruz: 'icave', manzanillo: 'timsa', ensenada: 'eit' };
const COLOR = { rojo: 'Rojo', amarillo: 'Amarillo', verde: 'Verde' };
const ACTA = { envio: 'Envío', recepcion: 'Recepción', 'instalacion-campo': 'Instalación', 'prueba-sitio': 'Prueba', 'diagnostico-campo': 'Diagnóstico', dano: 'Daño' };
const ACTA_LARGO = { envio: 'Acta de envío', recepcion: 'Acta de recepción', 'instalacion-campo': 'Acta de instalación', 'prueba-sitio': 'Prueba en sitio', 'diagnostico-campo': 'Diagnóstico en campo', dano: 'Acta de daño' };
const FORM = { instalacion: '/formularios/instalacion.html', prueba: '/formularios/prueba-sitio.html', recepcion: '/formularios/recepcion.html', diagnostico: '/formularios/diagnostico.html' };
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
// Íconos de línea (24×24, trazo redondo), como los del resto del sitio.
const ICONOS = {
documento: 'M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5zM14 3.5v5h5M9 13h6M9 16.5h4',
prueba: 'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM8.5 12.2l2.4 2.4 4.6-4.8',
vehiculo: 'M3.5 6.5h10v9.5h-10zM13.5 9.5h3.7l3.3 3.6V16h-7M7.2 18.8a1.7 1.7 0 1 0 0-3.4 1.7 1.7 0 0 0 0 3.4zM16.8 18.8a1.7 1.7 0 1 0 0-3.4 1.7 1.7 0 0 0 0 3.4z',
fecha: 'M6 5h12a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zM8 3v4M16 3v4M4 10h16',
persona: 'M12 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM5 20c.8-3.8 3.6-6 7-6s6.2 2.2 7 6',
aviso: 'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 8v5M12 16.2v.3',
ok: 'M5 12.5l4.5 4.5L19 7.5',
cruz: 'M7 7l10 10M17 7 7 17',
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',
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',
flecha: 'M5 12h14M13 6l6 6-6 6',
unidad: 'M5 7.5 12 4l7 3.5v9L12 20l-7-3.5zM5 7.5l7 3.5 7-3.5M12 11v9'
};
const icono = n => '<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="' + (ICONOS[n] || '') + '"/></svg>';
// Avance de cada columna: un círculo que se llena (como los estados de Pendientes).
const C = 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z';
const aro = c => '<path d="' + C + '" style="fill:none;stroke:' + c + ';stroke-width:2"/>';
const ETAPA_SVG = {
enviada: '<path d="' + C + '" style="fill:none;stroke:var(--hg-texto-3);stroke-width:2;stroke-dasharray:2.6 2.6"/>',
recibida: aro('var(--hg-curso)') + '<path d="M12 12V4a8 8 0 0 1 8 8z" style="fill:var(--hg-curso)"/>',
instalada: aro('var(--hg-curso)') + '<path d="M12 4a8 8 0 0 1 0 16z" style="fill:var(--hg-curso)"/>',
probada: aro('var(--hg-curso)') + '<path d="M12 12V4a8 8 0 1 1-8 8z" style="fill:var(--hg-curso)"/>',
operacion: '<path d="' + C + '" style="fill:var(--hg-verde)"/><path d="M8.3 12.3l2.5 2.5 4.9-5" style="fill:none;stroke:var(--hg-plano);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round"/>',
falla: aro('var(--hg-bloqueado)') + '<path d="M12 8v4.6M12 15.8v.2" style="fill:none;stroke:var(--hg-bloqueado);stroke-width:2.2;stroke-linecap:round"/>'
};
const svgEtapa = k => '<svg viewBox="0 0 24 24" aria-hidden="true">' + (ETAPA_SVG[k] || '') + '</svg>';
const plural = (n, uno, varios) => n + ' ' + (n === 1 ? uno : varios);
function fechaC(iso) {
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''));
if (!m) return '';
return (+m[3]) + ' ' + MES[+m[2] - 1] + (+m[1] !== new Date().getFullYear() ? ' ' + m[1] : '');
}
const qs = o => Object.keys(o).filter(k => o[k]).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(o[k])).join('&');
const forma = (f, o) => FORM[f] + (o && qs(o) ? '?' + qs(o) : '');
const expediente = (serie, archivo) => '/tableros/unidad/?serie=' + encodeURIComponent(serie)
+ (archivo ? '#a-' + String(archivo).replace(/[^A-Za-z0-9_-]/g, '') : '');
function meta(s) {
const t = String(s.meta || '');
let m = /(\d+)\s*[–—-]\s*(\d+)/.exec(t);
if (m) return [+m[1], Math.max(+m[1], +m[2])];
m = /(\d+)/.exec(t);
return m ? [+m[1], +m[1]] : [1, 2];
}
let DATOS = null, PROY = null, SITIOS = [], vista = 'resumen', cargadoEn = 0;
const cont = $('ins-cont'), tabs = $('ins-tabs');
const sitioDe = id => SITIOS.find(s => s.id === id) || { id, nombre: String(id || '').toUpperCase(), ciudad: '', estado: '', nota: '', meta: '' };
const R = id => (DATOS && DATOS.resumen && DATOS.resumen[id]) || { enviada: 0, recibida: 0, instalada: 0, probada: 0, operacion: 0, falla: 0, unidades: 0, instaladas: 0, probadas: 0, sin_vehiculo: 0, sin_prueba: 0 };
const unidades = id => ((DATOS && DATOS.unidades) || []).filter(u => !id || u.sitio === id);
const conVehiculo = u => !!(u.vehiculo && u.vehiculo.numero);
// ---------------------------------------------------------------- siguiente acta de cada unidad
function siguiente(u) {
const v = u.vehiculo || {};
const base = { serie: u.serie, sitio: u.sitio };
const prueba = Object.assign({}, base, { maquina_id: v.numero, tipo_vehiculo: v.tipo, id_monitor: u.id_monitor });
if (u.falla) {
if (u.falla.tipo === 'dano') return { txt: 'Ver el daño', href: expediente(u.serie, u.falla.archivo), sec: true };
if (u.retirada) return { txt: 'Ver expediente', href: expediente(u.serie), sec: true };
if (u.falla.tipo === 'prueba-sitio') return { txt: 'Repetir la prueba', href: forma('prueba', prueba), ic: 'prueba' };
if (u.falla.tipo === 'recepcion') return { txt: 'Ver la recepción', href: expediente(u.serie, u.falla.archivo), sec: true };
if (u.falla.tipo === 'instalacion-campo' && !u.instalada)
return { txt: 'Acta de instalación', href: forma('instalacion', Object.assign({}, base, { maquina_id: v.numero || u.vehiculo_destino })), ic: 'documento' };
return { txt: 'Diagnóstico en campo', href: forma('diagnostico', Object.assign({}, base, { maquina_id: v.numero })), ic: 'documento' };
}
if (u.etapa === 'enviada') return { txt: 'Registrar recepción', href: forma('recepcion', Object.assign({}, base, { maquina_destino: u.vehiculo_destino })), ic: 'documento' };
if (u.etapa === 'recibida') return { txt: 'Acta de instalación', href: forma('instalacion', Object.assign({}, base, { maquina_id: u.vehiculo_destino })), ic: 'documento' };
if (u.etapa === 'instalada') return { txt: 'Prueba en sitio', href: forma('prueba', prueba), ic: 'prueba' };
return null;
}
// ---------------------------------------------------------------- tarjeta de una unidad
function tarjeta(u, conSitio) {
const c = el('article', { class: 'ins-card' + (u.falla ? ' con-falla' : '') });
const cab = el('div', { class: 'cab' });
cab.append(el('a', { class: 'id', href: expediente(u.serie), title: 'Expediente de ' + u.serie, text: u.id_monitor || u.serie }));
if (conSitio) cab.append(el('span', { class: 'sitio-b', text: sitioDe(u.sitio).nombre }));
c.append(cab);
if (u.id_monitor) c.append(el('div', { class: 'serie', text: u.serie }));
// en qué vehículo está
const v = u.vehiculo || {};
const enSitio = u.instalada || ['instalada', 'probada', 'operacion'].includes(u.etapa);
if (conVehiculo(u)) {
const t = el('span', { class: 'veh' }, el('b', { text: v.numero }), v.tipo ? ' · ' + v.tipo : '');
const f = el('div', { class: 'fila', html: icono('vehiculo') }, t);
if (v.voltaje && /^\d/.test(v.voltaje)) f.append(el('span', { class: 'v-chip', title: 'Sistema eléctrico del vehículo', text: v.voltaje }));
c.append(f);
} else if (enSitio && !u.retirada) {
c.append(el('span', { class: 'sin-veh', html: icono('aviso') }, 'Sin vehículo registrado' + (v.tipo ? ' (' + v.tipo + ')' : '')));
} else if (u.vehiculo_destino) {
c.append(el('div', { class: 'fila', html: icono('vehiculo') }, 'Para ', el('b', { text: u.vehiculo_destino })));
}
// cuándo y en qué quedó
const fe = u.fechas || {};
if (u.falla) {
c.append(el('div', { class: 'fila falla', html: icono('aviso') }, [u.falla.motivo, fechaC(u.falla.fecha)].filter(Boolean).join(' · ')));
if (u.etapa) c.append(el('div', { class: 'fila', html: icono('flecha') }, 'Llegó a: ' + NOM_ETAPA[u.etapa]));
} else {
let txt = '';
if (u.etapa === 'enviada') txt = 'Enviada ' + fechaC(fe.enviada);
else if (u.etapa === 'recibida') txt = 'Recibida ' + fechaC(fe.recibida);
else if (u.etapa === 'instalada') txt = fe.instalada ? 'Instalada ' + fechaC(fe.instalada) : 'Instalada';
else if (u.etapa === 'probada') txt = 'Probada ' + fechaC(fe.probada) + (u.operacion_desde ? ' · pasa a operación el ' + fechaC(u.operacion_desde) + ' si no falla' : '');
else if (u.etapa === 'operacion') txt = 'En operación desde el ' + fechaC(u.operacion_desde);
if (txt) c.append(el('div', { class: 'fila', html: icono('fecha') }, txt));
}
if (u.tecnico) c.append(el('div', { class: 'fila', html: icono('persona') }, u.tecnico));
if (u.sin_acta_instalacion && !u.retirada) c.append(el('div', { class: 'fila aviso', html: icono('aviso') }, 'Falta el acta de instalación'));
// sus actas: la última de cada tipo, liga a esa acta en el expediente
const ult = {};
(u.actas || []).forEach(a => { if (!ult[a.tipo]) ult[a.tipo] = a; });
const chips = el('div', { class: 'actas' });
Object.keys(ACTA).forEach(t => {
const a = ult[t];
if (!a) return;
const mal = a.resultado === 'FALLA' || /^(No se pudo|No aprobada|No entra|Rechazada)/.test(a.accion || '');
chips.append(el('a', { class: mal ? 'falla' : null, href: expediente(u.serie, a.archivo),
title: ACTA_LARGO[t] + (a.fecha ? ' · ' + fechaC(a.fecha) : '') + (a.tecnico ? ' · ' + a.tecnico : '') + (a.accion ? ' · ' + a.accion : ''),
html: icono(mal ? 'cruz' : 'ok') }, ACTA[t]));
});
if (chips.children.length) c.append(chips);
// la siguiente acta, ya con la unidad, el sitio y el vehículo
const acc = el('div', { class: 'acc' });
const s = siguiente(u);
if (s) acc.append(el('a', { class: 'boton' + (s.sec ? ' sec' : ''), href: s.href, html: s.ic ? icono(s.ic) : '' }, s.txt));
const base = { serie: u.serie, sitio: u.sitio };
const probada = u.etapa === 'probada' || u.etapa === 'operacion';
const sinVeh = probada && !conVehiculo(u);
if (!u.falla && !u.retirada && (sinVeh || u.sin_acta_instalacion))
acc.append(el('a', { class: 'boton sec', href: forma('instalacion', Object.assign({}, base, { maquina_id: v.numero, tipo_vehiculo: v.tipo, id_monitor: u.id_monitor })),
html: icono(sinVeh ? 'vehiculo' : 'documento') }, sinVeh ? 'Registrar el vehículo' : 'Acta de instalación'));
if (!u.falla && probada) acc.append(el('a', { class: 'liga', href: forma('diagnostico', Object.assign({}, base, { maquina_id: v.numero })) }, 'Reportar una falla'));
if (acc.children.length) c.append(acc);
return c;
}
// ---------------------------------------------------------------- un sitio: cabecera o tarjeta del resumen
function fichaSitio(s, enResumen) {
const r = R(s.id);
const card = el('section', { class: 'ins-sitio' + (enResumen ? '' : ' cab') + (COLOR[s.estado] ? ' est-' + s.estado : ''), 'aria-label': s.nombre });
const t = el('div', { class: 'ins-sitio-t' }, el('h2', { text: s.nombre }), el('span', { class: 'ciudad', text: s.ciudad || '' }));
if (COLOR[s.estado]) t.append(el('span', { class: 'ins-est', title: 'Semáforo del sitio (página de inicio)', html: '<span class="luz" aria-hidden="true"></span>' }, COLOR[s.estado]));
card.append(t);
if (s.nota) card.append(el('p', { class: 'ins-nota', text: s.nota }));
// la meta cuenta unidades PROBADAS (prueba en sitio aprobada)
const [mmin, mmax] = meta(s);
const m = el('div', { class: 'ins-meta' });
const n = Math.max(mmax, r.instaladas, r.probadas);
const cas = el('span', { class: 'ins-casillas', role: 'img',
'aria-label': plural(r.probadas, 'probada', 'probadas') + ' y ' + plural(r.instaladas, 'instalada', 'instaladas') + ' contra una meta de ' + mmin + ' a ' + mmax });
for (let i = 1; i <= n; i++) cas.append(el('span', { class: i <= r.probadas ? 'prob' : i <= r.instaladas ? 'inst' : null,
title: i <= r.probadas ? 'Probada en sitio' : i <= r.instaladas ? 'Instalada, sin prueba aprobada' : 'Falta' }));
m.append(el('span', null, 'Meta ', el('b', { text: mmin === mmax ? String(mmin) : mmin + '–' + mmax })), cas,
el('span', null, el('b', { text: String(r.probadas) }), ' ' + (r.probadas === 1 ? 'probada' : 'probadas') + ' · ' + plural(r.instaladas, 'instalada', 'instaladas')));
m.append(el('span', { class: 'ins-logro' + (r.probadas >= mmin ? ' si' : ''), text: r.probadas >= mmin ? 'Meta cumplida' : 'Falta la meta' }));
card.append(m);
const emb = el('div', { class: 'ins-embudo' });
ETAPAS.map(e => e[0]).concat('falla').forEach(k => emb.append(el('div', { class: (k === 'falla' ? 'falla' : '') + (r[k] ? '' : ' cero'), title: k === 'falla' ? 'Con falla' : NOM_ETAPA[k] },
el('b', { text: String(r[k] || 0) }), el('span', { text: CORTO[k] }))));
card.append(emb);
if (!r.unidades) {
card.append(el('div', { class: 'ins-vacio' },
el('span', null, 'Sin unidades registradas. ¿Hay unidades instaladas sin acta? Llénala con la ', el('b', { text: 'fecha real' }), ' de la instalación.'),
el('a', { class: 'boton', href: forma('instalacion', { sitio: s.id }), html: icono('documento') }, 'Acta de instalación')));
}
const acc = el('div', { class: 'ins-acc' });
if (enResumen) {
const ver = el('button', { type: 'button', class: 'sec', html: icono('columnas') }, 'Ver ' + s.nombre);
ver.addEventListener('click', () => ir(s.id));
acc.append(ver);
}
acc.append(el('a', { class: 'liga', href: '/tableros/pendientes/?sitio=' + encodeURIComponent(s.id) }, 'Pendientes de ' + s.nombre),
el('a', { class: 'liga', href: '/tableros/sitio/?id=' + encodeURIComponent(s.id), html: icono('sitio') }, 'Página del sitio'));
card.append(acc);
return card;
}
// ---------------------------------------------------------------- vistas
function pintarSitio(id) {
const s = sitioDe(id), us = unidades(id);
cont.innerHTML = '';
cont.append(fichaSitio(s, false));
const fallas = us.filter(u => u.columna === 'falla');
if (fallas.length) {
const car = el('section', { class: 'ins-carril', 'aria-label': 'Con falla' });
car.append(el('h3', { html: svgEtapa('falla') }, 'Con falla ', el('span', { class: 'n', text: String(fallas.length) })));
const g = el('div', { class: 'ins-carril-g' });
fallas.forEach(u => g.append(tarjeta(u)));
car.append(g);
cont.append(car);
}
const kb = el('div', { class: 'ins-kb' });
ETAPAS.forEach(([k, nombre]) => {
const lista = us.filter(u => u.columna === k);
const col = el('section', { class: 'ins-col', 'aria-label': nombre });
col.append(el('h3', { html: svgEtapa(k) }, nombre + ' ', el('span', { class: 'n', text: String(lista.length) })));
if (!lista.length) col.append(el('div', { class: 'hueco', text: '—' }));
lista.forEach(u => col.append(tarjeta(u)));
kb.append(col);
});
cont.append(kb);
}
function pintarResumen() {
cont.innerHTML = '';
const todas = unidades();
const sum = k => SITIOS.reduce((a, s) => a + (R(s.id)[k] || 0), 0);
const conMeta = SITIOS.filter(s => R(s.id).probadas >= meta(s)[0]).length;
const kp = el('div', { class: 'ins-kpis' });
[[sum('instaladas'), 'Instaladas', ''], [sum('probadas'), 'Probadas en sitio (cuentan para la meta)', sum('probadas') ? 'bien' : ''],
[sum('sin_prueba'), 'Instaladas sin prueba', sum('sin_prueba') ? 'aviso' : ''], [sum('falla'), 'Con falla', sum('falla') ? 'mal' : ''],
[conMeta + ' de ' + SITIOS.length, 'Sitios con la meta cumplida', conMeta === SITIOS.length && SITIOS.length ? 'bien' : '']]
.forEach(([n, l, cls]) => kp.append(el('div', { class: 'ins-kpi ' + cls }, el('div', { class: 'n', text: String(n) }), el('div', { class: 'l', text: l }))));
cont.append(kp);
const g = el('div', { class: 'ins-sitios' });
SITIOS.forEach(s => g.append(fichaSitio(s, true)));
cont.append(g);
// qué falta, en todos los sitios
const falta = el('div', { class: 'ins-falta' });
const caja = (titulo, lista, fila, nada) => {
const sec = el('section', { 'aria-label': titulo });
sec.append(el('h3', null, titulo + ' ', el('span', { class: 'n', text: String(lista.length) })));
if (!lista.length) sec.append(el('p', { class: 'nada', text: nada }));
else { const ul = el('ul'); lista.forEach(u => ul.append(fila(u))); sec.append(ul); }
return sec;
};
const quien = u => el('span', { class: 'q' }, el('b', { text: u.id_monitor || u.serie }), ' · ' + sitioDe(u.sitio).nombre
+ (conVehiculo(u) ? ' · ' + u.vehiculo.numero + (u.vehiculo.tipo ? ' (' + u.vehiculo.tipo + ')' : '') : ''));
const boton = (s) => s ? el('a', { class: 'boton' + (s.sec ? ' sec' : ''), href: s.href }, s.txt) : null;
falta.append(caja('Instaladas sin prueba', todas.filter(u => u.columna === 'instalada'),
u => el('li', null, quien(u), boton(siguiente(u))), 'Ninguna: toda unidad instalada ya tiene su prueba en sitio.'));
falta.append(caja('Sin vehículo registrado', todas.filter(u => u.instalada && !conVehiculo(u)),
u => el('li', null, quien(u), el('a', { class: 'boton sec', href: forma('instalacion', { serie: u.serie, sitio: u.sitio, id_monitor: u.id_monitor }) }, 'Registrar el vehículo')),
'Ninguna: cada unidad instalada dice en qué vehículo está.'));
falta.append(caja('Con falla', todas.filter(u => u.columna === 'falla'),
u => el('li', null, el('span', { class: 'q' }, el('b', { text: u.id_monitor || u.serie }), ' · ' + sitioDe(u.sitio).nombre + ' · ' + ((u.falla || {}).motivo || '')), boton(siguiente(u))),
'Ninguna unidad con falla abierta.'));
cont.append(el('h2', { text: 'Qué falta', style: 'margin:6px 0 10px' }), falta);
}
function pintarTabs() {
tabs.innerHTML = '';
const b = (id, nombre, n, estado) => {
const x = el('button', { type: 'button', 'aria-pressed': String(vista === id), class: COLOR[estado] ? 'est-' + estado : null,
title: id === 'resumen' ? 'Todos los sitios' : nombre + (COLOR[estado] ? ' — semáforo ' + COLOR[estado].toLowerCase() : '') });
if (id !== 'resumen') x.append(el('span', { class: 'luz', 'aria-hidden': 'true' }));
x.append(nombre);
if (n != null) x.append(el('span', { class: 'n', text: String(n) }));
x.addEventListener('click', () => ir(id));
return x;
};
tabs.append(b('resumen', 'Resumen'));
SITIOS.forEach(s => tabs.append(b(s.id, s.nombre, R(s.id).unidades, s.estado)));
}
function pintar() {
if (vista !== 'resumen' && !SITIOS.some(s => s.id === vista)) vista = 'resumen';
pintarTabs();
const s = vista === 'resumen' ? null : sitioDe(vista);
const hora = new Date(cargadoEn).toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit', hour12: false });
const us = unidades(s && s.id);
$('ins-meta').textContent = (s ? s.nombre + ' · ' : '') + plural(us.length, 'unidad', 'unidades') + (s ? '' : ' en ' + plural(SITIOS.length, 'sitio', 'sitios')) + ' · al día a las ' + hora;
$('ins-a-inst').href = forma('instalacion', s ? { sitio: s.id } : null);
$('ins-a-prueba').href = forma('prueba', s ? { sitio: s.id } : null);
document.title = 'Instalaciones' + (s ? ' · ' + s.nombre : '') + ' - Drogheda';
if (s) pintarSitio(s.id); else pintarResumen();
}
function ir(id) {
vista = id;
const u = new URL(location.href);
if (id === 'resumen') u.searchParams.delete('sitio'); else u.searchParams.set('sitio', id);
history.replaceState(null, '', u.pathname + u.search + u.hash);
pintar();
if (window.scrollY > tabs.offsetTop) tabs.scrollIntoView({ block: 'start' });
}
function armarSitios() {
const api = (DATOS && DATOS.sitios) || [], proy = (PROY && PROY.sitios) || [];
const hay = id => api.some(s => s.id === id) || proy.some(s => s.id === id);
const ids = [];
ORDEN.filter(hay).concat(api.map(s => s.id), proy.map(s => s.id)).forEach(id => { if (id && id !== 'qro' && !ids.includes(id)) ids.push(id); });
SITIOS = ids.map(id => {
const a = api.find(s => s.id === id) || {}, p = proy.find(s => s.id === id) || {};
return { id, nombre: p.nombre || a.nombre || id.toUpperCase(), ciudad: p.ciudad || a.ubicacion || '', estado: p.estado || '', nota: p.nota || '', meta: p.meta || '' };
});
}
async function cargar() {
const btn = $('ins-actualizar');
if (btn) btn.disabled = true;
try {
const [a, p] = await Promise.all([
fetch('/api/instalaciones', { cache: 'no-store' }).then(async r => {
let j = null;
try { j = await r.json(); } catch (e) {}
if (!r.ok || !j || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
return j;
}),
fetch('/datos/proyecto.json', { cache: 'no-store' }).then(r => (r.ok ? r.json() : null)).catch(() => null)
]);
DATOS = a; PROY = p; cargadoEn = Date.now();
armarSitios();
pintar();
} catch (e) {
$('ins-meta').textContent = 'Sin datos';
cont.innerHTML = '';
cont.append(el('div', { class: 'err' }, 'No se pudo leer el tablero de instalaciones (' + e.message + '). '
+ (/404/.test(e.message) ? 'Si se acaba de publicar, falta reiniciar el servicio actas-hbox en el servidor. ' : '')
+ 'Las actas siguen en su lugar; avisa a Leo si no se arregla.'));
}
if (btn) btn.disabled = false;
}
const q = (new URLSearchParams(location.search).get('sitio') || '').trim().toLowerCase();
vista = q ? (ALIAS[q] || q) : 'resumen';
const btn = $('ins-actualizar');
if (btn) btn.addEventListener('click', cargar);
document.addEventListener('visibilitychange', () => { if (!document.hidden && cargadoEn && Date.now() - cargadoEn > 60000) cargar(); });
cargar();
})();

View File

@ -0,0 +1,35 @@
---
title: "Instalaciones"
description: "Instalaciones por sitio: en qué vehículo está cada unidad, sus actas, la prueba en sitio y la meta de 1–2 unidades."
template: tablero.html
icon: helgen/vehiculo
date: 2026-10-22
hide:
- toc
---
<link rel="stylesheet" href="/tableros/instalaciones.css">
<div class="tb tb-ins" id="ins">
<header class="tb-cab">
<div class="tb-cab__t"><h1>Instalaciones</h1><span class="meta" id="ins-meta">Cargando…</span></div>
<div class="tb-cab__acc"><a class="boton sec" id="ins-a-inst" href="/formularios/instalacion.html"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5zM14 3.5v5h5M9 13h6M9 16.5h4"/></svg>Acta de instalación</a><a class="boton sec" id="ins-a-prueba" href="/formularios/prueba-sitio.html"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM8.5 12.2l2.4 2.4 4.6-4.8"/></svg>Prueba en sitio</a><button type="button" class="fantasma" id="ins-actualizar" title="Volver a leer las actas"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4"/></svg>Actualizar</button></div>
</header>
<details class="info tb-como">
<summary>Cómo se usa</summary>
<ol>
<li>Una tarjeta por unidad en cada sitio. Avanza sola con las actas: <b>envío → recepción → instalación → prueba en sitio</b>. Aquí no se captura nada.</li>
<li>Cada tarjeta dice <b>en qué vehículo</b> está la unidad (número económico, tipo y 12 o 24 V) y su nombre en el monitor (H00xx). «Sin vehículo registrado» quiere decir que falta el acta de instalación o que no trae el vehículo.</li>
<li>Una unidad cuenta como <b>probada</b> solo con el acta <a href="/formularios/prueba-sitio.html">Prueba en sitio</a> aprobada, y solo las probadas cuentan para la meta de cada sitio. Pasa a <b>En operación</b> 7 días después de la prueba si no se registra una falla.</li>
<li><b>Con falla</b>: la última acta dice que algo salió mal (instalación sin reportar, prueba no aprobada, diagnóstico con falla, unidad desmontada o con marca de daño).</li>
<li>El botón de cada tarjeta abre la siguiente acta con la unidad, el sitio y el vehículo ya puestos. Los nombres de las actas abren esa acta en el expediente de la unidad.</li>
<li>¿Instalaste una unidad y no aparece? Llena su <a href="/formularios/instalacion.html">acta de instalación</a> con la <b>fecha real</b>. El color, la nota y la meta de cada sitio son los de la página de inicio.</li>
</ol>
</details>
<nav class="ins-tabs" id="ins-tabs" aria-label="Sitio"></nav>
<div id="ins-cont"><p class="vacio">Cargando…</p></div>
</div>
<script src="/tableros/instalaciones.js"></script>

View File

@ -36,7 +36,7 @@ const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (co
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };
const fmt = iso => (iso || '').replace('T', ' ').slice(0, 16);
const $ = id => document.getElementById(id);
const NOMBRE = { 'instalacion-campo': 'Instalación', 'diagnostico-campo': 'Diagnóstico en campo', 'envio': 'Envío', 'recepcion': 'Recepción', 'dano': 'Daño o incidente' };
const NOMBRE = { 'instalacion-campo': 'Instalación', 'prueba-sitio': 'Prueba en sitio', 'diagnostico-campo': 'Diagnóstico en campo', 'envio': 'Envío', 'recepcion': 'Recepción', 'dano': 'Daño o incidente' };
let SITIOS = [], ADMIN = false, ACTUAL = null, DATOS = null;
const dlg = $('dlg'), aviso = $('aviso');
function mensaje(t, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = t; clearTimeout(aviso._t); aviso._t = setTimeout(() => aviso.className = 'msg', bien ? 4000 : 9000); }
@ -55,6 +55,7 @@ function render(j) {
c('Unidades instaladas', String(j.unidades.filter(u => u.instalada).length) + ' <span style="font-weight:400;color:var(--tinta2);font-size:12.5px">con acta de instalación OK</span>');
c('Con daño o baja', '<span style="color:' + (j.danos.length ? 'var(--falla)' : 'inherit') + '">' + j.danos.length + '</span>');
cont.append(ficha);
cont.append(el('p', { class: 'sub', style: 'margin:-6px 0 12px', html: '<a href="/tableros/instalaciones/?sitio=' + encodeURIComponent(s.id) + '">Instalaciones de ' + esc(s.nombre) + '</a>: en qué vehículo está cada unidad, su prueba en sitio y la meta del sitio.' }));
if (s.notas) cont.append(el('p', { class: 'sub', style: 'margin:-6px 0 12px', text: s.notas }));
// contactos

View File

@ -31,7 +31,7 @@ const fecha=s=>(s||'').replace('T',' ').slice(0,16);
const kb=n=>n>1048576?(n/1048576).toFixed(1)+' MB':Math.max(1,Math.round(n/1024))+' kB';
const NOMBRE={'diagnostico-campo':['Diagnóstico en campo','campo'],'diagnostico-banco':['Diagnóstico en banco','banco'],
'reflasheo':['Reflasheo','flash'],'instalacion-campo':['Instalación en sitio','campo'],
'reflasheo':['Reflasheo','flash'],'instalacion-campo':['Instalación en sitio','campo'],'prueba-sitio':['Prueba en sitio','campo'],
'dano':['Daño o incidente','falla'],'envio':['Envío','prod'],'recepcion':['Recepción','prod'],
'prueba-v5':['Prueba de tarjeta V5','prod'],'prueba-v4':['Prueba de tarjeta V4','prod'],
'modulo-ard':['Módulo ArduSimple','prod'],'modulo-4g':['Módem 4G','prod'],
@ -115,7 +115,7 @@ function render(j){
const lt=el('div',{class:'linea'});
j.actas.forEach(a=>{
const [nom,cls]=NOMBRE[a.tipo]||[a.tipo,'pend'];
const ev=el('div',{class:'ev '+cls});
const ev=el('div',{class:'ev '+cls,id:'a-'+String(a.archivo||'').replace(/[^A-Za-z0-9_-]/g,'')}); // #a-<archivo>: lo abre el tablero de Instalaciones
const cab=el('div',{class:'cab'});
cab.append(el('div',null, el('div',{class:'t',text:nom}),
el('div',{class:'f',text:(a.fecha?a.fecha+' · ':'')+fecha(a.recibido)+' · '+(a.tecnico||'—')+(a.maquina?' · '+a.maquina:'')})));
@ -141,6 +141,8 @@ function render(j){
b.append(lt);
}
cont.append(b);
const irA_=location.hash&&document.getElementById(location.hash.slice(1)); // ligas #a-<archivo> del tablero de Instalaciones
if(irA_){ const d=irA_.querySelector('details'); if(d) d.open=true; irA_.scrollIntoView({block:'center'}); }
// ---- todos los archivos ----
const f=el('section',{class:'bloque'});
@ -248,6 +250,7 @@ function trazabilidad(j){
const pEnv=env?paso('Envío',env.accion&&/detenida/i.test(env.accion)?'info':'ok',env.accion||'Enviada',(env.fecha||'')+(env.clasificacion?' · '+env.clasificacion:'')):paso('Envío','pend','sin acta'); campo.append(pEnv); conSitio(pEnv,env);
const pRec=rec?paso('Recepción',rec.resultado==='FALLA'?'falla':'ok',rec.accion||rec.resultado||'Recibida',(rec.fecha||'')+(rec.clasificacion?' · '+rec.clasificacion:'')):paso('Recepción','pend','sin acta'); campo.append(pRec); conSitio(pRec,rec);
const pIns=ins?paso('Instalación',ins.resultado==='FALLA'?'falla':'ok',ins.accion||(ins.resultado==='FALLA'?'Falla':'Instalada'),(ins.fecha||'')+(ins.maquina?' · '+ins.maquina:'')):paso('Instalación','pend','sin acta','<a href="/formularios/instalacion.html">llenar acta</a>'); campo.append(pIns); conSitio(pIns,ins);
const pru=ultima(A,['prueba-sitio']); const pPr=pru?paso('Prueba en sitio',pru.resultado==='FALLA'?'falla':'ok',pru.resultado==='FALLA'?'No aprobada':'Aprobada',(pru.fecha||'')+(pru.maquina?' · '+pru.maquina:'')):paso('Prueba en sitio','pend','sin acta','<a href="/formularios/prueba-sitio.html?serie='+encodeURIComponent(j.serie)+'">llenar acta</a>'); campo.append(pPr); conSitio(pPr,pru);
const pDg=diags.length?paso('Diagnósticos',diags[0].resultado==='FALLA'?'falla':'info',diags.length+' visita(s)',(diags[0].fecha||'')+(diags[0].accion?' · '+diags[0].accion:'')):paso('Diagnósticos','pend','ninguno'); campo.append(pDg); conSitio(pDg,diags[0]);
const pDn=j.marca?paso('Daños',j.marca==='BAJA'?'falla':'falla',j.marca==='BAJA'?'Baja':'Dañada',(j.marca_acta&&j.marca_acta.fecha)||''):(danos.length?paso('Daños','ok','reparada / sin marca',danos.length+' acta(s)'):paso('Daños','ok','sin daños')); campo.append(pDn); conSitio(pDn,danos[0]);
return sec;

View File

@ -93,6 +93,7 @@ nav:
- Producción: tableros/produccion.md
- Actas: tableros/actas.md
- Inventario: tableros/inventario.md
- Instalaciones: tableros/instalaciones.md
- Sitios: tableros/sitio.md
- Equipo: tableros/equipo.md
- Expediente de unidad: tableros/unidad.md
@ -142,6 +143,7 @@ nav:
- Instalación:
- Checklist para imprimir: procedimientos/v5-checklist-puesta-en-marcha.md
- Acta de instalación: formularios/instalacion.html
- Prueba en sitio: formularios/prueba-sitio.html
- Envío y recepción:
- Ciclo de vida: procedimientos/ciclo-de-vida.md
- Acta de envío: formularios/envio.html
@ -155,7 +157,7 @@ nav:
- Auditorías y pasadas:
- 22 oct · Auditoría final V5.6/V5.7: hardware/auditoria-final-22oct.md
- 22 oct · Protocolo auditoría final: hardware/auditoria-final-protocolo.md
- 22 oct · Estado v6 (T25, T28, T38, T39): hardware/v6-estado-21oct.md
- 22 oct · Estado v6 (T25–T40): hardware/v6-estado-21oct.md
- 21 oct · V5.6/V5.7, pasadas 4 a 6: hardware/v56-v57-auditoria-21oct.md
- 19 oct · Críticos V5.6: hardware/v56-clasificacion-19oct.md
- 19 oct · Re-auditoría: hardware/re-auditoria-19oct.md

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6.5h10v9.5h-10zM13.5 9.5h3.7l3.3 3.6V16h-7M7.2 18.8a1.7 1.7 0 1 0 0-3.4 1.7 1.7 0 0 0 0 3.4zM16.8 18.8a1.7 1.7 0 1 0 0-3.4 1.7 1.7 0 0 0 0 3.4z"/></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -17,7 +17,7 @@ echo "Sitio publicado en $REMOTE:$REMOTE_PATH (puerto $PORT)"
# Sin --delete (ahí junto al app.py viven el venv y __pycache__).
BACKEND_PATH="$(dirname "$REMOTE_PATH")/backend"
rsync -az -e "ssh -p $PORT" backend/app.py backend/requirements.txt backend/seed_1013.py \
backend/parche-nginx-api.py backend/actas-hbox.service backend/tablero.py "$REMOTE:$BACKEND_PATH/"
backend/parche-nginx-api.py backend/actas-hbox.service backend/tablero.py backend/instalaciones.py "$REMOTE:$BACKEND_PATH/"
echo "Backend copiado en $REMOTE:$BACKEND_PATH"
# Tablero (21-oct): baja la foto a _estado/ (para que Claude lea el tablero) y avisa si hay lotes

2
scripts/sync-docs.sh Normal file → Executable file
View File

@ -140,6 +140,8 @@ declare -a MAP=(
"03-Architecture-Hardware/img/2026-10-21_Drogheda_v6-T38-Courtyards-U25-Barrenos_v1.0.png|docs/hardware/img/2026-10-21_Drogheda_v6-T38-Courtyards-U25-Barrenos_v1.0.png"
# 2026-10-22: T39 en la v6 (bloque A con la propuesta α, In1/In2 limpios, MH8 libre)
"03-Architecture-Hardware/img/2026-10-22_Drogheda_v6-T39-Bloque-A_v1.0.png|docs/hardware/img/2026-10-22_Drogheda_v6-T39-Bloque-A_v1.0.png"
# 2026-10-22 (tarde): T40 en la v6 (MH4 libre: D23/D24 fuera del barreno, /SIM_CLK por In1, /NTC alrededor)
"03-Architecture-Hardware/img/2026-10-22_Drogheda_v6-T40-MH4_v1.0.png|docs/hardware/img/2026-10-22_Drogheda_v6-T40-MH4_v1.0.png"
# 2026-10-22: auditoría final de producción de V5.6 y V5.7 (GO al piloto con 3 condiciones): doc, protocolo, PDFs y 8 figuras
"03-Architecture-Hardware/2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_ES_v1.0.md|docs/hardware/auditoria-final-22oct.md"
"03-Architecture-Hardware/2026-10-22_Drogheda_Auditoria-Final-Produccion_V5.6-V5.7_Protocolo_ES_v1.0.md|docs/hardware/auditoria-final-protocolo.md"