Reorganizacion del sitio (8 pestanas, Historia), tableros kanban/ingenieria/produccion, inventario, sitios, acta de instalacion, backend nuevo (13-oct)
This commit is contained in:
parent
217311a418
commit
9f3d41ed49
@ -9,7 +9,11 @@ Servidor: `git.helgen.online` · dominio público: `https://drogheda.helgen.onli
|
||||
| Sitio (docs, formularios, tableros) | `/srv/projects/drogheda/hbox-docs/site` | `./scripts/deploy.sh` desde la Mac |
|
||||
| Backend de actas (`app.py`) | `/srv/projects/drogheda/hbox-docs/backend` | `git pull` en el servidor + `systemctl restart actas-hbox` |
|
||||
| Actas recibidas (JSON + índice CSV) | `/srv/projects/drogheda/actas` | las escribe el backend |
|
||||
| **Fotos de campo** | `/srv/projects/drogheda/adjuntos` | las escribe el backend — **nuevo** |
|
||||
| **Fotos de campo** | `/srv/projects/drogheda/adjuntos` | las escribe el backend |
|
||||
| **Pendientes del tablero (kanban)** | `/srv/projects/drogheda/tareas` | las escribe el backend — **13-oct** |
|
||||
| **Inventario (ítems y movimientos)** | `/srv/projects/drogheda/inventario` | las escribe el backend — **13-oct** |
|
||||
| **Sitios** | `/srv/projects/drogheda/sitios` | las escribe el backend — **13-oct** |
|
||||
| **Plan de producción** | `/srv/projects/drogheda/plan-produccion.json` | lo escribe el backend (admin) — **13-oct** |
|
||||
| Usuarios (HTTP Basic) | `/etc/nginx/hbox-docs.htpasswd` | `htpasswd` |
|
||||
|
||||
**Lo importante:** `deploy.sh` publica **solo el sitio**. Si cambió `backend/app.py` —como en esta
|
||||
@ -136,6 +140,90 @@ cambiar la suya sola: restablécesela desde la propia página, que la vuelve a g
|
||||
|
||||
---
|
||||
|
||||
## A2 · Despliegue 13-oct — tableros, inventario, sitios e instalación en campo
|
||||
|
||||
Esta versión agrega al backend cuatro colecciones nuevas (pendientes del tablero, plan de
|
||||
producción, inventario con movimientos, sitios) y páginas que las usan (`tableros/`, `inventario/`,
|
||||
`sitios/`, `formularios/instalacion.html`). Igual que siempre: **el sitio se publica desde la Mac;
|
||||
el backend se actualiza en el servidor**. Si solo publicas el sitio, los tableros dirán «No se pudo
|
||||
cargar» porque hablan con endpoints que el backend viejo no tiene.
|
||||
|
||||
### 1 · Desde la Mac — publicar el sitio
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Claude/Projects/Drogheda/hbox-docs
|
||||
git add -A && git commit -m "tableros kanban, inventario, sitios, acta de instalación (13-oct)"
|
||||
git push
|
||||
./scripts/deploy.sh
|
||||
```
|
||||
|
||||
### 2 · En el servidor — backend y carpetas (las carpetas, una sola vez)
|
||||
|
||||
```bash
|
||||
ssh leo@git.helgen.online
|
||||
cd /srv/projects/drogheda/hbox-docs && git pull
|
||||
|
||||
# carpetas de las colecciones nuevas (junto a /actas; el backend las deduce de ACTAS_DIR)
|
||||
sudo mkdir -p /srv/projects/drogheda/{tareas,inventario/items,inventario/movimientos,sitios}
|
||||
sudo chown -R leo:leo /srv/projects/drogheda/{tareas,inventario,sitios}
|
||||
sudo chown leo:leo /srv/projects/drogheda # (sin -R) el backend escribe plan-produccion.json en esta carpeta
|
||||
|
||||
sudo systemctl restart actas-hbox
|
||||
curl -s http://127.0.0.1:8081/api/salud # {"ok":true,"servicio":"actas-hbox"}
|
||||
curl -s -H 'X-Remote-User: leo' http://127.0.0.1:8081/api/tareas/resumen # {"ok":true,"total":0,...}
|
||||
```
|
||||
|
||||
Variables de entorno: **no hace falta tocar el `.service`**. Si quieres mover las carpetas, descomenta
|
||||
en `/etc/systemd/system/actas-hbox.service` las líneas `TAREAS_DIR`, `INVENTARIO_DIR`, `SITIOS_DIR`,
|
||||
`PLAN_FILE` (vienen comentadas en `backend/actas-hbox.service`) y luego `daemon-reload` + `restart`.
|
||||
|
||||
### 3 · Siembra inicial (una sola vez; repetirla no duplica nada)
|
||||
|
||||
```bash
|
||||
cd /srv/projects/drogheda/hbox-docs
|
||||
./backend/venv/bin/python3 backend/seed_1013.py --usuario leo
|
||||
```
|
||||
|
||||
Crea: el plan (570 → 113 V5.6 / 327 V5.7 / 130 v6), los 4 sitios (icave, lzc, eit, timsa — ubicación
|
||||
y contactos «por confirmar» donde no se sabe), 18 ítems de inventario con **stock 0** (Jesús hace el
|
||||
conteo físico con movimientos «ajuste») y los 12 pendientes del tablero del 13-oct. Lo que ya exista
|
||||
con el mismo `id` se deja intacto. Para importar los pendientes del Task Tracker de Notion, exporta
|
||||
una lista JSON con `id`, `titulo`, `pista`, `estado`, `responsable` y pásala con `--extra archivo.json`.
|
||||
|
||||
### 4 · Verificar
|
||||
|
||||
```bash
|
||||
curl -s -u leo:PASS https://drogheda.helgen.online/api/tareas | head -c 300
|
||||
curl -s -u leo:PASS https://drogheda.helgen.online/api/plan | head -c 200
|
||||
curl -s -u leo:PASS https://drogheda.helgen.online/api/inventario | head -c 200
|
||||
curl -s -u leo:PASS https://drogheda.helgen.online/api/sitios | head -c 200
|
||||
```
|
||||
|
||||
Y desde el navegador: `/tableros/kanban.html` (arrastra un pendiente y revisa que el historial
|
||||
anote quién lo movió), `/inventario/inventario.html` (registra un «ajuste» con el conteo real),
|
||||
`/sitios/sitio.html?id=icave`, `/formularios/instalacion.html` con la unidad **PRUEBA**.
|
||||
|
||||
### 5 · Respaldo
|
||||
|
||||
Se suman tres carpetas y un archivo a la rutina de siempre:
|
||||
|
||||
```bash
|
||||
rsync -az leo@git.helgen.online:/srv/projects/drogheda/tareas/ ~/respaldo-tareas/
|
||||
rsync -az leo@git.helgen.online:/srv/projects/drogheda/inventario/ ~/respaldo-inventario/
|
||||
rsync -az leo@git.helgen.online:/srv/projects/drogheda/sitios/ ~/respaldo-sitios/
|
||||
rsync -az leo@git.helgen.online:/srv/projects/drogheda/plan-produccion.json ~/respaldo-plan/
|
||||
```
|
||||
|
||||
| Síntoma | Causa | Arreglo |
|
||||
|---|---|---|
|
||||
| Los tableros dicen «No se pudo cargar (HTTP 404)» | backend viejo | paso 2 (`git pull` + restart) |
|
||||
| «No puedo identificarte (falta X-Remote-User…)» al crear o mover | nginx no manda la cabecera | paso 4 del despliegue anterior |
|
||||
| El tablero sale vacío | falta la siembra | paso 3 |
|
||||
| «Solo un administrador…» al borrar o editar sitios/plan | ese usuario no está en `ACTAS_ADMINS` | así debe ser; pide a Leo |
|
||||
| `PermissionError` al guardar el plan | `/srv/projects/drogheda` no es de `leo` | `chown` del paso 2 |
|
||||
|
||||
---
|
||||
|
||||
## B · Verificación después de desplegar
|
||||
|
||||
```bash
|
||||
|
||||
54
NUEVOS-1013.md
Normal file
54
NUEVOS-1013.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Archivos nuevos y modificados — 13-oct-2026 (Constructor: WP2–WP5)
|
||||
|
||||
Para quien arma la navegación (`mkdocs.yml`) y `scripts/sync-docs.sh`: no toqué ninguno de los dos.
|
||||
Las rutas son relativas a `docs/`; el título propuesto es el que iría en el `nav`. Los `.html` se
|
||||
copian tal cual; los `.md` llevan frontmatter con `title` y `date`.
|
||||
|
||||
## Páginas nuevas (con título propuesto para el nav)
|
||||
|
||||
| Ruta en `docs/` | Título propuesto | Sección sugerida |
|
||||
|---|---|---|
|
||||
| `tableros/index.md` | **Tableros** (índice: qué es cada tablero) | 📋 Tableros (pestaña nueva) |
|
||||
| `tableros/kanban.html` | Pendientes (kanban) | 📋 Tableros |
|
||||
| `tableros/ingenieria.html` | Ingeniería — ¿dónde está el hardware? | 📋 Tableros |
|
||||
| `tableros/produccion.html` | Producción — pedido 570 | 📋 Tableros (y enlazar desde Producción) |
|
||||
| `inventario/index.md` | **Inventario** (política: quién registra, cuándo, mínimos) | 📦 Inventario (o dentro de Producción) |
|
||||
| `inventario/inventario.html` | Inventario de materiales | 📦 Inventario |
|
||||
| `sitios/index.md` | **Sitios** (lista viva + cómo dar de alta) | 🚜 Campo |
|
||||
| `sitios/sitio.html` | Página de un sitio (`?id=icave`) | 🚜 Campo |
|
||||
| `formularios/instalacion.html` | Acta de instalación en campo | 🚜 Campo → «Instalación en el puerto» (junto al checklist de puesta en marcha) |
|
||||
|
||||
Archivos de apoyo (no van al nav): `tableros/tablero.css`, `tableros/kanban.js`.
|
||||
|
||||
Sugerencia para `actas/index.md` (no lo edité, para no chocar con la reorganización): agregar la fila
|
||||
«[Instalación en campo](../formularios/instalacion.html) · Una por unidad y vehículo; sitio, alimentación,
|
||||
antenas, RTK FIX, fotos · [Sitios](../sitios/index.md)» en la tabla **Campo**.
|
||||
|
||||
## Páginas existentes modificadas
|
||||
|
||||
| Ruta | Cambio |
|
||||
|---|---|
|
||||
| `formularios/acta.js` | nuevo tipo de ítem `sitio` (select alimentado por `/api/sitios`; `guardaEn:'sitio'`); `fillFrom` respeta un select cuyas opciones aún no cargan |
|
||||
| `formularios/envio.html`, `recepcion.html`, `diagnostico.html`, `dano.html` | un ítem `sitio` (opcional) en la sección 0; en daño solo aparece si fue en campo / transporte / no se sabe |
|
||||
| `unidad.html` | bloque **Trazabilidad**: cadena de producción (tarjeta → módulos → integración → ensamble → configuración) y cadena de campo (envío → recepción → instalación → diagnósticos → daños) con sitio y fechas |
|
||||
|
||||
## Backend y despliegue
|
||||
|
||||
| Ruta | Cambio |
|
||||
|---|---|
|
||||
| `backend/app.py` | colecciones `tareas`, `plan`, `inventario` (+ `movimientos`), `sitios`; `/api/actas` y `/api/unidad` exponen `sitio`/`fecha`/`componentes` |
|
||||
| `backend/seed_1013.py` | **nuevo** — siembra idempotente (plan, 4 sitios, 18 ítems, 12 pendientes; `--extra` para importar Notion) |
|
||||
| `backend/tests-1013.sh`, `backend/TESTS-1013.md` | **nuevos** — pruebas con `curl` (61) y resumen de las pruebas de navegador |
|
||||
| `backend/README.md` | sección 5: colecciones, variables, ejemplos `curl` por endpoint |
|
||||
| `backend/actas-hbox.service` | variables `TAREAS_DIR`, `INVENTARIO_DIR`, `SITIOS_DIR`, `PLAN_FILE` comentadas (valores por omisión junto a `/actas`) |
|
||||
| `DESPLIEGUE.md` | sección **A2 · Despliegue 13-oct** (git pull, mkdir, restart, siembra, verificación, respaldo, problemas frecuentes) y filas nuevas en «Qué vive dónde» |
|
||||
|
||||
## Enlaces que las páginas nuevas esperan encontrar
|
||||
|
||||
Las páginas enlazan a estas rutas del sitio (todas existen hoy; si la reorganización las mueve, hay que
|
||||
actualizar `tableros/ingenieria.html` (constante `VERSIONES` y lista de decisiones) y `tableros/produccion.html` (constante `FORM`)):
|
||||
`hardware/v56-layout-pasada-2/`, `hardware/v56-auditoria-layout-13oct/`, `hardware/v56-eco/`, `hardware/v56-datasheet/`,
|
||||
`hardware/v56-pinout/`, `hardware/v57-eco-fuente/`, `hardware/v57-layout-guia/`, `hardware/v6-proximos-pasos/`,
|
||||
`hardware/v6-plan-correccion/`, `hardware/v6-revision-lecciones-v55/`, `hardware/EinarBoard-v6_Plan-de-Cobre_ES.html`,
|
||||
`manuales/manual-tarjeta-V5/`, `manuales/manual-tarjeta-v6/`, `tablero.html`, `unidad.html`, `actas/`,
|
||||
`diagnostico/tablero-campo.html` y los formularios de `formularios/`.
|
||||
@ -22,8 +22,11 @@ mkdocs serve # http://127.0.0.1:8000
|
||||
```
|
||||
|
||||
**Política de publicación:** solo se publica lo que está en la lista de
|
||||
`scripts/sync-docs.sh`. Nada de finanzas, briefs de diseño ni auditorías EMC; para
|
||||
agregar un documento al sitio se agrega a esa lista (y con eso queda auditado en git).
|
||||
`scripts/sync-docs.sh`. Se publica lo que sirve al técnico o al estado de ingeniería
|
||||
(procedimientos, actas, manuales, ECOs, auditorías de diseño y layout, guías de layout);
|
||||
nada de finanzas, fundraising ni briefs comerciales. Para agregar un documento al sitio se
|
||||
agrega a esa lista (y con eso queda auditado en git). Lo superado no se borra: baja a
|
||||
«Referencia e histórico» con un banner que dice qué lo sustituye.
|
||||
|
||||
## Configuración del servidor (una sola vez)
|
||||
|
||||
|
||||
@ -61,3 +61,101 @@ Tras un `git pull` que cambie `app.py`:
|
||||
```bash
|
||||
sudo systemctl restart actas-hbox
|
||||
```
|
||||
|
||||
## 5. Colecciones del 13-oct — tableros, plan, inventario y sitios
|
||||
|
||||
Mismo esquema que las actas: **un JSON por registro** más un `_indice.csv` que se regenera en
|
||||
cada cambio. Carpetas (variables de entorno, con su valor por omisión junto a `ACTAS_DIR`):
|
||||
|
||||
| Variable | Omisión | Qué guarda |
|
||||
|---|---|---|
|
||||
| `TAREAS_DIR` | `/srv/projects/drogheda/tareas` | pendientes del tablero (kanban) |
|
||||
| `INVENTARIO_DIR` | `/srv/projects/drogheda/inventario` | `items/` y `movimientos/` |
|
||||
| `SITIOS_DIR` | `/srv/projects/drogheda/sitios` | un JSON por sitio (`icave.json`…) |
|
||||
| `PLAN_FILE` | `/srv/projects/drogheda/plan-produccion.json` | el plan del pedido (570 → 113/327/130) |
|
||||
|
||||
Reglas comunes: toda escritura exige el usuario autenticado (`X-Remote-User`; sin él, 403);
|
||||
cada registro guarda `creado_por/creado_en/modificado_por/modificado_en` y un `historial`
|
||||
de `{quien, cuando, cambio}`; **solo un administrador borra** (`ACTAS_ADMINS`). Un `POST`
|
||||
puede traer su propio `id` (lo usa `seed_1013.py` para ser idempotente): si ya existe → 409.
|
||||
|
||||
Siembra inicial (plan, 4 sitios, ítems de inventario, pendientes del 13-oct), idempotente:
|
||||
|
||||
```bash
|
||||
cd /srv/projects/drogheda/hbox-docs && ./backend/venv/bin/python3 backend/seed_1013.py
|
||||
# agrega --usuario leo (omisión) para firmar la siembra; --extra tareas.json para importar más pendientes
|
||||
```
|
||||
|
||||
### Pendientes (tablero kanban) — `tareas`
|
||||
|
||||
Campos: `id`, `titulo`, `descripcion`, `pista` ∈ ingenieria·produccion·mecanica·compras·campo,
|
||||
`estado` ∈ por-hacer·en-curso·bloqueado·hecho, `responsable`, `fecha_limite` (AAAA-MM-DD o null),
|
||||
`enlaces` [{titulo,url}], `bloqueada_por` [ids], `orden`, `etiquetas` [], `version_tarjeta` ∈ V5.5·V5.6·V5.7·v6·—.
|
||||
|
||||
```bash
|
||||
U=leo:PASS; D=https://drogheda.helgen.online
|
||||
curl -s -u $U "$D/api/tareas?pista=ingenieria&estado=en-curso&responsable=michelle"
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/tareas \
|
||||
-d '{"titulo":"V5.6 pasada 2","pista":"ingenieria","responsable":"michelle","estado":"en-curso",
|
||||
"version_tarjeta":"V5.6","fecha_limite":"2026-10-17","enlaces":[{"titulo":"Pasada 2","url":"/hardware/v56-layout-pasada-2/"}]}'
|
||||
# mover de columna (y posición): PATCH parcial; el historial anota quién y qué cambió
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X PATCH "$D/api/tarea?id=T261013-ab12" \
|
||||
-d '{"estado":"hecho","orden":0,"_nota":"commit d9f1 subido"}'
|
||||
# renumerar una columna después de arrastrar (no escribe historial)
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/tareas/orden \
|
||||
-d '{"estado":"en-curso","ids":["T261013-ab12","t-v57-fuente"]}'
|
||||
curl -s -u $U -X DELETE "$D/api/tarea?id=T261013-ab12" # solo admin → 403 para los demás
|
||||
curl -s -u $U $D/api/tareas/resumen # conteo pista×estado + vencidas
|
||||
```
|
||||
|
||||
### Plan de producción — `plan`
|
||||
|
||||
```bash
|
||||
curl -s -u $U $D/api/plan # si no existe el archivo devuelve el plan por omisión con "_default": true
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X PUT $D/api/plan -d '{
|
||||
"pedido_total": 570,
|
||||
"versiones": [{"version":"V5.6","objetivo":113,"nota":"con Mean Well SD-15B","serie_prefijo":"V56","fecha":"2026-11-15"},
|
||||
{"version":"V5.7","objetivo":327,"nota":"fuente a bordo","serie_prefijo":"V57"},
|
||||
{"version":"v6","objetivo":130,"nota":"+ reemplazos futuros","serie_prefijo":"V6"}]}' # solo admin
|
||||
```
|
||||
|
||||
`etapas` (embudo) se puede omitir: se conserva la que ya había. Cada etapa dice de qué tipos de
|
||||
acta se cuenta (`tipos`), si solo cuentan las aprobadas (`ok`) y si solo cuentan unidades HBOX
|
||||
(`solo_unidades`). El tablero de producción hace el conteo en el navegador con `/api/actas`.
|
||||
|
||||
### Inventario — `inventario` + `movimientos`
|
||||
|
||||
El stock **no se edita a mano**: es la suma de los movimientos del ítem (`entrada` +n, `salida` −n,
|
||||
`ajuste` = conteo físico; el servidor calcula el delta). Una salida mayor al stock registrado → 409.
|
||||
|
||||
```bash
|
||||
curl -s -u $U "$D/api/inventario?categoria=fuente&aplica_a=V5.6&alertas=1" # alerta = stock < minimo
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario \
|
||||
-d '{"codigo":"SD-15B-05","descripcion":"Mean Well SD-15B-05","categoria":"fuente","aplica_a":["V5.6"],
|
||||
"unidad":"pza","minimo":113,"ubicacion":"Anaquel B1","proveedor":"Mouser","codigo_proveedor":"709-SD15B05","stock_inicial":0}'
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X PATCH "$D/api/inventario/item?id=SD-15B-05" -d '{"ubicacion":"Anaquel B2","minimo":120}'
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario/movimiento \
|
||||
-d '{"item_id":"SD-15B-05","tipo":"entrada","cantidad":50,"motivo":"llegó pedido Mouser","referencia":"OC-0042"}'
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario/movimiento \
|
||||
-d '{"item_id":"EB-V56","tipo":"salida","cantidad":1,"motivo":"ensamble","referencia":"HBOX5-E07-A010-R0006"}'
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario/movimiento \
|
||||
-d '{"item_id":"EB-V56","tipo":"ajuste","cantidad":12,"motivo":"conteo físico 13-oct"}'
|
||||
curl -s -u $U "$D/api/inventario/movimientos?item_id=EB-V56&n=50"
|
||||
curl -s -u $U -o inventario.csv $D/api/inventario/export.csv
|
||||
```
|
||||
|
||||
### Sitios — `sitios`
|
||||
|
||||
```bash
|
||||
curl -s -u $U "$D/api/sitios" # activos; &todos=1 incluye los inactivos
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/sitios -d '{"id":"icave","nombre":"ICAVE",
|
||||
"cliente":"Hutchison Ports","ubicacion":"Veracruz, Ver.","alias":["Veracruz"],
|
||||
"contactos":[{"nombre":"por confirmar","rol":"supervisor de patio","telefono":"","correo":""}],"notas":""}' # solo admin
|
||||
curl -s -u $U -H 'Content-Type: application/json' -X PATCH "$D/api/sitio?id=icave" -d '{"activo":true,"ubicacion":"Veracruz, Ver."}' # solo admin
|
||||
curl -s -u $U "$D/api/sitio/unidades?id=icave"
|
||||
```
|
||||
|
||||
`/api/sitio/unidades` junta las actas de envío, recepción, instalación, diagnóstico y daño cuyo
|
||||
campo `sitio` sea el slug del sitio (`coincidencia: "sitio"`), y también las que solo coinciden por
|
||||
texto (`terminal` de la flota, destino del envío) con el nombre o un alias (`coincidencia: "texto"`),
|
||||
agrupadas por unidad con su marca de daño vigente.
|
||||
|
||||
143
backend/TESTS-1013.md
Normal file
143
backend/TESTS-1013.md
Normal file
@ -0,0 +1,143 @@
|
||||
# Pruebas del 13-oct — backend (tareas, plan, inventario, sitios) y páginas
|
||||
|
||||
Todo se corrió en el sandbox contra `backend/app.py` arrancado con carpetas temporales
|
||||
(`ACTAS_DIR=<tmp>/actas`; las demás colecciones se deducen de ahí). Sin nginx: el usuario se
|
||||
inyecta con la cabecera `X-Remote-User` tal como la fija nginx en producción. Nada de lo de
|
||||
abajo toca el servidor real.
|
||||
|
||||
## 1 · Backend — `backend/tests-1013.sh` (61 comprobaciones)
|
||||
|
||||
El script arranca `app.py` en un puerto libre, ejercita cada endpoint nuevo con `curl` como
|
||||
**leo** (admin) y como **alan** (técnico), corre la siembra dos veces y una importación `--extra`.
|
||||
Se puede repetir en cualquier máquina con Python 3 + Flask:
|
||||
|
||||
```bash
|
||||
bash backend/tests-1013.sh /ruta/a/hbox-docs 8098
|
||||
```
|
||||
|
||||
Resultado de la corrida (13-oct-2026):
|
||||
|
||||
```
|
||||
## salud y endpoints viejos
|
||||
✓ salud
|
||||
✓ whoami leo admin
|
||||
✓ actas vacías
|
||||
## tareas
|
||||
✓ sin usuario → 403
|
||||
✓ POST (alan)
|
||||
✓ POST id duplicado → 409
|
||||
✓ POST pista inválida → 400
|
||||
✓ POST fecha inválida → 400
|
||||
✓ GET filtro pista
|
||||
✓ GET filtro responsable
|
||||
✓ vencida calculada
|
||||
✓ PATCH mover (alan) → historial con quién
|
||||
✓ PATCH sin cambios
|
||||
✓ PATCH estado inválido → 400
|
||||
✓ PATCH inexistente → 404
|
||||
✓ POST orden (renumera columna: t-b cambia de columna, t-a de posición)
|
||||
✓ resumen
|
||||
✓ resumen vencidas
|
||||
✓ DELETE alan → 403
|
||||
✓ DELETE leo → ok
|
||||
✓ índice CSV escrito
|
||||
## plan
|
||||
✓ GET sin archivo → por omisión
|
||||
✓ PUT alan → 403
|
||||
✓ PUT leo
|
||||
✓ PUT conserva etapas
|
||||
✓ PUT sin versiones → 400
|
||||
## inventario
|
||||
✓ POST ítem (alan)
|
||||
✓ POST código duplicado → 409
|
||||
✓ POST categoría inválida → 400
|
||||
✓ POST con stock_inicial → entrada
|
||||
✓ salida sin stock → 409
|
||||
✓ entrada
|
||||
✓ salida
|
||||
✓ ajuste (conteo 4 → delta −3)
|
||||
✓ sin motivo → 400
|
||||
✓ alerta = stock < mínimo
|
||||
✓ PATCH stock a mano → 400
|
||||
✓ PATCH ubicación → historial
|
||||
✓ movimientos del ítem
|
||||
✓ export CSV
|
||||
✓ export CSV cabecera
|
||||
## sitios
|
||||
✓ POST alan → 403
|
||||
✓ POST leo
|
||||
✓ POST duplicado → 409
|
||||
✓ PATCH alan → 403
|
||||
✓ PATCH leo → historial
|
||||
✓ GET sitios
|
||||
✓ unidades del sitio
|
||||
✓ coincidencia por slug
|
||||
✓ coincidencia por texto (terminal/destino)
|
||||
✓ instalada con fecha
|
||||
✓ marca de daño en la unidad
|
||||
✓ sitio inexistente → 404
|
||||
✓ /api/unidad trae sitio
|
||||
✓ /api/actas trae sitio
|
||||
## siembra (idempotente)
|
||||
✓ primera corrida crea
|
||||
✓ sitios: 3 nuevos (icave ya existía)
|
||||
✓ segunda corrida no duplica
|
||||
✓ inventario no duplica
|
||||
✓ --extra importa
|
||||
✓ resumen tras siembra (1 + 12 sembrados + 1 extra)
|
||||
|
||||
RESULTADO: 61 correctas · 0 fallidas · datos en <tmp>
|
||||
```
|
||||
|
||||
Qué cubre, por colección:
|
||||
|
||||
| Colección | Probado |
|
||||
|---|---|
|
||||
| `tareas` | 403 sin usuario; POST con y sin `id`; 409 por id duplicado; 400 por pista/estado/fecha inválidos; filtros `pista`, `responsable`; `vencida`; PATCH que mueve de columna y deja `historial` con **quién**; PATCH sin cambios; 404; `POST /api/tareas/orden`; `resumen` (conteo y vencidas); DELETE 403 para técnico y ok para admin; `_indice.csv` |
|
||||
| `plan` | GET devuelve el plan por omisión (`_default`) sin archivo; PUT 403 para técnico; PUT admin guarda `actualizado_por` y conserva las `etapas`; 400 sin versiones |
|
||||
| `inventario` | POST ítem; 409 código duplicado (insensible a mayúsculas); 400 categoría inválida; `stock_inicial` → entrada; salida mayor al stock → 409; entrada/salida/ajuste con `delta` y `stock_despues`; 400 sin motivo; `alerta` = stock < mínimo; PATCH `stock` rechazado; PATCH con historial; listado de movimientos; CSV con cabecera `text/csv` |
|
||||
| `sitios` | POST/PATCH 403 para técnico; POST admin; 409 duplicado; PATCH con historial; GET; `/api/sitio/unidades` con coincidencia por slug (`sitio`) y por texto (`terminal`, destino de envío), fecha de instalación, marca de daño; 404 |
|
||||
| actas existentes | `/api/actas` y `/api/unidad` ahora traen `sitio` (y `fecha`, `componentes` en las propias) sin romper lo demás |
|
||||
| `seed_1013.py` | primera corrida crea plan, 4 sitios, 18 ítems, 12 pendientes; la segunda no duplica nada; `--extra archivo.json` importa pendientes adicionales (Notion) de forma idempotente |
|
||||
|
||||
Además, antes del script: `python3 -m py_compile backend/app.py` sin errores; los endpoints
|
||||
viejos (`/api/salud`, `/api/whoami`, `/api/actas`, `/api/acta`, `/api/adjuntos`, `/api/unidad`,
|
||||
`/api/series`, `/api/estado`, `/api/danos`) se usaron desde las páginas durante las pruebas de
|
||||
navegador sin cambios de comportamiento.
|
||||
|
||||
## 2 · Páginas — navegador real (Chromium headless con Playwright)
|
||||
|
||||
Se sirvió `docs/` tal cual con un proxy que reenvía `/api/` al backend y pone `X-Remote-User`
|
||||
(un servidor como **leo** y otro como **alan**). Pruebas en teléfono (390×844, táctil) y escritorio.
|
||||
|
||||
| Página | Probado | Resultado |
|
||||
|---|---|---|
|
||||
| `tableros/kanban.html` | carga 12 pendientes sembrados; en teléfono se ve «Mover a…» y mueve de columna; el historial del detalle dice `alan · estado: por-hacer → en-curso`; alan no ve «Borrar»; crear pendiente con fecha pasada → tarjeta roja (vencida); filtro por pista; en escritorio **arrastrar y soltar** a Hecho; leo borra con confirmación | ✓ sin errores de consola |
|
||||
| `tableros/ingenieria.html` | 4 tarjetas de versión con enlaces; pendientes filtrados a Ingeniería; contador por versión | ✓ |
|
||||
| `tableros/produccion.html` | KPIs, plan vs real, embudo, bloqueos, alertas, daños; leo ve «Editar plan» y guarda una fecha objetivo; alan no ve el botón | ✓ |
|
||||
| `inventario/inventario.html` | 18 ítems; ajuste a 15 desde el diálogo; salida de 99 → error visible «No hay stock suficiente»; historial del ítem; nuevo ítem con stock inicial 3; filtro «solo alertas»; export CSV con `Content-Disposition` | ✓ (el único mensaje de consola es el 409 esperado) |
|
||||
| `formularios/instalacion.html` | lista de sitios desde `/api/sitios`; técnico precargado; unidad «otra»; exige las 3 fotos obligatorias; con fotos subidas envía: «Registro enviado … con 3 foto(s)»; segunda visita muestra el historial | ✓ |
|
||||
| `formularios/envio.html` · `dano.html` | el select de sitio existe; en daño solo aparece al elegir «En campo / transporte / no se sabe» | ✓ |
|
||||
| `sitios/sitio.html` | LZC muestra la unidad recién instalada; ICAVE muestra RTK-014 «por texto» (acta vieja sin sitio) y DAÑADA; alan no ve «Editar sitio»; leo edita ubicación + contacto y crea un sitio nuevo desde el selector | ✓ |
|
||||
| `unidad.html` | bloque «Trazabilidad»: cadena de producción (módulos deducidos del número de unidad cuando no hay ensamble) y cadena de campo con sitio y fecha | ✓ |
|
||||
|
||||
Artefactos de Playwright (no van al repo): capturas `kanban-leo.png`, `produccion.png`,
|
||||
`inventario-movil.png`, `instalacion-movil.png`, `sitio-icave.png`, `unidad.png`.
|
||||
|
||||
Dos falsos negativos que salieron en el camino y son del arnés de prueba, no de las páginas:
|
||||
(1) Playwright hace scroll entre `mousedown` y `dragstart`, así que con una ventana corta
|
||||
arrastraba la tarjeta equivocada — se resolvió con una ventana alta; (2) disparar `change`
|
||||
sintético en el campo «otra unidad» y luego el `change` nativo al perder el foco hacía dos
|
||||
`loadSerie()` — se resolvió haciendo blur normal (Tab), como lo haría un usuario.
|
||||
|
||||
## 3 · Sitio estático
|
||||
|
||||
- `mkdocs build --strict` sobre una copia: **55 advertencias, las mismas 55 de antes** (PDF y
|
||||
binarios que no existen en esta copia). Ninguna menciona `tableros/`, `inventario/`, `sitios/`,
|
||||
`instalacion.html` ni `unidad.html`. Las tres páginas `.md` nuevas se construyen (quedan fuera
|
||||
del `nav` hasta que se aplique la nueva navegación; ver `NUEVOS-1013.md`).
|
||||
- Comprobación de referencias en los HTML nuevos y modificados (todo `src`/`href` relativo y
|
||||
los enlaces `../hardware/…/` dentro del JS): **0 rotas**.
|
||||
- `node --check` sobre `tableros/kanban.js` y `formularios/acta.js`: sin errores de sintaxis.
|
||||
- El `<script>` embebido en `sitios/index.md` sobrevive al render de Markdown (verificado en
|
||||
el HTML generado).
|
||||
@ -12,6 +12,12 @@ Environment=ADJUNTO_MAX_MB=8
|
||||
Environment=ARCHIVO_MAX_MB=20
|
||||
Environment=ACTAS_REPETIBLES=diagnostico-campo,diagnostico-banco,instalacion-campo,reflasheo,dano,envio,recepcion,prueba-bateria-modem,prueba-ethernet-serie
|
||||
Environment=ACTAS_ADMINS=leo
|
||||
# Colecciones del 13-oct (tableros, inventario, sitios). Si no se fijan, el backend las deduce
|
||||
# de ACTAS_DIR: viven junto a /actas. Descomenta solo para moverlas de lugar.
|
||||
#Environment=TAREAS_DIR=/srv/projects/drogheda/tareas
|
||||
#Environment=INVENTARIO_DIR=/srv/projects/drogheda/inventario
|
||||
#Environment=SITIOS_DIR=/srv/projects/drogheda/sitios
|
||||
#Environment=PLAN_FILE=/srv/projects/drogheda/plan-produccion.json
|
||||
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
|
||||
Environment=PASS_MIN=8
|
||||
Environment=PORT=8081
|
||||
|
||||
879
backend/app.py
879
backend/app.py
@ -25,6 +25,15 @@ Rutas:
|
||||
|
||||
Actas compuestas (integracion, ensamble): traen d["componentes"] = {clave: serie_modulo}. En los tipos
|
||||
de TIPOS_UNICOS un módulo no puede aparecer en dos actas reales (409).
|
||||
|
||||
Colecciones agregadas el 13-oct-2026 (tableros, inventario, sitios) — un JSON por registro + _indice.csv,
|
||||
campos de auditoría creado_por/creado_en/modificado_por/modificado_en/historial; cualquier usuario
|
||||
autenticado crea y modifica, solo un administrador borra:
|
||||
GET/POST /api/tareas · PATCH/DELETE /api/tarea?id= · GET /api/tareas/resumen · POST /api/tareas/orden
|
||||
GET /api/plan · PUT /api/plan (admin)
|
||||
GET/POST /api/inventario · PATCH /api/inventario/item?id= · POST /api/inventario/movimiento
|
||||
GET /api/inventario/movimientos?item_id= · GET /api/inventario/export.csv
|
||||
GET /api/sitios · POST /api/sitios (admin) · PATCH /api/sitio?id= (admin) · GET /api/sitio/unidades?id=
|
||||
"""
|
||||
import os, re, json, csv, datetime, uuid, hashlib
|
||||
from flask import Flask, request, jsonify, abort, send_file
|
||||
@ -292,7 +301,8 @@ def listar():
|
||||
"resultado": d.get("resultado"), "recibido": d.get("_recibido")}
|
||||
if isinstance(d.get("componentes"), dict):
|
||||
fila["componentes"] = d["componentes"]
|
||||
for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad", "resumen"):
|
||||
for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad", "resumen",
|
||||
"sitio", "fecha", "maquina_id"):
|
||||
if d.get(k):
|
||||
fila[k] = d[k]
|
||||
if d.get("archivos"):
|
||||
@ -616,10 +626,13 @@ def expediente():
|
||||
"tecnico": d.get("tecnico"), "resultado": d.get("resultado"),
|
||||
"clasificacion": d.get("clasificacion"), "accion": d.get("accion"),
|
||||
"motivo": d.get("motivo"), "maquina": d.get("maquina_id") or d.get("maquina"),
|
||||
"sitio": d.get("sitio") or None,
|
||||
"fotos": [f2.get("ruta") for f2 in (d.get("fotos") or []) if f2.get("ruta")],
|
||||
"archivos": [{"ruta": a.get("ruta"), "nombre": a.get("nombre")}
|
||||
for a in (d.get("archivos") or []) if a.get("ruta")]}
|
||||
if d.get("serie") == serie:
|
||||
if isinstance(d.get("componentes"), dict):
|
||||
fila["componentes"] = d["componentes"] # ensamble/integración: para la cadena de producción
|
||||
actas.append(fila)
|
||||
elif isinstance(d.get("componentes"), dict) and serie in d["componentes"].values():
|
||||
fila["componentes"] = d["componentes"]
|
||||
@ -717,5 +730,869 @@ def danos():
|
||||
return jsonify(ok=True, total=len(out), series=out)
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Colecciones 13-oct-2026: tareas (tablero kanban), plan de producción, inventario, sitios
|
||||
# =====================================================================================
|
||||
# Mismo esquema que las actas: un archivo JSON por registro dentro de su carpeta, más un
|
||||
# _indice.csv que se reescribe en cada cambio (para abrirlo en Excel). Los directorios se
|
||||
# toman de variables de entorno y, si no están, viven junto a ACTAS_DIR.
|
||||
DATOS_BASE = os.path.dirname(BASE.rstrip("/")) or "/srv/projects/drogheda"
|
||||
TAREAS_DIR = os.environ.get("TAREAS_DIR", os.path.join(DATOS_BASE, "tareas"))
|
||||
INVENTARIO_DIR = os.environ.get("INVENTARIO_DIR", os.path.join(DATOS_BASE, "inventario"))
|
||||
SITIOS_DIR = os.environ.get("SITIOS_DIR", os.path.join(DATOS_BASE, "sitios"))
|
||||
PLAN_FILE = os.environ.get("PLAN_FILE", os.path.join(DATOS_BASE, "plan-produccion.json"))
|
||||
|
||||
PISTAS = ("ingenieria", "produccion", "mecanica", "compras", "campo")
|
||||
ESTADOS = ("por-hacer", "en-curso", "bloqueado", "hecho")
|
||||
VERSIONES_TARJETA = ("V5.5", "V5.6", "V5.7", "v6", "—")
|
||||
CATEGORIAS_INV = ("tarjeta", "modulo", "gabinete", "caja", "fuente", "bateria", "antena", "cable",
|
||||
"conector", "componente-critico", "otro")
|
||||
UNIDADES_INV = ("pza", "m")
|
||||
TIPOS_MOV = ("entrada", "salida", "ajuste")
|
||||
# Tipos de acta que "pasan por un sitio": son los que se listan en la página de cada sitio.
|
||||
TIPOS_SITIO = ("envio", "recepcion", "instalacion-campo", "diagnostico-campo", "dano")
|
||||
FECHA_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
|
||||
def ahora():
|
||||
return datetime.datetime.now().replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
def quien():
|
||||
"""Usuario que escribe. Las colecciones nuevas exigen identificarlo: sin X-Remote-User no se escribe."""
|
||||
return current_user()
|
||||
|
||||
|
||||
def _sin_usuario():
|
||||
return jsonify(ok=False, error="No puedo identificarte (falta X-Remote-User en nginx). "
|
||||
"Vuelve a iniciar sesión; si sigue, avisa a Leo."), 403
|
||||
|
||||
|
||||
def _json_body():
|
||||
try:
|
||||
d = request.get_json(force=True)
|
||||
except Exception:
|
||||
return None
|
||||
return d if isinstance(d, dict) else None
|
||||
|
||||
|
||||
def _leer_json(path):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _escribir_json(path, d):
|
||||
"""Escritura atómica: primero a un temporal en la misma carpeta, luego os.replace."""
|
||||
carpeta = os.path.dirname(path) or "."
|
||||
os.makedirs(carpeta, exist_ok=True)
|
||||
tmp = os.path.join(carpeta, ".tmp-" + uuid.uuid4().hex[:8] + ".json")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(d, f, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def _listar_json(carpeta):
|
||||
out = []
|
||||
if not os.path.isdir(carpeta):
|
||||
return out
|
||||
for fn in sorted(os.listdir(carpeta)):
|
||||
if fn.endswith(".json") and not fn.startswith("."):
|
||||
d = _leer_json(os.path.join(carpeta, fn))
|
||||
if isinstance(d, dict):
|
||||
out.append(d)
|
||||
return out
|
||||
|
||||
|
||||
def _reescribir_indice(carpeta, registros, columnas):
|
||||
"""_indice.csv de la colección completa (pequeña): se regenera entero en cada cambio."""
|
||||
if not os.path.isdir(carpeta):
|
||||
return
|
||||
idx = os.path.join(carpeta, "_indice.csv")
|
||||
with open(idx, "w", newline="", encoding="utf-8") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(columnas)
|
||||
for r in registros:
|
||||
fila = []
|
||||
for c in columnas:
|
||||
v = r.get(c, "")
|
||||
if isinstance(v, list):
|
||||
v = " | ".join(str(x.get("titulo", x) if isinstance(x, dict) else x) for x in v)
|
||||
fila.append("" if v is None else v)
|
||||
w.writerow(fila)
|
||||
|
||||
|
||||
def _hist(d, usuario, cambio):
|
||||
d.setdefault("historial", []).append({"quien": usuario, "cuando": ahora(), "cambio": cambio})
|
||||
d["modificado_por"] = usuario
|
||||
d["modificado_en"] = ahora()
|
||||
|
||||
|
||||
def _corto(v, n=60):
|
||||
if isinstance(v, list):
|
||||
return f"[{len(v)}]" if len(v) > 3 else ", ".join(str(x.get("titulo", x) if isinstance(x, dict) else x) for x in v)
|
||||
s = "" if v is None else str(v)
|
||||
return s if len(s) <= n else s[:n - 1] + "…"
|
||||
|
||||
|
||||
def _texto(v, n=4000):
|
||||
return str(v if v is not None else "").strip()[:n]
|
||||
|
||||
|
||||
def _entero(v, defecto=0):
|
||||
try:
|
||||
return int(v)
|
||||
except Exception:
|
||||
return defecto
|
||||
|
||||
|
||||
def _bool(v, defecto=True):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if v in (None, ""):
|
||||
return defecto
|
||||
return str(v).strip().lower() in ("1", "true", "si", "sí", "yes")
|
||||
|
||||
|
||||
def _fecha_o_none(v):
|
||||
s = _texto(v, 10)
|
||||
return s if s and FECHA_RE.match(s) else None
|
||||
|
||||
|
||||
def _enlaces(v):
|
||||
out = []
|
||||
for e in (v if isinstance(v, list) else []):
|
||||
if isinstance(e, dict) and e.get("url"):
|
||||
url = _texto(e.get("url"), 500)
|
||||
if url.lower().startswith(("javascript:", "data:")):
|
||||
continue
|
||||
out.append({"titulo": _texto(e.get("titulo"), 120) or url, "url": url})
|
||||
return out[:30]
|
||||
|
||||
|
||||
def _lista_textos(v, n=30, largo=60):
|
||||
if isinstance(v, str):
|
||||
v = [x for x in re.split(r"[,\n;]+", v)]
|
||||
return [_texto(x, largo) for x in (v if isinstance(v, list) else []) if _texto(x, largo)][:n]
|
||||
|
||||
|
||||
def _ruta_registro(carpeta, rid):
|
||||
return os.path.join(carpeta, rid + ".json")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ tareas (tablero)
|
||||
|
||||
COLS_TAREAS = ["id", "titulo", "pista", "estado", "responsable", "fecha_limite", "version_tarjeta",
|
||||
"orden", "etiquetas", "bloqueada_por", "creado_por", "creado_en", "modificado_por", "modificado_en"]
|
||||
|
||||
|
||||
def _tarea_valida(d, base=None):
|
||||
"""Normaliza los campos de una tarjeta del tablero. Devuelve (tarea, error).
|
||||
Con base=None es un alta (todos los campos con su valor por defecto); con base es un PATCH
|
||||
(solo se tocan las llaves que vienen)."""
|
||||
t = dict(base) if base else {}
|
||||
alta = base is None
|
||||
|
||||
def pon(k, val):
|
||||
t[k] = val
|
||||
|
||||
if alta or "titulo" in d:
|
||||
titulo = _texto(d.get("titulo"), 200)
|
||||
if not titulo:
|
||||
return None, "Falta el título del pendiente."
|
||||
pon("titulo", titulo)
|
||||
if alta or "descripcion" in d:
|
||||
pon("descripcion", _texto(d.get("descripcion"), 4000))
|
||||
if alta or "pista" in d:
|
||||
p = _texto(d.get("pista"), 20).lower() or "produccion"
|
||||
if p not in PISTAS:
|
||||
return None, f"Pista inválida: {p}. Usa una de {', '.join(PISTAS)}."
|
||||
pon("pista", p)
|
||||
if alta or "estado" in d:
|
||||
e = _texto(d.get("estado"), 20).lower() or "por-hacer"
|
||||
if e not in ESTADOS:
|
||||
return None, f"Estado inválido: {e}. Usa uno de {', '.join(ESTADOS)}."
|
||||
pon("estado", e)
|
||||
if alta or "responsable" in d:
|
||||
pon("responsable", _texto(d.get("responsable"), 80))
|
||||
if alta or "fecha_limite" in d:
|
||||
f = d.get("fecha_limite")
|
||||
if f not in (None, "") and not _fecha_o_none(f):
|
||||
return None, "La fecha límite debe ser AAAA-MM-DD."
|
||||
pon("fecha_limite", _fecha_o_none(f))
|
||||
if alta or "enlaces" in d:
|
||||
pon("enlaces", _enlaces(d.get("enlaces")))
|
||||
if alta or "bloqueada_por" in d:
|
||||
pon("bloqueada_por", [slug(x, 60) for x in (d.get("bloqueada_por") or []) if slug(x, 60)][:20])
|
||||
if alta or "orden" in d:
|
||||
pon("orden", _entero(d.get("orden"), 0))
|
||||
if alta or "etiquetas" in d:
|
||||
pon("etiquetas", _lista_textos(d.get("etiquetas"), 15, 40))
|
||||
if alta or "version_tarjeta" in d:
|
||||
v = _texto(d.get("version_tarjeta"), 10) or "—"
|
||||
if v not in VERSIONES_TARJETA:
|
||||
return None, f"Versión de tarjeta inválida: {v}. Usa una de {', '.join(VERSIONES_TARJETA)}."
|
||||
pon("version_tarjeta", v)
|
||||
return t, None
|
||||
|
||||
|
||||
def _guardar_tarea(t):
|
||||
_escribir_json(_ruta_registro(TAREAS_DIR, t["id"]), t)
|
||||
_reescribir_indice(TAREAS_DIR, _listar_json(TAREAS_DIR), COLS_TAREAS)
|
||||
|
||||
|
||||
def _vencida(t, hoy=None):
|
||||
hoy = hoy or datetime.date.today().isoformat()
|
||||
return bool(t.get("fecha_limite")) and t.get("estado") != "hecho" and t["fecha_limite"] < hoy
|
||||
|
||||
|
||||
@app.get("/api/tareas")
|
||||
def tareas_listar():
|
||||
pista = _texto(request.args.get("pista"), 20).lower()
|
||||
estado = _texto(request.args.get("estado"), 20).lower()
|
||||
resp = _texto(request.args.get("responsable"), 80).lower()
|
||||
version = _texto(request.args.get("version"), 10)
|
||||
hoy = datetime.date.today().isoformat()
|
||||
out = []
|
||||
for t in _listar_json(TAREAS_DIR):
|
||||
if pista and t.get("pista") != pista:
|
||||
continue
|
||||
if estado and t.get("estado") != estado:
|
||||
continue
|
||||
if resp and resp not in str(t.get("responsable", "")).lower():
|
||||
continue
|
||||
if version and t.get("version_tarjeta") != version:
|
||||
continue
|
||||
t["vencida"] = _vencida(t, hoy)
|
||||
out.append(t)
|
||||
out.sort(key=lambda t: (ESTADOS.index(t.get("estado")) if t.get("estado") in ESTADOS else 9,
|
||||
t.get("orden", 0), t.get("creado_en", "")))
|
||||
return jsonify(ok=True, total=len(out), tareas=out, pistas=PISTAS, estados=ESTADOS,
|
||||
versiones=VERSIONES_TARJETA, admin=is_admin(), user=current_user())
|
||||
|
||||
|
||||
@app.post("/api/tareas")
|
||||
def tareas_crear():
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
t, err = _tarea_valida(d)
|
||||
if err:
|
||||
return jsonify(ok=False, error=err), 400
|
||||
rid = slug(d.get("id"), 60) or ("T" + datetime.datetime.now().strftime("%y%m%d") + "-" + uuid.uuid4().hex[:4])
|
||||
if os.path.exists(_ruta_registro(TAREAS_DIR, rid)):
|
||||
return jsonify(ok=False, existe=True, error=f"Ya existe el pendiente {rid}."), 409
|
||||
t["id"] = rid
|
||||
if "orden" not in d:
|
||||
# al final de su columna
|
||||
mismos = [x for x in _listar_json(TAREAS_DIR) if x.get("estado") == t["estado"]]
|
||||
t["orden"] = (max([x.get("orden", 0) for x in mismos]) + 1) if mismos else 0
|
||||
t["creado_por"] = u
|
||||
t["creado_en"] = ahora()
|
||||
t["modificado_por"] = u
|
||||
t["modificado_en"] = t["creado_en"]
|
||||
t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "creado en " + t["estado"]}]
|
||||
_guardar_tarea(t)
|
||||
t["vencida"] = _vencida(t)
|
||||
return jsonify(ok=True, tarea=t), 201
|
||||
|
||||
|
||||
@app.patch("/api/tarea")
|
||||
def tarea_modificar():
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
rid = slug(request.args.get("id"), 60)
|
||||
if not rid:
|
||||
return jsonify(ok=False, error="falta id"), 400
|
||||
ruta = _ruta_registro(TAREAS_DIR, rid)
|
||||
base = _leer_json(ruta)
|
||||
if not base:
|
||||
return jsonify(ok=False, error=f"No existe el pendiente {rid}."), 404
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
# id, auditoría e historial no se editan por aquí
|
||||
for k in ("id", "creado_por", "creado_en", "modificado_por", "modificado_en", "historial"):
|
||||
d.pop(k, None)
|
||||
t, err = _tarea_valida(d, base)
|
||||
if err:
|
||||
return jsonify(ok=False, error=err), 400
|
||||
cambios = []
|
||||
for k in d:
|
||||
if k in t and t.get(k) != base.get(k):
|
||||
if k == "orden":
|
||||
continue # la posición dentro de la columna no ensucia el historial
|
||||
cambios.append(f"{k}: {_corto(base.get(k))} → {_corto(t.get(k))}")
|
||||
nota = _texto(d.get("_nota"), 300)
|
||||
if not cambios and not nota:
|
||||
if t.get("orden") != base.get("orden"):
|
||||
# solo cambió la posición: se guarda sin entrada en el historial
|
||||
t["modificado_por"] = u
|
||||
t["modificado_en"] = ahora()
|
||||
_guardar_tarea(t)
|
||||
t["vencida"] = _vencida(t)
|
||||
return jsonify(ok=True, tarea=t)
|
||||
t["vencida"] = _vencida(t)
|
||||
return jsonify(ok=True, tarea=t, sin_cambios=True)
|
||||
_hist(t, u, ("; ".join(cambios) + (" — " + nota if nota else "")) if cambios else nota)
|
||||
_guardar_tarea(t)
|
||||
t["vencida"] = _vencida(t)
|
||||
return jsonify(ok=True, tarea=t)
|
||||
|
||||
|
||||
@app.post("/api/tareas/orden")
|
||||
def tareas_orden():
|
||||
"""Renumera una columna completa: {estado, ids:[...]} → orden 0..n. No escribe historial."""
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
d = _json_body() or {}
|
||||
estado = _texto(d.get("estado"), 20).lower()
|
||||
ids = [slug(x, 60) for x in (d.get("ids") or []) if slug(x, 60)]
|
||||
if estado not in ESTADOS or not ids:
|
||||
return jsonify(ok=False, error="Se esperaba {estado, ids:[...]}"), 400
|
||||
n = 0
|
||||
for i, rid in enumerate(ids):
|
||||
ruta = _ruta_registro(TAREAS_DIR, rid)
|
||||
t = _leer_json(ruta)
|
||||
if not t:
|
||||
continue
|
||||
if t.get("estado") != estado or t.get("orden") != i:
|
||||
if t.get("estado") != estado:
|
||||
_hist(t, u, f"estado: {t.get('estado')} → {estado}")
|
||||
t["estado"] = estado
|
||||
t["orden"] = i
|
||||
t["modificado_por"] = u
|
||||
t["modificado_en"] = ahora()
|
||||
_escribir_json(ruta, t)
|
||||
n += 1
|
||||
if n:
|
||||
_reescribir_indice(TAREAS_DIR, _listar_json(TAREAS_DIR), COLS_TAREAS)
|
||||
return jsonify(ok=True, actualizadas=n)
|
||||
|
||||
|
||||
@app.delete("/api/tarea")
|
||||
def tarea_borrar():
|
||||
if not is_admin():
|
||||
return jsonify(ok=False, error="Solo un administrador puede borrar pendientes. "
|
||||
"Si ya no aplica, muévelo a Hecho."), 403
|
||||
rid = slug(request.args.get("id"), 60)
|
||||
if not rid:
|
||||
return jsonify(ok=False, error="falta id"), 400
|
||||
ruta = _ruta_registro(TAREAS_DIR, rid)
|
||||
if not os.path.isfile(ruta):
|
||||
return jsonify(ok=False, error=f"No existe el pendiente {rid}."), 404
|
||||
os.remove(ruta)
|
||||
_reescribir_indice(TAREAS_DIR, _listar_json(TAREAS_DIR), COLS_TAREAS)
|
||||
return jsonify(ok=True, borrado=rid)
|
||||
|
||||
|
||||
@app.get("/api/tareas/resumen")
|
||||
def tareas_resumen():
|
||||
hoy = datetime.date.today().isoformat()
|
||||
conteo = {p: {e: 0 for e in ESTADOS} for p in PISTAS}
|
||||
vencidas = []
|
||||
total = 0
|
||||
for t in _listar_json(TAREAS_DIR):
|
||||
total += 1
|
||||
p, e = t.get("pista"), t.get("estado")
|
||||
if p in conteo and e in conteo[p]:
|
||||
conteo[p][e] += 1
|
||||
if _vencida(t, hoy):
|
||||
vencidas.append({k: t.get(k) for k in ("id", "titulo", "pista", "estado", "responsable", "fecha_limite")})
|
||||
vencidas.sort(key=lambda x: x.get("fecha_limite") or "")
|
||||
return jsonify(ok=True, total=total, hoy=hoy, por_pista=conteo, vencidas=vencidas)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ plan de producción
|
||||
|
||||
PLAN_DEFECTO = {
|
||||
"pedido_total": 570,
|
||||
"nota": "Pedido de 570 HBOX (13-oct-2026). Los reemplazos futuros se hacen en v6.",
|
||||
"versiones": [
|
||||
{"version": "V5.6", "objetivo": 113, "nota": "con Mean Well SD-15B", "serie_prefijo": "V56", "fecha": None},
|
||||
{"version": "V5.7", "objetivo": 327, "nota": "fuente a bordo", "serie_prefijo": "V57", "fecha": None},
|
||||
{"version": "v6", "objetivo": 130, "nota": "+ reemplazos futuros", "serie_prefijo": "V6", "fecha": None},
|
||||
],
|
||||
# Embudo: cada etapa dice de qué tipo(s) de acta se cuenta. 'ok' = solo resultado distinto de FALLA.
|
||||
"etapas": [
|
||||
{"clave": "tarjeta", "nombre": "Tarjeta probada", "tipos": ["prueba-v5"], "ok": True},
|
||||
{"clave": "modulos", "nombre": "Módulos (ArduSimple y 4G)", "tipos": ["modulo-ard", "modulo-4g"], "ok": True},
|
||||
{"clave": "integracion", "nombre": "Integración", "tipos": ["integracion"], "ok": True},
|
||||
{"clave": "ensamble", "nombre": "Ensamble", "tipos": ["ensamble"], "ok": True},
|
||||
{"clave": "prueba-final", "nombre": "Prueba final", "tipos": ["ensamble"], "ok": True,
|
||||
"nota": "va dentro del acta de ensamble"},
|
||||
{"clave": "configuracion", "nombre": "Configuración", "tipos": ["reflasheo", "prueba-ethernet-serie"], "ok": True,
|
||||
"solo_unidades": True},
|
||||
{"clave": "envio", "nombre": "Envío", "tipos": ["envio"], "ok": False, "solo_unidades": True},
|
||||
{"clave": "instalacion", "nombre": "Instalación", "tipos": ["instalacion-campo"], "ok": True, "solo_unidades": True},
|
||||
],
|
||||
"actualizado_por": None,
|
||||
"actualizado_en": None,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/plan")
|
||||
def plan_leer():
|
||||
p = _leer_json(PLAN_FILE)
|
||||
if not p:
|
||||
p = dict(PLAN_DEFECTO)
|
||||
p["_default"] = True
|
||||
return jsonify(ok=True, plan=p, admin=is_admin())
|
||||
|
||||
|
||||
@app.put("/api/plan")
|
||||
def plan_escribir():
|
||||
if not is_admin():
|
||||
return jsonify(ok=False, error="Solo un administrador puede cambiar el plan."), 403
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
base = _leer_json(PLAN_FILE) or dict(PLAN_DEFECTO)
|
||||
p = {"pedido_total": _entero(d.get("pedido_total", base.get("pedido_total")), 0),
|
||||
"nota": _texto(d.get("nota", base.get("nota")), 500)}
|
||||
vers = []
|
||||
for v in (d.get("versiones") if isinstance(d.get("versiones"), list) else base.get("versiones", [])):
|
||||
if not isinstance(v, dict) or not _texto(v.get("version"), 10):
|
||||
continue
|
||||
vers.append({"version": _texto(v.get("version"), 10), "objetivo": _entero(v.get("objetivo"), 0),
|
||||
"nota": _texto(v.get("nota"), 200), "serie_prefijo": slug(v.get("serie_prefijo"), 10),
|
||||
"fecha": _fecha_o_none(v.get("fecha"))})
|
||||
if not vers:
|
||||
return jsonify(ok=False, error="El plan necesita al menos una versión con objetivo."), 400
|
||||
p["versiones"] = vers[:10]
|
||||
etapas = []
|
||||
for e in (d.get("etapas") if isinstance(d.get("etapas"), list) else base.get("etapas", [])):
|
||||
if not isinstance(e, dict) or not slug(e.get("clave"), 30):
|
||||
continue
|
||||
etapas.append({"clave": slug(e.get("clave"), 30), "nombre": _texto(e.get("nombre"), 80) or e.get("clave"),
|
||||
"tipos": [slug(t) for t in (e.get("tipos") or []) if slug(t)][:6],
|
||||
"ok": _bool(e.get("ok"), True), "solo_unidades": _bool(e.get("solo_unidades"), False),
|
||||
"nota": _texto(e.get("nota"), 200)})
|
||||
p["etapas"] = etapas[:12] or PLAN_DEFECTO["etapas"]
|
||||
p["historial"] = (base.get("historial") or [])[-30:] + [{"quien": u, "cuando": ahora(), "cambio": "plan actualizado"}]
|
||||
p["actualizado_por"] = u
|
||||
p["actualizado_en"] = ahora()
|
||||
_escribir_json(PLAN_FILE, p)
|
||||
return jsonify(ok=True, plan=p)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ inventario
|
||||
|
||||
INV_ITEMS = lambda: os.path.join(INVENTARIO_DIR, "items")
|
||||
INV_MOVS = lambda: os.path.join(INVENTARIO_DIR, "movimientos")
|
||||
COLS_INV = ["id", "codigo", "descripcion", "categoria", "aplica_a", "unidad", "stock", "minimo", "alerta",
|
||||
"ubicacion", "proveedor", "codigo_proveedor", "modificado_por", "modificado_en"]
|
||||
COLS_MOV = ["id", "cuando", "quien", "item_id", "tipo", "cantidad", "delta", "stock_despues", "motivo", "referencia"]
|
||||
|
||||
|
||||
def _item_valido(d, base=None):
|
||||
t = dict(base) if base else {}
|
||||
alta = base is None
|
||||
if alta or "codigo" in d:
|
||||
c = _texto(d.get("codigo"), 40).upper()
|
||||
if not c:
|
||||
return None, "Falta el código del ítem."
|
||||
t["codigo"] = c
|
||||
if alta or "descripcion" in d:
|
||||
desc = _texto(d.get("descripcion"), 200)
|
||||
if not desc:
|
||||
return None, "Falta la descripción."
|
||||
t["descripcion"] = desc
|
||||
if alta or "categoria" in d:
|
||||
cat = _texto(d.get("categoria"), 30).lower() or "otro"
|
||||
if cat not in CATEGORIAS_INV:
|
||||
return None, f"Categoría inválida: {cat}. Usa una de {', '.join(CATEGORIAS_INV)}."
|
||||
t["categoria"] = cat
|
||||
if alta or "aplica_a" in d:
|
||||
t["aplica_a"] = _lista_textos(d.get("aplica_a"), 8, 10)
|
||||
if alta or "unidad" in d:
|
||||
un = _texto(d.get("unidad"), 5).lower() or "pza"
|
||||
if un not in UNIDADES_INV:
|
||||
return None, "Unidad inválida: usa pza o m."
|
||||
t["unidad"] = un
|
||||
if alta or "minimo" in d:
|
||||
t["minimo"] = max(0, _entero(d.get("minimo"), 0))
|
||||
for k, n in (("ubicacion", 120), ("proveedor", 120), ("codigo_proveedor", 60), ("notas", 1000)):
|
||||
if alta or k in d:
|
||||
t[k] = _texto(d.get(k), n)
|
||||
if alta:
|
||||
t["stock"] = 0
|
||||
t["alerta"] = bool(t.get("minimo", 0)) and t.get("stock", 0) < t.get("minimo", 0)
|
||||
return t, None
|
||||
|
||||
|
||||
def _guardar_item(t):
|
||||
t["alerta"] = bool(t.get("minimo", 0)) and t.get("stock", 0) < t.get("minimo", 0)
|
||||
_escribir_json(_ruta_registro(INV_ITEMS(), t["id"]), t)
|
||||
_reescribir_indice(INV_ITEMS(), _listar_json(INV_ITEMS()), COLS_INV)
|
||||
|
||||
|
||||
def _movs_de(item_id):
|
||||
return sorted([m for m in _listar_json(INV_MOVS()) if m.get("item_id") == item_id],
|
||||
key=lambda m: m.get("cuando", ""))
|
||||
|
||||
|
||||
def _stock_desde_movimientos(item_id):
|
||||
return sum(_entero(m.get("delta"), 0) for m in _movs_de(item_id))
|
||||
|
||||
|
||||
@app.get("/api/inventario")
|
||||
def inventario_listar():
|
||||
cat = _texto(request.args.get("categoria"), 30).lower()
|
||||
aplica = _texto(request.args.get("aplica_a"), 10)
|
||||
solo_alertas = request.args.get("alertas") == "1"
|
||||
items = []
|
||||
for it in _listar_json(INV_ITEMS()):
|
||||
it["alerta"] = bool(it.get("minimo", 0)) and it.get("stock", 0) < it.get("minimo", 0)
|
||||
if cat and it.get("categoria") != cat:
|
||||
continue
|
||||
if aplica and aplica not in (it.get("aplica_a") or []):
|
||||
continue
|
||||
if solo_alertas and not it["alerta"]:
|
||||
continue
|
||||
items.append(it)
|
||||
items.sort(key=lambda x: (CATEGORIAS_INV.index(x["categoria"]) if x.get("categoria") in CATEGORIAS_INV else 99,
|
||||
x.get("codigo", "")))
|
||||
return jsonify(ok=True, total=len(items), alertas=sum(1 for x in items if x["alerta"]), items=items,
|
||||
categorias=CATEGORIAS_INV, unidades=UNIDADES_INV, tipos_movimiento=TIPOS_MOV,
|
||||
admin=is_admin(), user=current_user())
|
||||
|
||||
|
||||
@app.post("/api/inventario")
|
||||
def inventario_crear():
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
t, err = _item_valido(d)
|
||||
if err:
|
||||
return jsonify(ok=False, error=err), 400
|
||||
rid = slug(d.get("id"), 40) or slug(t["codigo"], 40)
|
||||
if not rid:
|
||||
return jsonify(ok=False, error="El código debe tener letras o números."), 400
|
||||
if os.path.exists(_ruta_registro(INV_ITEMS(), rid)):
|
||||
return jsonify(ok=False, existe=True, error=f"Ya existe el ítem {rid}."), 409
|
||||
for otro in _listar_json(INV_ITEMS()):
|
||||
if otro.get("codigo") == t["codigo"]:
|
||||
return jsonify(ok=False, existe=True, error=f"El código {t['codigo']} ya lo usa el ítem {otro.get('id')}."), 409
|
||||
t["id"] = rid
|
||||
t["creado_por"] = u
|
||||
t["creado_en"] = ahora()
|
||||
t["modificado_por"] = u
|
||||
t["modificado_en"] = t["creado_en"]
|
||||
t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "ítem creado"}]
|
||||
stock_inicial = _entero(d.get("stock_inicial"), 0)
|
||||
_guardar_item(t)
|
||||
if stock_inicial:
|
||||
_registrar_movimiento(t, "entrada", stock_inicial, "stock inicial", "", u)
|
||||
return jsonify(ok=True, item=t), 201
|
||||
|
||||
|
||||
@app.patch("/api/inventario/item")
|
||||
def inventario_modificar():
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
rid = slug(request.args.get("id"), 40)
|
||||
ruta = _ruta_registro(INV_ITEMS(), rid) if rid else ""
|
||||
base = _leer_json(ruta) if rid else None
|
||||
if not base:
|
||||
return jsonify(ok=False, error=f"No existe el ítem {rid}."), 404
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
if "stock" in d:
|
||||
return jsonify(ok=False, error="El stock no se edita a mano: registra un movimiento (entrada, salida o ajuste)."), 400
|
||||
for k in ("id", "stock", "alerta", "creado_por", "creado_en", "modificado_por", "modificado_en", "historial"):
|
||||
d.pop(k, None)
|
||||
t, err = _item_valido(d, base)
|
||||
if err:
|
||||
return jsonify(ok=False, error=err), 400
|
||||
if "codigo" in d and t["codigo"] != base.get("codigo"):
|
||||
for otro in _listar_json(INV_ITEMS()):
|
||||
if otro.get("id") != rid and otro.get("codigo") == t["codigo"]:
|
||||
return jsonify(ok=False, error=f"El código {t['codigo']} ya lo usa el ítem {otro.get('id')}."), 409
|
||||
cambios = [f"{k}: {_corto(base.get(k))} → {_corto(t.get(k))}" for k in d if k in t and t.get(k) != base.get(k)]
|
||||
if not cambios:
|
||||
return jsonify(ok=True, item=t, sin_cambios=True)
|
||||
_hist(t, u, "; ".join(cambios))
|
||||
_guardar_item(t)
|
||||
return jsonify(ok=True, item=t)
|
||||
|
||||
|
||||
def _registrar_movimiento(item, tipo, cantidad, motivo, referencia, u):
|
||||
"""Escribe el movimiento y recalcula el stock del ítem como suma de todos sus movimientos."""
|
||||
actual = _stock_desde_movimientos(item["id"])
|
||||
if tipo == "entrada":
|
||||
delta = cantidad
|
||||
elif tipo == "salida":
|
||||
delta = -cantidad
|
||||
else: # ajuste: 'cantidad' es el conteo físico; el delta es lo que falta para llegar a él
|
||||
delta = cantidad - actual
|
||||
m = {"id": "M" + datetime.datetime.now().strftime("%y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:4],
|
||||
"item_id": item["id"], "codigo": item.get("codigo"), "tipo": tipo, "cantidad": cantidad, "delta": delta,
|
||||
"motivo": motivo, "referencia": referencia, "quien": u, "cuando": ahora(),
|
||||
"stock_antes": actual, "stock_despues": actual + delta}
|
||||
_escribir_json(_ruta_registro(INV_MOVS(), m["id"]), m)
|
||||
_reescribir_indice(INV_MOVS(), _listar_json(INV_MOVS()), COLS_MOV)
|
||||
item["stock"] = _stock_desde_movimientos(item["id"])
|
||||
item["modificado_por"] = u
|
||||
item["modificado_en"] = m["cuando"]
|
||||
_guardar_item(item)
|
||||
return m
|
||||
|
||||
|
||||
@app.post("/api/inventario/movimiento")
|
||||
def inventario_movimiento():
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
rid = slug(d.get("item_id"), 40)
|
||||
item = _leer_json(_ruta_registro(INV_ITEMS(), rid)) if rid else None
|
||||
if not item:
|
||||
return jsonify(ok=False, error=f"No existe el ítem {rid}."), 404
|
||||
tipo = _texto(d.get("tipo"), 10).lower()
|
||||
if tipo not in TIPOS_MOV:
|
||||
return jsonify(ok=False, error="Tipo de movimiento inválido: entrada, salida o ajuste."), 400
|
||||
try:
|
||||
cantidad = int(d.get("cantidad"))
|
||||
except Exception:
|
||||
return jsonify(ok=False, error="La cantidad debe ser un número entero."), 400
|
||||
if tipo != "ajuste" and cantidad <= 0:
|
||||
return jsonify(ok=False, error="La cantidad debe ser mayor que cero."), 400
|
||||
if tipo == "ajuste" and cantidad < 0:
|
||||
return jsonify(ok=False, error="El conteo físico no puede ser negativo."), 400
|
||||
motivo = _texto(d.get("motivo"), 200)
|
||||
if not motivo:
|
||||
return jsonify(ok=False, error="Escribe el motivo del movimiento."), 400
|
||||
referencia = _texto(d.get("referencia"), 120)
|
||||
if tipo == "salida" and _stock_desde_movimientos(rid) - cantidad < 0:
|
||||
return jsonify(ok=False, error=f"No hay stock suficiente registrado ({_stock_desde_movimientos(rid)} {item.get('unidad')}). "
|
||||
"Registra primero la entrada o haz un ajuste con el conteo físico."), 409
|
||||
m = _registrar_movimiento(item, tipo, cantidad, motivo, referencia, u)
|
||||
return jsonify(ok=True, movimiento=m, item=item), 201
|
||||
|
||||
|
||||
@app.get("/api/inventario/movimientos")
|
||||
def inventario_movimientos():
|
||||
rid = slug(request.args.get("item_id"), 40)
|
||||
n = min(500, max(1, _entero(request.args.get("n"), 200)))
|
||||
movs = _movs_de(rid) if rid else sorted(_listar_json(INV_MOVS()), key=lambda m: m.get("cuando", ""))
|
||||
movs.reverse()
|
||||
return jsonify(ok=True, total=len(movs), movimientos=movs[:n])
|
||||
|
||||
|
||||
@app.get("/api/inventario/export.csv")
|
||||
def inventario_export():
|
||||
import io
|
||||
buf = io.StringIO()
|
||||
w = csv.writer(buf)
|
||||
w.writerow(["codigo", "descripcion", "categoria", "aplica_a", "unidad", "stock", "minimo", "alerta",
|
||||
"ubicacion", "proveedor", "codigo_proveedor", "notas", "modificado_por", "modificado_en"])
|
||||
for it in sorted(_listar_json(INV_ITEMS()), key=lambda x: (x.get("categoria", ""), x.get("codigo", ""))):
|
||||
alerta = bool(it.get("minimo", 0)) and it.get("stock", 0) < it.get("minimo", 0)
|
||||
w.writerow([it.get("codigo"), it.get("descripcion"), it.get("categoria"), " ".join(it.get("aplica_a") or []),
|
||||
it.get("unidad"), it.get("stock", 0), it.get("minimo", 0), "SI" if alerta else "",
|
||||
it.get("ubicacion"), it.get("proveedor"), it.get("codigo_proveedor"), it.get("notas"),
|
||||
it.get("modificado_por"), it.get("modificado_en")])
|
||||
nombre = "inventario-hbox-" + datetime.date.today().isoformat() + ".csv"
|
||||
return app.response_class("" + buf.getvalue(), mimetype="text/csv",
|
||||
headers={"Content-Disposition": f'attachment; filename="{nombre}"'})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ sitios
|
||||
|
||||
COLS_SITIOS = ["id", "nombre", "cliente", "ubicacion", "activo", "contactos", "modificado_por", "modificado_en"]
|
||||
|
||||
|
||||
def _sitio_valido(d, base=None):
|
||||
t = dict(base) if base else {}
|
||||
alta = base is None
|
||||
if alta or "nombre" in d:
|
||||
n = _texto(d.get("nombre"), 80)
|
||||
if not n:
|
||||
return None, "Falta el nombre del sitio."
|
||||
t["nombre"] = n
|
||||
for k, n in (("cliente", 120), ("ubicacion", 200), ("notas", 2000)):
|
||||
if alta or k in d:
|
||||
t[k] = _texto(d.get(k), n)
|
||||
if alta or "alias" in d:
|
||||
t["alias"] = _lista_textos(d.get("alias"), 10, 40)
|
||||
if alta or "contactos" in d:
|
||||
cs = []
|
||||
for c in (d.get("contactos") if isinstance(d.get("contactos"), list) else []):
|
||||
if isinstance(c, dict) and _texto(c.get("nombre"), 80):
|
||||
cs.append({"nombre": _texto(c.get("nombre"), 80), "rol": _texto(c.get("rol"), 80),
|
||||
"telefono": _texto(c.get("telefono"), 40), "correo": _texto(c.get("correo"), 120)})
|
||||
t["contactos"] = cs[:20]
|
||||
if alta or "activo" in d:
|
||||
t["activo"] = _bool(d.get("activo"), True)
|
||||
return t, None
|
||||
|
||||
|
||||
def _guardar_sitio(t):
|
||||
_escribir_json(_ruta_registro(SITIOS_DIR, t["id"]), t)
|
||||
_reescribir_indice(SITIOS_DIR, _listar_json(SITIOS_DIR), COLS_SITIOS)
|
||||
|
||||
|
||||
@app.get("/api/sitios")
|
||||
def sitios_listar():
|
||||
sitios = _listar_json(SITIOS_DIR)
|
||||
if request.args.get("todos") != "1":
|
||||
sitios = [s for s in sitios if s.get("activo", True)]
|
||||
sitios.sort(key=lambda s: (s.get("orden", 50), s.get("nombre", "")))
|
||||
return jsonify(ok=True, total=len(sitios), sitios=sitios, admin=is_admin(), user=current_user())
|
||||
|
||||
|
||||
@app.post("/api/sitios")
|
||||
def sitios_crear():
|
||||
if not is_admin():
|
||||
return jsonify(ok=False, error="Solo un administrador da de alta sitios. Pide a Leo que lo cree."), 403
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
t, err = _sitio_valido(d)
|
||||
if err:
|
||||
return jsonify(ok=False, error=err), 400
|
||||
rid = slug(d.get("id") or t["nombre"], 30).lower()
|
||||
if not rid:
|
||||
return jsonify(ok=False, error="El id del sitio debe tener letras o números (p. ej. icave)."), 400
|
||||
if os.path.exists(_ruta_registro(SITIOS_DIR, rid)):
|
||||
return jsonify(ok=False, existe=True, error=f"Ya existe el sitio {rid}."), 409
|
||||
t["id"] = rid
|
||||
t["orden"] = _entero(d.get("orden"), 50)
|
||||
t["creado_por"] = u
|
||||
t["creado_en"] = ahora()
|
||||
t["modificado_por"] = u
|
||||
t["modificado_en"] = t["creado_en"]
|
||||
t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "sitio creado"}]
|
||||
_guardar_sitio(t)
|
||||
return jsonify(ok=True, sitio=t), 201
|
||||
|
||||
|
||||
@app.patch("/api/sitio")
|
||||
def sitio_modificar():
|
||||
if not is_admin():
|
||||
return jsonify(ok=False, error="Solo un administrador edita los datos de un sitio."), 403
|
||||
u = quien()
|
||||
if not u:
|
||||
return _sin_usuario()
|
||||
rid = slug(request.args.get("id"), 30).lower()
|
||||
base = _leer_json(_ruta_registro(SITIOS_DIR, rid)) if rid else None
|
||||
if not base:
|
||||
return jsonify(ok=False, error=f"No existe el sitio {rid}."), 404
|
||||
d = _json_body()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
for k in ("id", "creado_por", "creado_en", "modificado_por", "modificado_en", "historial"):
|
||||
d.pop(k, None)
|
||||
t, err = _sitio_valido(d, base)
|
||||
if err:
|
||||
return jsonify(ok=False, error=err), 400
|
||||
if "orden" in d:
|
||||
t["orden"] = _entero(d.get("orden"), 50)
|
||||
cambios = [f"{k}: {_corto(base.get(k))} → {_corto(t.get(k))}" for k in d if k in t and t.get(k) != base.get(k)]
|
||||
if not cambios:
|
||||
return jsonify(ok=True, sitio=t, sin_cambios=True)
|
||||
_hist(t, u, "; ".join(cambios))
|
||||
_guardar_sitio(t)
|
||||
return jsonify(ok=True, sitio=t)
|
||||
|
||||
|
||||
def _acta_en_sitio(d, sitio):
|
||||
"""'sitio' si el acta trae el slug del sitio; 'texto' si solo coincide por nombre/alias
|
||||
(terminal de la flota, destino del envío); None si no es de este sitio."""
|
||||
if str(d.get("sitio") or "").lower() == sitio["id"]:
|
||||
return "sitio"
|
||||
claves = {sitio["id"].lower(), str(sitio.get("nombre", "")).lower()} | {a.lower() for a in (sitio.get("alias") or [])}
|
||||
claves.discard("")
|
||||
for campo in ("terminal", "clasificacion", "destino", "donde"):
|
||||
v = str(d.get(campo) or "").strip().lower()
|
||||
if not v:
|
||||
continue
|
||||
for k in claves:
|
||||
if v == k or v.startswith(k + " ") or v.startswith(k + "(") or v.startswith(k + " ("):
|
||||
return "texto"
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/api/sitio/unidades")
|
||||
def sitio_unidades():
|
||||
"""Unidades y actas que pasaron por un sitio (envío, recepción, instalación, diagnóstico, daño)."""
|
||||
rid = slug(request.args.get("id"), 30).lower()
|
||||
sitio = _leer_json(_ruta_registro(SITIOS_DIR, rid)) if rid else None
|
||||
if not sitio:
|
||||
return jsonify(ok=False, error=f"No existe el sitio {rid}."), 404
|
||||
actas = []
|
||||
for tipo in TIPOS_SITIO:
|
||||
carpeta = os.path.join(BASE, tipo)
|
||||
if not os.path.isdir(carpeta):
|
||||
continue
|
||||
for fn in sorted(os.listdir(carpeta)):
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
d = _leer_json(os.path.join(carpeta, fn))
|
||||
if not isinstance(d, dict) or "PRUEBA" in str(d.get("serie", "")).upper():
|
||||
continue
|
||||
modo = _acta_en_sitio(d, sitio)
|
||||
if not modo:
|
||||
continue
|
||||
actas.append({"tipo": tipo, "archivo": fn, "serie": d.get("serie"), "fecha": d.get("fecha"),
|
||||
"recibido": d.get("_recibido"), "tecnico": d.get("tecnico"), "resultado": d.get("resultado"),
|
||||
"accion": d.get("accion"), "clasificacion": d.get("clasificacion"), "motivo": d.get("motivo"),
|
||||
"maquina": d.get("maquina_id") or d.get("maquina"), "resumen": d.get("resumen"),
|
||||
"coincidencia": modo, "fotos": len(d.get("fotos") or [])})
|
||||
|
||||
def _clave(r):
|
||||
return ((r.get("fecha") or (r.get("recibido") or "")[:10]), r.get("recibido") or "")
|
||||
actas.sort(key=_clave, reverse=True)
|
||||
unidades = {}
|
||||
for a in actas:
|
||||
s = a.get("serie")
|
||||
if not s:
|
||||
continue
|
||||
u = unidades.setdefault(s, {"serie": s, "maquina": None, "instalada": None, "ultima": None, "actas": 0,
|
||||
"tipos": [], "coincidencia": "texto"})
|
||||
u["actas"] += 1
|
||||
if a["tipo"] not in u["tipos"]:
|
||||
u["tipos"].append(a["tipo"])
|
||||
if a.get("maquina") and not u["maquina"]:
|
||||
u["maquina"] = a["maquina"]
|
||||
if a["coincidencia"] == "sitio":
|
||||
u["coincidencia"] = "sitio"
|
||||
if a["tipo"] == "instalacion-campo" and a.get("resultado") != "FALLA":
|
||||
f = a.get("fecha") or (a.get("recibido") or "")[:10]
|
||||
if not u["instalada"] or f > u["instalada"]:
|
||||
u["instalada"] = f
|
||||
if not u["ultima"]:
|
||||
u["ultima"] = {"tipo": a["tipo"], "fecha": a.get("fecha") or (a.get("recibido") or "")[:10],
|
||||
"accion": a.get("accion"), "resultado": a.get("resultado")}
|
||||
for s, u in unidades.items():
|
||||
e = estado_serie(s)
|
||||
u["marca"] = e["marca"]
|
||||
u["marca_acta"] = e["acta"]
|
||||
lista = sorted(unidades.values(), key=lambda u: (u["marca"] is None, u["serie"]))
|
||||
return jsonify(ok=True, sitio=sitio, unidades=lista, actas=actas, total_unidades=len(lista),
|
||||
total_actas=len(actas), danos=[u for u in lista if u["marca"]], admin=is_admin())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=int(os.environ.get("PORT", "8081")))
|
||||
|
||||
239
backend/seed_1013.py
Normal file
239
backend/seed_1013.py
Normal file
@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Siembra inicial del 13-oct-2026: plan de producción, los 4 sitios, los ítems de inventario
|
||||
y los pendientes del tablero (brief §4). Idempotente: lo que ya existe (mismo id) se deja como está.
|
||||
|
||||
Uso (en el servidor, como el usuario del servicio, normalmente leo):
|
||||
cd /srv/projects/drogheda/hbox-docs
|
||||
./backend/venv/bin/python3 backend/seed_1013.py # usa los mismos valores por omisión que app.py
|
||||
TAREAS_DIR=/otra/ruta ./backend/venv/bin/python3 backend/seed_1013.py --usuario leo
|
||||
./backend/venv/bin/python3 backend/seed_1013.py --extra tareas-notion.json # importa más pendientes
|
||||
./backend/venv/bin/python3 backend/seed_1013.py --solo plan,sitios # una parte
|
||||
|
||||
Escribe directo en disco con las mismas funciones del backend (no pasa por nginx), así que no
|
||||
necesita contraseña. El servicio puede seguir corriendo mientras se siembra.
|
||||
|
||||
--extra espera un JSON con una lista de pendientes con los mismos campos que POST /api/tareas
|
||||
(cada uno con su "id" para que la importación también sea idempotente).
|
||||
"""
|
||||
import os, sys, json, argparse, datetime
|
||||
|
||||
AQUI = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, AQUI)
|
||||
import app as backend # noqa: E402 (usa ACTAS_DIR/TAREAS_DIR/INVENTARIO_DIR/SITIOS_DIR/PLAN_FILE del entorno)
|
||||
|
||||
HOY = datetime.date.today().isoformat()
|
||||
|
||||
|
||||
def marca(u):
|
||||
t = backend.ahora()
|
||||
return {"creado_por": u, "creado_en": t, "modificado_por": u, "modificado_en": t}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ datos
|
||||
|
||||
SITIOS = [
|
||||
{"id": "icave", "nombre": "ICAVE", "cliente": "Hutchison Ports", "ubicacion": "Veracruz, Ver.",
|
||||
"alias": ["ICAVE (Veracruz)", "Oficina ICAVE"], "orden": 10,
|
||||
"contactos": [{"nombre": "por confirmar", "rol": "supervisor de patio (autoriza acceso a máquinas)", "telefono": "", "correo": ""}],
|
||||
"notas": "Terminal de contenedores. El acceso a cada máquina se pide por correo (Operaciones). "
|
||||
"Compara el HBOX RTK con un CalAmp legado (viajes en Wialon)."},
|
||||
{"id": "lzc", "nombre": "LZC", "cliente": "Hutchison Ports", "ubicacion": "Lázaro Cárdenas, Mich.",
|
||||
"alias": ["LCT", "Lázaro Cárdenas"], "orden": 20,
|
||||
"contactos": [{"nombre": "por confirmar", "rol": "", "telefono": "", "correo": ""}],
|
||||
"notas": "En notas antiguas aparece como LCT."},
|
||||
{"id": "eit", "nombre": "EIT", "cliente": "Hutchison Ports", "ubicacion": "por confirmar",
|
||||
"alias": [], "orden": 30,
|
||||
"contactos": [{"nombre": "por confirmar", "rol": "", "telefono": "", "correo": ""}], "notas": ""},
|
||||
{"id": "timsa", "nombre": "TIMSA", "cliente": "Hutchison Ports", "ubicacion": "por confirmar",
|
||||
"alias": [], "orden": 40,
|
||||
"contactos": [{"nombre": "por confirmar", "rol": "", "telefono": "", "correo": ""}], "notas": ""},
|
||||
]
|
||||
|
||||
# stock 0: Leo y Jesús lo llenan con el conteo físico (movimiento 'ajuste').
|
||||
INVENTARIO = [
|
||||
("EB-V55", "EinarBoard V5.5 (tarjeta probada o por probar)", "tarjeta", ["V5.5"], 0, "JLCPCB", "", "Lote de 15 (22-ago). Inspeccionar U6 con lupa antes de energizar."),
|
||||
("EB-V56", "EinarBoard V5.6", "tarjeta", ["V5.6"], 0, "Aldair (por confirmar)", "", "Pedido 113. Pasta EP U6 ≤50 %, vías tapadas, X-ray/AOI en U6."),
|
||||
("EB-V57", "EinarBoard V5.7 (fuente a bordo)", "tarjeta", ["V5.7"], 0, "Aldair (por confirmar)", "", "Pedido 327."),
|
||||
("EB-V6", "EinarBoard v6 (GNSS + 4G a bordo)", "tarjeta", ["v6"], 0, "Aldair (por confirmar)", "", "Pedido 130 + reemplazos."),
|
||||
("ARD-SRTK2B", "ArduSimple simpleRTK2B Pro (ZED-F9P)", "modulo", ["V5.5", "V5.6", "V5.7"], 0, "ArduSimple", "", "500 pedidos (26-sep)."),
|
||||
("MODEM-4G-NTRIP", "4G NTRIP Master", "modulo", ["V5.5", "V5.6", "V5.7"], 0, "ArduSimple", "", ""),
|
||||
("SD-15B-05", "Fuente Mean Well SD-15B-05 (24 V → 5 V)", "fuente", ["V5.5", "V5.6"], 113, "Mouser / Digikey", "709-SD15B05", "Solo V5.6: 113 para el pedido. V5.7 y v6 no la usan."),
|
||||
("PACK-LFP-26650-1S4P", "Paquete LFP 26650 1S4P (alta temperatura)", "bateria", ["V5.6", "V5.7", "v6"], 0, "por confirmar", "", "JP2 = 0 (LiFePO4). Flotación 3.50 V (R55 5.6 k)."),
|
||||
("GAB-IP67", "Gabinete IP67 (máquina)", "gabinete", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", ""),
|
||||
("CAJA-HBOX", "Caja plástica HBOX", "caja", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "Pendiente: el cable Ethernet no cabe (tablero, pista Mecánica)."),
|
||||
("ANT-GNSS", "Antena GNSS (SMA, con cable)", "antena", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "~4 semanas de entrega."),
|
||||
("ANT-LTE", "Antena LTE (2 por unidad)", "antena", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "Dos por unidad."),
|
||||
("CABLE-ETH", "Cable Ethernet (exterior)", "cable", ["V5.6", "V5.7", "v6"], 0, "por confirmar", "", "Ver pendiente de la caja HBOX."),
|
||||
("CONN-GX16", "Conector GX16 (aéreo + panel) y arnés", "conector", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", "1 = +24 V, 2 = GND, 3 = ignición, 4 = GND."),
|
||||
("FUSIBLE-DIN", "Fusible DIN (gabinete)", "componente-critico", ["V5.5", "V5.6", "V5.7", "v6"], 0, "por confirmar", "", ""),
|
||||
("LTC4040", "LTC4040 (U6, QFN 4x5) — cargador/respaldo", "componente-critico", ["V5.5", "V5.6", "V5.7", "v6"], 0, "LCSC / ADI", "C2057152", "Largo plazo de entrega. Regla DFM: pasta EP ≤50 %."),
|
||||
("TPS54360B", "TPS54360B — buck 24 V → 5 V a bordo (U25)", "componente-critico", ["V5.7", "v6"], 0, "LCSC / TI", "", "Fuente a bordo de V5.7 y v6."),
|
||||
("SRP1265A", "Inductor SRP1265A (fuente a bordo)", "componente-critico", ["V5.7", "v6"], 0, "LCSC / Bourns", "", ""),
|
||||
]
|
||||
|
||||
TAREAS = [
|
||||
{"id": "t-v56-pasada-2", "pista": "ingenieria", "estado": "en-curso", "responsable": "michelle", "version_tarjeta": "V5.6",
|
||||
"titulo": "V5.6 pasada 2 — correcciones antes del pedido",
|
||||
"descripcion": "BAT_PLUS fuera de In1/In2 (bloqueante), MAIN5V, SYS5V, SW3_NODE, pistas bajo U16 a B.Cu, sueltos, rev 5.6, stackup. "
|
||||
"Los conectores se quedan donde están (opción A). Zona x 170–195 / y 42–72 libre para la fuente de V5.7.",
|
||||
"enlaces": [{"titulo": "Instrucciones pasada 2", "url": "/hardware/v56-layout-pasada-2/"},
|
||||
{"titulo": "Auditoría 13-oct (inglés)", "url": "/hardware/v56-auditoria-layout-13oct/"}],
|
||||
"etiquetas": ["layout", "bloqueante"]},
|
||||
{"id": "t-v57-fuente", "pista": "ingenieria", "estado": "en-curso", "responsable": "leo", "version_tarjeta": "V5.7",
|
||||
"titulo": "V5.7 — colocar y rutear la fuente a bordo (guía de layout)",
|
||||
"descripcion": "Bloque U25 (TPS54360B) + F2 SMD en la esquina superior derecha. Solo las zonas acordadas con Michelle.",
|
||||
"enlaces": [{"titulo": "Guía de layout V5.7", "url": "/hardware/v57-layout-guia/"},
|
||||
{"titulo": "ECO V5.7", "url": "/hardware/v57-eco-fuente/"}],
|
||||
"etiquetas": ["layout"]},
|
||||
{"id": "t-v57-cierre", "pista": "ingenieria", "estado": "por-hacer", "responsable": "michelle", "version_tarjeta": "V5.7",
|
||||
"titulo": "V5.7 — replicar correcciones de V5.6 y cerrar layout",
|
||||
"descripcion": "Después de que Leo deje la fuente colocada y V5.6 pasada 2 esté cerrada.",
|
||||
"bloqueada_por": ["t-v56-pasada-2", "t-v57-fuente"],
|
||||
"enlaces": [{"titulo": "Guía de layout V5.7", "url": "/hardware/v57-layout-guia/"}], "etiquetas": ["layout"]},
|
||||
{"id": "t-v56-revision-gerber", "pista": "ingenieria", "estado": "por-hacer", "responsable": "claude", "version_tarjeta": "V5.6",
|
||||
"titulo": "V5.6 — revisión pad a pad + Gerber/BOM/CPL",
|
||||
"descripcion": "Paridad con netlist, DRC, pasta EP U6 ≤50 %, vías tapadas; generar el paquete de fabricación.",
|
||||
"bloqueada_por": ["t-v56-pasada-2"], "etiquetas": ["auditoría", "fabricación"]},
|
||||
{"id": "t-v6-cierre-pedido", "pista": "ingenieria", "estado": "por-hacer", "responsable": "leo, claude", "version_tarjeta": "v6",
|
||||
"titulo": "v6 — cierre para pedido (según «Próximos pasos para ordenar»)",
|
||||
"enlaces": [{"titulo": "Próximos pasos v6", "url": "/hardware/v6-proximos-pasos/"},
|
||||
{"titulo": "Plan de corrección", "url": "/hardware/v6-plan-correccion/"}],
|
||||
"descripcion": "Aplicar a P7/P8 lo decidido para P12 en V5.6; regla DFM U6; no habrá v6.1.", "etiquetas": ["fabricación"]},
|
||||
{"id": "t-caja-ethernet", "pista": "mecanica", "estado": "bloqueado", "responsable": "", "version_tarjeta": "—",
|
||||
"titulo": "Caja plástica HBOX — el cable Ethernet no cabe: definir y aplicar el cambio",
|
||||
"descripcion": "Falta definir la causa exacta (largo del boot RJ45, tamaño de la abertura o radio de curvatura) y quién lo corrige "
|
||||
"(pieza impresa en 3D o proveedor). Al definirlo: escribir el procedimiento y mover a En curso.",
|
||||
"etiquetas": ["caja", "por asignar"]},
|
||||
{"id": "t-paquetes-aldair", "pista": "compras", "estado": "por-hacer", "responsable": "leo", "version_tarjeta": "—",
|
||||
"titulo": "Enviar paquetes de fabricación V5.6, V5.7 y v6 a Aldair; cotización para 570 unidades (113 / 327 / 130)",
|
||||
"descripcion": "Gerber + BOM + CPL + planos. Aldair = fabricación/ensamble (rol por confirmar).",
|
||||
"bloqueada_por": ["t-v56-revision-gerber"], "etiquetas": ["pedido 570"]},
|
||||
{"id": "t-compras-materiales", "pista": "compras", "estado": "por-hacer", "responsable": "leo, jesús", "version_tarjeta": "—",
|
||||
"titulo": "Mean Well SD-15B-05 ×113 (solo V5.6); paquetes LFP; gabinetes; cajas HBOX; antenas",
|
||||
"descripcion": "Chips y antenas RTK: ~4 semanas de entrega. Registrar las llegadas como entradas en el inventario.",
|
||||
"enlaces": [{"titulo": "Inventario", "url": "/inventario/inventario.html"}], "etiquetas": ["pedido 570"]},
|
||||
{"id": "t-plan-570", "pista": "produccion", "estado": "por-hacer", "responsable": "leo", "version_tarjeta": "—",
|
||||
"titulo": "Plan 570: cargar objetivos por versión y fechas",
|
||||
"enlaces": [{"titulo": "Tablero de producción", "url": "/tableros/produccion.html"}],
|
||||
"descripcion": "Editar el plan desde el tablero de producción (solo admin): fecha objetivo por versión."},
|
||||
{"id": "t-inventario-inicial", "pista": "produccion", "estado": "por-hacer", "responsable": "jesús", "version_tarjeta": "—",
|
||||
"titulo": "Inventario inicial: conteo físico de tarjetas, módulos, gabinetes, cajas, packs",
|
||||
"enlaces": [{"titulo": "Inventario", "url": "/inventario/inventario.html"}],
|
||||
"descripcion": "Un movimiento de tipo «ajuste» por ítem con el conteo físico y el motivo «conteo inicial»."},
|
||||
{"id": "t-sitios-alta", "pista": "campo", "estado": "por-hacer", "responsable": "leo", "version_tarjeta": "—",
|
||||
"titulo": "Dar de alta los 4 sitios (ICAVE, LZC, EIT, TIMSA) con contactos y unidades instaladas",
|
||||
"enlaces": [{"titulo": "Sitios", "url": "/sitios/"}],
|
||||
"descripcion": "Los 4 sitios ya existen con «por confirmar»: completar ubicación y contactos (Editar sitio, solo admin)."},
|
||||
{"id": "t-acta-instalacion", "pista": "campo", "estado": "por-hacer", "responsable": "claude", "version_tarjeta": "—",
|
||||
"titulo": "Acta de instalación en campo: formulario nuevo + instrucciones",
|
||||
"enlaces": [{"titulo": "Acta de instalación", "url": "/formularios/instalacion.html"}],
|
||||
"descripcion": "Formulario publicado el 13-oct; falta probarlo en una instalación real y ajustar el checklist."},
|
||||
]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ siembra
|
||||
|
||||
def sembrar_plan(u, fuerza=False):
|
||||
if os.path.isfile(backend.PLAN_FILE) and not fuerza:
|
||||
return "plan: ya existe, se deja (usa --forzar-plan para reescribirlo)"
|
||||
p = json.loads(json.dumps(backend.PLAN_DEFECTO))
|
||||
p["actualizado_por"] = u
|
||||
p["actualizado_en"] = backend.ahora()
|
||||
p["historial"] = [{"quien": u, "cuando": p["actualizado_en"], "cambio": "plan sembrado (seed_1013)"}]
|
||||
backend._escribir_json(backend.PLAN_FILE, p)
|
||||
return "plan: creado (570 → 113 V5.6 / 327 V5.7 / 130 v6)"
|
||||
|
||||
|
||||
def sembrar_sitios(u):
|
||||
n = 0
|
||||
for s in SITIOS:
|
||||
if os.path.isfile(backend._ruta_registro(backend.SITIOS_DIR, s["id"])):
|
||||
continue
|
||||
t, err = backend._sitio_valido(s)
|
||||
if err:
|
||||
print(" sitio", s["id"], "->", err); continue
|
||||
t["id"] = s["id"]
|
||||
t["orden"] = s.get("orden", 50)
|
||||
t.update(marca(u))
|
||||
t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "sitio sembrado (seed_1013)"}]
|
||||
backend._guardar_sitio(t)
|
||||
n += 1
|
||||
return f"sitios: {n} creados, {len(SITIOS) - n} ya existían"
|
||||
|
||||
|
||||
def sembrar_inventario(u):
|
||||
n = 0
|
||||
for codigo, desc, cat, aplica, minimo, prov, cod_prov, notas in INVENTARIO:
|
||||
rid = backend.slug(codigo, 40)
|
||||
if os.path.isfile(backend._ruta_registro(backend.INV_ITEMS(), rid)):
|
||||
continue
|
||||
t, err = backend._item_valido({"codigo": codigo, "descripcion": desc, "categoria": cat, "aplica_a": aplica,
|
||||
"unidad": "pza", "minimo": minimo, "proveedor": prov,
|
||||
"codigo_proveedor": cod_prov, "notas": notas, "ubicacion": ""})
|
||||
if err:
|
||||
print(" item", codigo, "->", err); continue
|
||||
t["id"] = rid
|
||||
t.update(marca(u))
|
||||
t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": "ítem sembrado (seed_1013), stock 0"}]
|
||||
backend._guardar_item(t)
|
||||
n += 1
|
||||
return f"inventario: {n} ítems creados, {len(INVENTARIO) - n} ya existían (stock 0: falta el conteo físico)"
|
||||
|
||||
|
||||
def sembrar_tareas(u, lista, origen="seed_1013"):
|
||||
n, orden = 0, {}
|
||||
existentes = backend._listar_json(backend.TAREAS_DIR)
|
||||
for t in existentes:
|
||||
orden[t.get("estado")] = max(orden.get(t.get("estado"), -1), t.get("orden", 0))
|
||||
for d in lista:
|
||||
rid = backend.slug(d.get("id"), 60)
|
||||
if not rid:
|
||||
print(" pendiente sin id, se omite:", d.get("titulo")); continue
|
||||
if os.path.isfile(backend._ruta_registro(backend.TAREAS_DIR, rid)):
|
||||
continue
|
||||
t, err = backend._tarea_valida(d)
|
||||
if err:
|
||||
print(" pendiente", rid, "->", err); continue
|
||||
t["id"] = rid
|
||||
if "orden" not in d:
|
||||
orden[t["estado"]] = orden.get(t["estado"], -1) + 1
|
||||
t["orden"] = orden[t["estado"]]
|
||||
t.update(marca(u))
|
||||
t["historial"] = [{"quien": u, "cuando": t["creado_en"], "cambio": f"creado en {t['estado']} ({origen})"}]
|
||||
backend._guardar_tarea(t)
|
||||
n += 1
|
||||
return f"pendientes ({origen}): {n} creados, {len(lista) - n} ya existían u omitidos"
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--usuario", default="leo", help="quién firma la siembra en los campos de auditoría")
|
||||
ap.add_argument("--solo", default="plan,sitios,inventario,tareas", help="partes a sembrar, separadas por coma")
|
||||
ap.add_argument("--extra", help="JSON con una lista de pendientes adicionales (p. ej. importación de Notion)")
|
||||
ap.add_argument("--forzar-plan", action="store_true", help="reescribe el plan aunque ya exista")
|
||||
a = ap.parse_args()
|
||||
partes = {p.strip() for p in a.solo.split(",")}
|
||||
print("Carpetas:", {"tareas": backend.TAREAS_DIR, "inventario": backend.INVENTARIO_DIR,
|
||||
"sitios": backend.SITIOS_DIR, "plan": backend.PLAN_FILE})
|
||||
for d in (backend.TAREAS_DIR, backend.INV_ITEMS(), backend.INV_MOVS(), backend.SITIOS_DIR,
|
||||
os.path.dirname(backend.PLAN_FILE)):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
if "plan" in partes:
|
||||
print(sembrar_plan(a.usuario, a.forzar_plan))
|
||||
if "sitios" in partes:
|
||||
print(sembrar_sitios(a.usuario))
|
||||
if "inventario" in partes:
|
||||
print(sembrar_inventario(a.usuario))
|
||||
if "tareas" in partes:
|
||||
print(sembrar_tareas(a.usuario, TAREAS))
|
||||
if a.extra:
|
||||
with open(a.extra, encoding="utf-8") as f:
|
||||
extra = json.load(f)
|
||||
if not isinstance(extra, list):
|
||||
sys.exit("--extra debe ser una lista JSON de pendientes")
|
||||
print(sembrar_tareas(a.usuario, extra, origen=os.path.basename(a.extra)))
|
||||
print("Listo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
100
backend/tests-1013.sh
Executable file
100
backend/tests-1013.sh
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pruebas del backend 13-oct: arranca app.py con carpetas temporales y ejercita cada endpoint nuevo.
|
||||
# Uso: bash tests-1013.sh <ruta-a-hbox-docs> [puerto]
|
||||
set -u
|
||||
REPO=${1:?ruta a hbox-docs}; PORT=${2:-8098}
|
||||
T=$(mktemp -d); export ACTAS_DIR=$T/actas PORT
|
||||
mkdir -p $ACTAS_DIR
|
||||
python3 $REPO/backend/app.py > $T/app.log 2>&1 &
|
||||
APP=$!; trap "kill $APP 2>/dev/null" EXIT; sleep 1.5
|
||||
U="http://127.0.0.1:$PORT"
|
||||
LEO=(-s -H "X-Remote-User: leo" -H "Content-Type: application/json")
|
||||
ALAN=(-s -H "X-Remote-User: alan" -H "Content-Type: application/json")
|
||||
ok=0; fail=0
|
||||
check() { # check "nombre" "esperado" "obtenido"
|
||||
if [[ "$3" == *"$2"* ]]; then ok=$((ok+1)); echo " ✓ $1"; else fail=$((fail+1)); echo " ✕ $1 — esperaba «$2», obtuve: ${3:0:160}"; fi; }
|
||||
|
||||
echo "## salud y endpoints viejos"
|
||||
check "salud" '"servicio":"actas-hbox"' "$(curl -s $U/api/salud)"
|
||||
check "whoami leo admin" '"admin":true' "$(curl "${LEO[@]}" $U/api/whoami)"
|
||||
check "actas vacías" '"total":0' "$(curl -s $U/api/actas)"
|
||||
|
||||
echo "## tareas"
|
||||
check "sin usuario → 403" "No puedo identificarte" "$(curl -s -H 'Content-Type: application/json' -X POST $U/api/tareas -d '{"titulo":"x"}')"
|
||||
check "POST (alan)" '"id":"t-a"' "$(curl "${ALAN[@]}" -X POST $U/api/tareas -d '{"id":"t-a","titulo":"Pendiente A","pista":"ingenieria","responsable":"michelle","fecha_limite":"2026-10-01","version_tarjeta":"V5.6","enlaces":[{"titulo":"ECO","url":"/hardware/v56-eco/"}]}')"
|
||||
check "POST id duplicado → 409" '"existe":true' "$(curl "${ALAN[@]}" -X POST $U/api/tareas -d '{"id":"t-a","titulo":"dup"}')"
|
||||
check "POST pista inválida → 400" "Pista inv" "$(curl "${ALAN[@]}" -X POST $U/api/tareas -d '{"titulo":"x","pista":"ventas"}')"
|
||||
check "POST fecha inválida → 400" "AAAA-MM-DD" "$(curl "${ALAN[@]}" -X POST $U/api/tareas -d '{"titulo":"x","fecha_limite":"13/10/2026"}')"
|
||||
curl "${LEO[@]}" -X POST $U/api/tareas -d '{"id":"t-b","titulo":"Pendiente B","pista":"compras","bloqueada_por":["t-a"]}' > /dev/null
|
||||
check "GET filtro pista" '"total":1' "$(curl -s "$U/api/tareas?pista=compras")"
|
||||
check "GET filtro responsable" '"total":1' "$(curl -s "$U/api/tareas?responsable=mich")"
|
||||
check "vencida calculada" '"vencida":true' "$(curl -s "$U/api/tareas?pista=ingenieria")"
|
||||
check "PATCH mover (alan) → historial con quién" '"quien":"alan"' "$(curl "${ALAN[@]}" -X PATCH "$U/api/tarea?id=t-a" -d '{"estado":"en-curso","orden":0}')"
|
||||
check "PATCH sin cambios" '"sin_cambios":true' "$(curl "${ALAN[@]}" -X PATCH "$U/api/tarea?id=t-a" -d '{"estado":"en-curso"}')"
|
||||
check "PATCH estado inválido → 400" "Estado inv" "$(curl "${ALAN[@]}" -X PATCH "$U/api/tarea?id=t-a" -d '{"estado":"listo"}')"
|
||||
check "PATCH inexistente → 404" "No existe" "$(curl "${ALAN[@]}" -X PATCH "$U/api/tarea?id=nada" -d '{"titulo":"x"}')"
|
||||
check "POST orden (renumera columna: t-b cambia de columna, t-a de posición)" '"actualizadas":2' "$(curl "${ALAN[@]}" -X POST $U/api/tareas/orden -d '{"estado":"en-curso","ids":["t-b","t-a"]}')"
|
||||
check "resumen" '"en-curso": 1' "$(curl -s $U/api/tareas/resumen | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)["por_pista"]["ingenieria"]))')"
|
||||
check "resumen vencidas" '"id":"t-a"' "$(curl -s $U/api/tareas/resumen)"
|
||||
check "DELETE alan → 403" "Solo un administrador" "$(curl "${ALAN[@]}" -X DELETE "$U/api/tarea?id=t-b")"
|
||||
check "DELETE leo → ok" '"borrado":"t-b"' "$(curl "${LEO[@]}" -X DELETE "$U/api/tarea?id=t-b")"
|
||||
check "índice CSV escrito" "t-a" "$(cat $T/tareas/_indice.csv)"
|
||||
|
||||
echo "## plan"
|
||||
check "GET sin archivo → por omisión" '"_default":true' "$(curl -s $U/api/plan)"
|
||||
check "PUT alan → 403" "Solo un administrador" "$(curl "${ALAN[@]}" -X PUT $U/api/plan -d '{"pedido_total":570}')"
|
||||
check "PUT leo" '"actualizado_por":"leo"' "$(curl "${LEO[@]}" -X PUT $U/api/plan -d '{"pedido_total":570,"versiones":[{"version":"V5.6","objetivo":113,"serie_prefijo":"V56","fecha":"2026-11-30"},{"version":"V5.7","objetivo":327},{"version":"v6","objetivo":130}]}')"
|
||||
check "PUT conserva etapas" '"clave":"instalacion"' "$(curl -s $U/api/plan)"
|
||||
check "PUT sin versiones → 400" "al menos una versi" "$(curl "${LEO[@]}" -X PUT $U/api/plan -d '{"versiones":[]}')"
|
||||
|
||||
echo "## inventario"
|
||||
check "POST ítem (alan)" '"id":"EB-V56"' "$(curl "${ALAN[@]}" -X POST $U/api/inventario -d '{"codigo":"EB-V56","descripcion":"EinarBoard V5.6","categoria":"tarjeta","aplica_a":["V5.6"],"minimo":5}')"
|
||||
check "POST código duplicado → 409" '"existe":true' "$(curl "${ALAN[@]}" -X POST $U/api/inventario -d '{"codigo":"eb-v56","descripcion":"dup"}')"
|
||||
check "POST categoría inválida → 400" "inv\\u00e1lida: tornillo" "$(curl "${ALAN[@]}" -X POST $U/api/inventario -d '{"codigo":"X1","descripcion":"x","categoria":"tornillo"}')"
|
||||
check "POST con stock_inicial → entrada" '"stock": 7' "$(curl "${ALAN[@]}" -X POST $U/api/inventario -d '{"codigo":"SD-15B-05","descripcion":"Mean Well","categoria":"fuente","minimo":113,"stock_inicial":7}' | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)["item"]))')"
|
||||
check "salida sin stock → 409" "No hay stock suficiente" "$(curl "${ALAN[@]}" -X POST $U/api/inventario/movimiento -d '{"item_id":"EB-V56","tipo":"salida","cantidad":3,"motivo":"ensamble"}')"
|
||||
check "entrada" '"stock_despues":10' "$(curl "${ALAN[@]}" -X POST $U/api/inventario/movimiento -d '{"item_id":"EB-V56","tipo":"entrada","cantidad":10,"motivo":"llegó pedido","referencia":"OC-1"}')"
|
||||
check "salida" '"stock_despues":7' "$(curl "${ALAN[@]}" -X POST $U/api/inventario/movimiento -d '{"item_id":"EB-V56","tipo":"salida","cantidad":3,"motivo":"ensamble","referencia":"HBOX5-E01-A001-R0001"}')"
|
||||
check "ajuste (conteo 4 → delta −3)" '"delta":-3' "$(curl "${ALAN[@]}" -X POST $U/api/inventario/movimiento -d '{"item_id":"EB-V56","tipo":"ajuste","cantidad":4,"motivo":"conteo físico"}')"
|
||||
check "sin motivo → 400" "Escribe el motivo" "$(curl "${ALAN[@]}" -X POST $U/api/inventario/movimiento -d '{"item_id":"EB-V56","tipo":"entrada","cantidad":1}')"
|
||||
check "alerta = stock < mínimo" '"alerta": true' "$(curl -s "$U/api/inventario?alertas=1" | python3 -c 'import sys,json; j=json.load(sys.stdin); print(json.dumps([i for i in j["items"] if i["id"]=="EB-V56"]))')"
|
||||
check "PATCH stock a mano → 400" "no se edita a mano" "$(curl "${ALAN[@]}" -X PATCH "$U/api/inventario/item?id=EB-V56" -d '{"stock":99}')"
|
||||
check "PATCH ubicación → historial" "ubicacion:" "$(curl "${ALAN[@]}" -X PATCH "$U/api/inventario/item?id=EB-V56" -d '{"ubicacion":"Anaquel A2","minimo":3}')"
|
||||
check "movimientos del ítem" '"total":3' "$(curl -s "$U/api/inventario/movimientos?item_id=EB-V56")"
|
||||
check "export CSV" 'codigo,descripcion' "$(curl -s $U/api/inventario/export.csv)"
|
||||
check "export CSV cabecera" 'text/csv' "$(curl -s -D - -o /dev/null $U/api/inventario/export.csv)"
|
||||
|
||||
echo "## sitios"
|
||||
check "POST alan → 403" "Solo un administrador" "$(curl "${ALAN[@]}" -X POST $U/api/sitios -d '{"id":"icave","nombre":"ICAVE"}')"
|
||||
check "POST leo" '"id":"icave"' "$(curl "${LEO[@]}" -X POST $U/api/sitios -d '{"id":"icave","nombre":"ICAVE","cliente":"Hutchison Ports","ubicacion":"Veracruz","alias":["ICAVE (Veracruz)"],"contactos":[{"nombre":"Abraham","rol":"supervisor"}]}')"
|
||||
check "POST duplicado → 409" '"existe":true' "$(curl "${LEO[@]}" -X POST $U/api/sitios -d '{"id":"icave","nombre":"ICAVE"}')"
|
||||
check "PATCH alan → 403" "Solo un administrador" "$(curl "${ALAN[@]}" -X PATCH "$U/api/sitio?id=icave" -d '{"ubicacion":"x"}')"
|
||||
check "PATCH leo → historial" "ubicacion: Veracruz" "$(curl "${LEO[@]}" -X PATCH "$U/api/sitio?id=icave" -d '{"ubicacion":"Veracruz, Ver."}')"
|
||||
check "GET sitios" '"nombre":"ICAVE"' "$(curl -s $U/api/sitios)"
|
||||
curl "${ALAN[@]}" -X POST $U/api/actas -d '{"tipo":"instalacion-campo","serie":"HBOX5-E01-A001-R0001","tecnico":"Alan","conforme":true,"resultado":"OK","fecha":"2026-10-10","sitio":"icave","maquina_id":"FL-JUN-060","accion":"Instalada y reportando"}' > /dev/null
|
||||
curl "${ALAN[@]}" -X POST $U/api/actas -d '{"tipo":"diagnostico-campo","serie":"RTK-014","tecnico":"Alan","conforme":true,"resultado":"FALLA","fecha":"2026-10-11","terminal":"ICAVE","maquina_id":"FL-JUN-060"}' > /dev/null
|
||||
curl "${ALAN[@]}" -X POST $U/api/actas -d '{"tipo":"dano","serie":"RTK-014","tecnico":"Alan","conforme":true,"resultado":"REGISTRADO","fecha":"2026-10-11","sitio":"icave","clasificacion":"Agua","accion":"Marcada como dañada","resumen":"agua adentro"}' > /dev/null
|
||||
curl "${ALAN[@]}" -X POST $U/api/actas -d '{"tipo":"envio","serie":"HBOX5-E02-A002-R0002","tecnico":"Alan","conforme":true,"resultado":"ENVIADA","fecha":"2026-10-12","clasificacion":"ICAVE (Veracruz)","accion":"Enviada"}' > /dev/null
|
||||
R=$(curl -s "$U/api/sitio/unidades?id=icave")
|
||||
check "unidades del sitio" '"total_unidades":3' "$R"
|
||||
check "coincidencia por slug" '"coincidencia":"sitio"' "$R"
|
||||
check "coincidencia por texto (terminal/destino)" '"coincidencia":"texto"' "$R"
|
||||
check "instalada con fecha" '"instalada":"2026-10-10"' "$R"
|
||||
check "marca de daño en la unidad" '"marca":"DANADA"' "$R"
|
||||
check "sitio inexistente → 404" "No existe el sitio" "$(curl -s "$U/api/sitio/unidades?id=nada")"
|
||||
check "/api/unidad trae sitio" '"sitio":"icave"' "$(curl -s "$U/api/unidad?serie=HBOX5-E01-A001-R0001")"
|
||||
check "/api/actas trae sitio" '"sitio":"icave"' "$(curl -s "$U/api/actas")"
|
||||
|
||||
echo "## siembra (idempotente)"
|
||||
S1=$(python3 $REPO/backend/seed_1013.py --usuario leo 2>&1)
|
||||
check "primera corrida crea" "pendientes (seed_1013): 12 creados" "$S1"
|
||||
check "sitios: 3 nuevos (icave ya existía)" "sitios: 3 creados, 1 ya existían" "$S1"
|
||||
S2=$(python3 $REPO/backend/seed_1013.py --usuario leo 2>&1)
|
||||
check "segunda corrida no duplica" "12 ya existían" "$S2"
|
||||
check "inventario no duplica" "0 ítems creados" "$S2"
|
||||
echo '[{"id":"t-notion-1","titulo":"Importado de Notion","pista":"ingenieria","estado":"hecho"}]' > $T/extra.json
|
||||
check "--extra importa" "extra.json): 1 creados" "$(python3 $REPO/backend/seed_1013.py --solo nada --extra $T/extra.json 2>&1)"
|
||||
check "resumen tras siembra (1 + 12 sembrados + 1 extra)" '"total":14' "$(curl -s $U/api/tareas/resumen)"
|
||||
|
||||
echo; echo "RESULTADO: $ok correctas · $fail fallidas · datos en $T"
|
||||
[ $fail -eq 0 ]
|
||||
@ -41,6 +41,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) |
|
||||
| [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](../diagnostico/tablero-campo.html) |
|
||||
| [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) |
|
||||
|
||||
@ -4,17 +4,42 @@ title: "Campo"
|
||||
|
||||
# Campo
|
||||
|
||||
Unidad en la máquina, envíos y recepciones. Regla: no reinicies ni desconectes nada antes de capturar la evidencia ([por qué](../diagnostico/procedimiento-diagnostico-campo.md)).
|
||||
Unidad **en la máquina o en camino**: instalación en el puerto, falla en sitio, envíos, recepciones y daños. Regla de oro: **no reinicies ni desconectes nada antes de capturar la evidencia** ([por qué](../diagnostico/procedimiento-diagnostico-campo.md)). Todo cuelga de la serie de la unidad y de su **sitio**.
|
||||
|
||||
## Sitios de instalación
|
||||
|
||||
| Sitio | Puerto | Página |
|
||||
|---|---|---|
|
||||
| ICAVE | Veracruz | [Sitio ICAVE](../sitios/sitio.html?id=icave) |
|
||||
| LZC | Lázaro Cárdenas | [Sitio LZC](../sitios/sitio.html?id=lzc) |
|
||||
| EIT | Ensenada | [Sitio EIT](../sitios/sitio.html?id=eit) |
|
||||
| TIMSA | Manzanillo | [Sitio TIMSA](../sitios/sitio.html?id=timsa) |
|
||||
|
||||
Cada sitio muestra sus unidades instaladas y sus actas de instalación y diagnóstico. Dar de alta un sitio nuevo: [Sitios](../sitios/index.md) (solo administradores). Las páginas de sitio las publica el otro agente (WP5); si una liga no abre todavía, usa el [índice de sitios](../sitios/index.md).
|
||||
|
||||
## Qué situación, qué procedimiento, qué acta
|
||||
|
||||
| Situación | Procedimiento | Acta |
|
||||
|---|---|---|
|
||||
| Falla en la máquina | [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](../formularios/diagnostico.html) |
|
||||
| Instalar en el puerto | [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md) | Impreso, al expediente |
|
||||
| Sale una unidad | [Ciclo de vida §6](../procedimientos/ciclo-de-vida.md#6-envio) | [Envío](../formularios/envio.html) |
|
||||
| Llega una unidad | [Ciclo de vida §7](../procedimientos/ciclo-de-vida.md#7-recepcion-e-inspeccion) | [Recepción e inspección](../formularios/recepcion.html) |
|
||||
| Algo se dañó | [Ciclo de vida: daño](../procedimientos/ciclo-de-vida.md#dano-o-incidente) | [Daño o incidente](../formularios/dano.html) |
|
||||
| **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 |
|
||||
| **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) |
|
||||
| Algo se dañó (cualquier serie) | [Ciclo de vida: daño](../procedimientos/ciclo-de-vida.md#dano-o-incidente) | [Daño o incidente](../formularios/dano.html) |
|
||||
|
||||
- [Expediente de una unidad](../unidad.html): cada acta, foto y log por serie.
|
||||
- [Tablero de campo](../diagnostico/tablero-campo.html): todas las visitas.
|
||||
- [Índice de actas](../actas/index.md)
|
||||
- Para el cliente: [manual del cliente](../manuales/manual-cliente.md) · [PDF](../manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf)
|
||||
El checklist impreso se sube al [expediente](../unidad.html) junto con el acta de instalación. El acta de instalación es nueva (oct-2026): mientras no esté publicada, el checklist impreso sigue siendo el registro.
|
||||
|
||||
## Lo que hay que saber en campo
|
||||
|
||||
- **Firmware vigente en campo: 0.6.0** (V4 y V5). 0.6.3 es de banco; no se flashea en la máquina. Reflashear es trabajo de [taller](../taller/index.md).
|
||||
- En **V5.5** no se apaga y prende la etapa RTK en batería «para ver si vuelve»: el módem queda oscuro hasta RESET 4G o 24 V.
|
||||
- La unidad en batería reporta cada 10 minutos (V5.6 en adelante) o de forma continua hasta agotar el paquete (V5.5 con 0.6.0). Si deja de reportar en batería, no es forzosamente una falla: ve el [manual del cliente](../manuales/manual-cliente.md).
|
||||
- Despliegue GPS-RTK (24-sep): bases RTK y gabinetes en ICAVE, EIT, LZC y TIMSA desde la semana del 28-sep; meta de unidades RTK en los puertos a mediados de noviembre.
|
||||
|
||||
## Tableros y expediente
|
||||
|
||||
- [Expediente de una unidad](../unidad.html): cada acta, foto y log por serie, incluida la cadena de campo (envío → instalación → diagnóstico).
|
||||
- [Tablero de campo](../diagnostico/tablero-campo.html): todas las visitas de diagnóstico.
|
||||
- [Sitios](../sitios/index.md): unidades por puerto.
|
||||
- [Índice de actas](../actas/index.md): cómo se llenan.
|
||||
- Para el cliente: [manual del cliente](../manuales/manual-cliente.md) · [PDF de instalación y operación](../manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf)
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
item.tipo = 'foto' → cámara del teléfono; sube a /api/adjuntos y guarda la ruta
|
||||
item.tipo = 'archivo' → captura de log (.txt .log .csv .nmea .ubx); sube a /api/archivos
|
||||
item.tipo = 'fecha' → selector de fecha; arranca con la de hoy
|
||||
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=)
|
||||
|
||||
Modos de serie:
|
||||
(a) simple: cfg.prefijo + cfg.series (cantidad) [+ cfg.desde, cfg.pad] → "RTK-005"
|
||||
@ -45,6 +48,13 @@
|
||||
return blob || file;
|
||||
}
|
||||
|
||||
/* Sitios (puertos / terminales) para el ítem tipo 'sitio': una sola consulta por página. */
|
||||
let sitiosProm = null;
|
||||
function cargarSitios() {
|
||||
if (!sitiosProm) sitiosProm = fetch('/api/sitios', { cache: 'no-store' }).then(r => r.json()).then(j => j.sitios || []).catch(() => []);
|
||||
return sitiosProm;
|
||||
}
|
||||
|
||||
function opciones(sel, prefijo, desde, hasta, pad) {
|
||||
sel.append($('option', { value: '', text: '— elegir —' }));
|
||||
sel.append($('option', { value: prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' }));
|
||||
@ -309,6 +319,20 @@
|
||||
tbody.append(tr);
|
||||
});
|
||||
t.append(tbody); scroll.append(t); box.append(scroll);
|
||||
} else if (it.tipo === 'sitio') {
|
||||
box.append($('label', { html: it.txt || 'Sitio' }));
|
||||
if (it.ayuda) box.append($('div', { class: 'nota-item', html: it.ayuda }));
|
||||
const sel = $('select', { 'data-id': id, 'data-lbl': String(it.txt || 'Sitio').replace(/<[^>]+>/g, ''), style: 'max-width:420px' });
|
||||
sel.append($('option', { value: '', text: it.req ? '— elegir sitio —' : '— sin sitio / no aplica —' }));
|
||||
sel.append($('option', { value: '…', text: 'cargando sitios…', disabled: 'disabled' }));
|
||||
cargarSitios().then(ss => {
|
||||
sel.querySelector('option[value="…"]').remove();
|
||||
ss.forEach(s => sel.append($('option', { value: s.id, text: s.nombre + (s.ubicacion && s.ubicacion !== 'por confirmar' ? ' — ' + s.ubicacion : '') })));
|
||||
sel.append($('option', { value: 'otro', text: 'Otro / no está en la lista (anótalo en observaciones)' }));
|
||||
if (sel.dataset.pend !== undefined) { sel.value = sel.dataset.pend; delete sel.dataset.pend; }
|
||||
if (!ss.length) sel.append($('option', { value: '', text: '(no hay sitios dados de alta; pide a Leo que los cree)' }));
|
||||
});
|
||||
box.append(sel);
|
||||
} else if (it.tipo === 'fecha') {
|
||||
box.append($('label', { html: it.txt }));
|
||||
const hoy = new Date(); hoy.setMinutes(hoy.getMinutes() - hoy.getTimezoneOffset());
|
||||
@ -427,7 +451,11 @@
|
||||
els.forEach(el => {
|
||||
if (el.type === 'checkbox') el.checked = !!v;
|
||||
else if (el.type === 'radio') el.checked = (el.value === v);
|
||||
else el.value = v || '';
|
||||
else {
|
||||
el.value = v || '';
|
||||
// un <select> cuyas opciones aún no llegan (sitios): guarda el valor y aplícalo al cargar
|
||||
if (el.tagName === 'SELECT' && v && el.value !== v) el.dataset.pend = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,9 @@ Acta.render({
|
||||
opciones:['Tarjeta EinarBoard','Módulo ArduSimple','Módem 4G','Unidad HBOX completa','Otro (cable, antena, gabinete…)'] },
|
||||
{ tipo:'radio', clave:'donde', req:true, txt:'Dónde ocurrió',
|
||||
opciones:['En el banco (prueba o flasheo)','Durante el ensamble','En transporte o almacén','En campo, instalada','No se sabe — así se encontró'] },
|
||||
{ tipo:'sitio', clave:'sitio', guardaEn:'sitio', txt:'Sitio (si fue en un puerto o la unidad pertenece a uno)',
|
||||
si:v=>/campo|transporte|No se sabe/.test(String(v.donde||'')),
|
||||
ayuda:'Así el daño aparece en la página del sitio. Si fue en el banco o en el ensamble, no aplica.' },
|
||||
{ tipo:'texto', clave:'quien_detecto', rows:'1', txt:'Quién lo detectó y cómo', ph:'Daniel, al medir continuidad antes de energizar' },
|
||||
] },
|
||||
|
||||
|
||||
@ -48,6 +48,8 @@ Acta.render({
|
||||
+ 'Sin correo contestado y horario acordado, no subas: avisa y reagenda.' },
|
||||
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha',
|
||||
ayuda:'Hoy por omisión; cámbiala si capturas una visita de otro día.' },
|
||||
{ tipo:'sitio', clave:'sitio', guardaEn:'sitio', txt:'Sitio (puerto o terminal)',
|
||||
ayuda:'Elige el puerto donde está la máquina; la visita aparece en la página del sitio.' },
|
||||
{ tipo:'texto', clave:'maquina_id', req:true, guardaEn:'maquina_id', rows:'1',
|
||||
txt:'Máquina (como la nombra ICAVE)',
|
||||
ph:'FL-JUN-060 · RT-ZPMC-20 · HS-WES-62' },
|
||||
|
||||
@ -32,6 +32,8 @@ Acta.render({
|
||||
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha' },
|
||||
{ tipo:'radio', clave:'destino', req:true, guardaEn:'clasificacion', txt:'Destino',
|
||||
opciones:['ICAVE (Veracruz)','TIMSA','EIT','LCT','Taller Veracruz','Querétaro (regreso)','Otro'] },
|
||||
{ tipo:'sitio', clave:'sitio', guardaEn:'sitio', txt:'Sitio de instalación (si va a un puerto)',
|
||||
ayuda:'Elige el sitio para que la unidad aparezca en su página (Sitios). Déjalo vacío si regresa al taller o a Querétaro.' },
|
||||
{ tipo:'texto', clave:'maquina_destino', rows:'1', guardaEn:'maquina_id', txt:'Máquina destino (si se sabe)', ph:'FL-JUN-060' },
|
||||
{ tipo:'radio', clave:'medio', req:true, txt:'Medio de envío',
|
||||
opciones:['Paquetería (guía abajo)','La lleva alguien de Helgen','La recoge el cliente'] },
|
||||
|
||||
124
docs/formularios/instalacion.html
Normal file
124
docs/formularios/instalacion.html
Normal file
@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de instalación en campo — HBOX</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
<style>
|
||||
details.como{background:#eef3f8;border:1px solid #c9dcec;border-radius:10px;padding:8px 14px;margin:0 0 12px;font-size:14px;color:#1b3b57}
|
||||
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>
|
||||
const fix = v => String(v.rtk_fix || '').startsWith('Sí');
|
||||
const sinFix = v => String(v.rtk_fix || '').startsWith('No');
|
||||
const conEth = v => String(v.ethernet || '').startsWith('Sí');
|
||||
const ign = v => String(v.ignicion || '');
|
||||
const reporta = v => String(v.monitor || '').startsWith('Sí');
|
||||
|
||||
Acta.render({
|
||||
tipo: 'instalacion-campo',
|
||||
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 instala',
|
||||
titulo: 'Acta de instalación en campo',
|
||||
subtitulo: 'Una por unidad y por vehículo. Se llena en la máquina, al terminar de instalar; no de memoria después.',
|
||||
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>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>'
|
||||
+ '</ol></details>',
|
||||
secciones: [
|
||||
|
||||
{ titulo: '0 · Dónde y en qué',
|
||||
items: [
|
||||
{ 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:'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)' },
|
||||
{ tipo:'texto', clave:'acompanante', rows:'1', txt:'Quién autorizó o acompañó (operador, supervisor)', ph:'Abraham (ICAVE), 10:30' },
|
||||
] },
|
||||
|
||||
{ titulo: '1 · Montaje del gabinete',
|
||||
items: [
|
||||
{ tipo:'radio', clave:'montaje', req:true, txt:'Gabinete montado',
|
||||
opciones:['Fijo, con los 4 puntos y sin vibración','Fijo, con menos puntos (explica)','Provisional (explica)'] },
|
||||
{ tipo:'texto', clave:'montaje_donde', rows:'1', txt:'Dónde quedó (cabina, chasis, poste…)', ph:'Lado derecho de la cabina, bajo el asiento' },
|
||||
{ tipo:'check', clave:'prensaestopas', req:true, txt:'Prensaestopas apretados y cables con bajada de agua (goteo hacia afuera).' },
|
||||
{ tipo:'check', clave:'tapa', req:true, txt:'Tapa cerrada con sello y tornillos en cruz; etiqueta de la unidad a la vista.' },
|
||||
{ tipo:'foto', clave:'f_gabinete', req:true, etiqueta:'gabinete', txt:'Gabinete montado, de lejos (que se vea dónde quedó).' },
|
||||
] },
|
||||
|
||||
{ titulo: '2 · Alimentación e ignición',
|
||||
sub:'Máquina encendida. Multímetro en DC.',
|
||||
items: [
|
||||
{ tipo:'medida', clave:'v_24', req:true, txt:'Voltaje en el GX16 (pines 1-2), máquina encendida', esperado:'22 – 28 V', u:'V',
|
||||
aviso:'Fuera de 20–30 V no conectes la unidad: revisa de dónde tomaste la alimentación.' },
|
||||
{ tipo:'radio', clave:'toma', req:true, txt:'De dónde se tomó la alimentación',
|
||||
opciones:['Batería directa con fusible','Llave de encendido (switched)','Tablero / caja de fusibles del vehículo','Otro (explica)'] },
|
||||
{ tipo:'check', clave:'fusible', req:true, txt:'Fusible en línea instalado en el positivo, cerca de la toma.' },
|
||||
{ tipo:'radio', clave:'ignicion', req:true, txt:'Señal de ignición (pin 3)',
|
||||
opciones:['Conectada y probada: sube con la llave, baja al apagar','Conectada, no se pudo probar','No conectada (explica)'] },
|
||||
{ tipo:'medida', clave:'v_ign', si:v=>ign(v).startsWith('Conectada y probada'), txt:'Voltaje de ignición con llave', esperado:'≈ voltaje de batería', u:'V' },
|
||||
{ tipo:'check', clave:'led_ok', req:true, txt:'Al energizar: LED de estado enciende y la unidad arranca (ventilador o LED de panel).' },
|
||||
] },
|
||||
|
||||
{ titulo: '3 · Antenas y cables',
|
||||
items: [
|
||||
{ tipo:'radio', clave:'ant_gnss_pos', req:true, txt:'Antena GNSS: posición',
|
||||
opciones:['Techo de la cabina, cielo libre','Mástil o soporte propio','Otra (explica); cielo parcialmente tapado'] },
|
||||
{ tipo:'check', clave:'ant_gnss_cable', req:true, txt:'Cable GNSS sin dobleces cerrados, sujeto cada ~30 cm, SMA apretado con la mano más 1/4 de vuelta.' },
|
||||
{ tipo:'check', clave:'ant_lte', req:true, txt:'2 antenas LTE instaladas, separadas entre sí y lejos de la GNSS.' },
|
||||
{ tipo:'radio', clave:'ethernet', req:true, txt:'Cable Ethernet (solo si la unidad lo usa)',
|
||||
opciones:['No aplica','Sí — conectado y probado','Sí — conectado, no se pudo probar','No cupo en la caja (reporta en el tablero)'] },
|
||||
{ tipo:'texto', clave:'ethernet_a', si:conEth, rows:'1', txt:'A qué equipo se conectó el Ethernet' },
|
||||
{ tipo:'foto', clave:'f_antena', req:true, etiqueta:'antena', txt:'Antena GNSS en su lugar definitivo.' },
|
||||
{ tipo:'foto', clave:'f_interior', req:true, etiqueta:'interior', txt:'Interior del gabinete con cables sujetos, antes de cerrar.' },
|
||||
] },
|
||||
|
||||
{ titulo: '4 · RTK, 4G y confirmación en el monitor',
|
||||
sub:'Con la antena ya fija. Si tienes laptop, usa la consola; si no, el LED de estado y el monitor.',
|
||||
items: [
|
||||
{ tipo:'radio', clave:'rtk_fix', req:true, txt:'RTK FIX en sitio',
|
||||
opciones:['Sí — FIX (fix=4)','Solo FLOAT (fix=5) tras 15 min','No — sin fix (explica)'] },
|
||||
{ tipo:'medida', clave:'t_fix', si:fix, req:true, txt:'Tiempo hasta FIX desde que encendió', esperado:'< 10 min con cielo libre', u:'min' },
|
||||
{ tipo:'medida', clave:'sats', si:v=>!sinFix(v), txt:'Satélites en uso', esperado:'≥ 10', u:'sat' },
|
||||
{ tipo:'medida', clave:'rssi', req:true, txt:'Señal 4G (RSSI en dBm o CSQ del módem)', esperado:'mejor que −95 dBm · CSQ ≥ 10', ph:'-87 dBm · CSQ 14', modo:'text' },
|
||||
{ tipo:'radio', clave:'red', req:true, txt:'Módem', opciones:['Registrado en la red (LED lento o fijo)','Registra y se cae','No registra'] },
|
||||
{ tipo:'radio', clave:'monitor', req:true, txt:'Posición confirmada en el monitor',
|
||||
opciones:['Sí — alguien con acceso la vio en el mapa','No — nadie pudo confirmar','No — no aparece'] },
|
||||
{ tipo:'texto', clave:'monitor_quien', si:reporta, rows:'1', txt:'Quién lo confirmó y a qué hora', ph:'Daniel, 11:42, posición correcta en el patio 4' },
|
||||
{ tipo:'archivo', clave:'log', etiqueta:'instalacion', txt:'Captura de consola (opcional)' },
|
||||
] },
|
||||
|
||||
{ titulo: '5 · Cierre',
|
||||
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.' },
|
||||
{ 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)' },
|
||||
] },
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -38,6 +38,8 @@ Acta.render({
|
||||
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha' },
|
||||
{ tipo:'radio', clave:'donde', req:true, guardaEn:'clasificacion', txt:'Dónde se recibe',
|
||||
opciones:['Taller Veracruz','Oficina ICAVE','Otra terminal','Querétaro'] },
|
||||
{ tipo:'sitio', clave:'sitio', guardaEn:'sitio', txt:'Sitio al que va la unidad (si se sabe)',
|
||||
ayuda:'Con esto la recepción aparece en la página del sitio. Si todavía no se sabe, déjalo vacío.' },
|
||||
{ tipo:'texto', clave:'guia', rows:'1', txt:'Guía o quién la entregó' },
|
||||
{ tipo:'texto', clave:'maquina_destino', rows:'1', guardaEn:'maquina_id', txt:'Máquina destino', ph:'FL-JUN-060' },
|
||||
] },
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de prueba — EinarBoard V4 (fw 0.5.6)</title>
|
||||
<title>Acta de prueba — EinarBoard V4 (fw 0.6.0)</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -21,7 +21,7 @@ author: Helgen Technologies
|
||||
|
||||
The complete KiCad design history of the HBOX watchdog board, versions v3 through v6.
|
||||
|
||||
- **Active design: `hbox-watchdog-v6.*`** — this is the only version under development.
|
||||
- **Active designs (Oct-2026):** `hbox-watchdog-v5.*` (V5.6, Michelle's layout), `hbox-watchdog-v57.*` (V5.7, Leo → Michelle) and `hbox-watchdog-v6.*` (v6). `v3-PROD`, `v4` and the tag `v5.5-as-fabbed` are historical: do not edit.
|
||||
- `hbox-watchdog-v3/v4/v5.*` are frozen history. Do not modify them.
|
||||
- Files ending in `-PROD` are snapshots of boards that were actually fabricated. Never touch these.
|
||||
- Symbol and footprint libraries (`Drogheda-v6.kicad_sym`, `Drogheda-v6.pretty/`, `sym-lib-table`, `fp-lib-table`) live inside the repo with project-relative paths, so a clone is self-contained — no external library setup needed.
|
||||
|
||||
@ -18,7 +18,7 @@ Contacto: Leo Rodríguez — leo@helgen.tech
|
||||
|
||||
El historial completo de diseño en KiCad de la tarjeta HBOX watchdog, versiones v3 a v6.
|
||||
|
||||
- **Diseño activo: `hbox-watchdog-v6.*`** — es la única versión en desarrollo.
|
||||
- **Diseños activos (oct-2026):** `hbox-watchdog-v5.*` (V5.6, layout de Michelle), `hbox-watchdog-v57.*` (V5.7, Leo → Michelle) y `hbox-watchdog-v6.*` (v6). `v3-PROD`, `v4` y el tag `v5.5-as-fabbed` son históricos: no se editan.
|
||||
- `hbox-watchdog-v3/v4/v5.*` son historial congelado. No los modifiques.
|
||||
- Los archivos terminados en `-PROD` son instantáneas de tarjetas que ya se fabricaron. No los toques nunca.
|
||||
- Las bibliotecas de símbolos y huellas (`Drogheda-v6.kicad_sym`, `Drogheda-v6.pretty/`, `sym-lib-table`, `fp-lib-table`) viven dentro del repositorio con rutas relativas al proyecto, así que el clon es autocontenido — no hay que configurar bibliotecas externas.
|
||||
|
||||
@ -26,6 +26,10 @@ Acceso: usuario y contraseña te los da Leo; cámbiala la primera vez en [Mi cue
|
||||
- [Campo](campo/index.md): unidad en la máquina, envíos, recepciones y daños.
|
||||
- [Tarjetas y hardware](hardware/index.md): pinout, hoja de datos, manuales por tarjeta.
|
||||
- [Descargas](descargas/index.md): firmware por tarjeta y SHA-256.
|
||||
- [Tableros](tableros/index.md): pendientes del equipo (kanban), estado por versión de tarjeta, plan 570 vs. avance.
|
||||
- [Inventario](inventario/index.md): existencias y movimientos de tarjetas, módulos, gabinetes y packs.
|
||||
- [Sitios](sitios/index.md): unidades instaladas por puerto y actas de instalación.
|
||||
- [Historia del proyecto](historia/index.md): jun → oct 2026, con ligas a cada documento.
|
||||
|
||||
## Reglas que se rompen seguido
|
||||
|
||||
|
||||
@ -4,16 +4,79 @@ title: "Tarjetas y hardware"
|
||||
|
||||
# Tarjetas y hardware
|
||||
|
||||
Estado, firmware y cómo reconocer cada tarjeta: [Manuales por tarjeta](../manuales/index.md).
|
||||
Estado de cada versión de la EinarBoard al **13-oct-2026**, con sus documentos vigentes, los históricos y el siguiente paso. ¿No sabes qué tarjeta tienes? [Manuales por tarjeta](../manuales/index.md) te la identifica por serigrafía, conectores y LEDs. Estado vivo (tareas, decisiones, responsables): [Tablero de ingeniería](../tableros/ingenieria.html).
|
||||
|
||||
| Tarjeta | Documentos |
|
||||
| Tarjeta | Estado | Pedido | Proyecto KiCad | Firmware |
|
||||
|---|---|---|---|---|
|
||||
| <span class="badge-v55">V5.5</span> | En campo (15 pzas, 22-ago). No se fabrica más. | — | `hbox-watchdog-v5` (tag `v5.5-as-fabbed`) | `hbox-v5-bringup_0.6.0` CAMPO · 0.6.3 banco |
|
||||
| <span class="badge-v56">V5.6</span> | En layout — pasada 2 (Michelle). **No se pide todavía.** | 113 | `hbox-watchdog-v5` | 0.7.0 (`board_v56.h`), pendiente |
|
||||
| <span class="badge-v57">V5.7</span> | En diseño — Leo coloca la fuente a bordo. | 327 | `hbox-watchdog-v57` (convive con v5) | 0.7.x |
|
||||
| <span class="badge-v6">v6</span> | En diseño — esquemático verificado; PCB sin corregir. | 130 + reemplazos | `hbox-watchdog-v6` | 0.7.x (`board_v6.h`) |
|
||||
| <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` |
|
||||
|
||||
!!! warning "Reglas que aplican a toda revisión"
|
||||
- **DFM del LTC4040 (U6, QFN 4×5, 8-oct):** pasta del pad expuesto ≤ 50 %; vías del EP rellenas y tapadas (JLC *Epoxy Filled & Capped*) o fuera de las ventanas de pasta; pedir X-ray/AOI en U6; revisar U6 con lupa en cada tarjeta antes de energizar. Aplica a V5.6, V5.7 y v6.
|
||||
- **Nada de corriente por capas internas** (In1 e In2 son de 0.5 oz): `/BAT_PLUS`, `/MAIN5V`, `/SYS5V` y los nodos de conmutación van por F.Cu/B.Cu a ≥ 1.5 mm. Hallazgo de la auditoría del 13-oct.
|
||||
- El firmware se elige por tarjeta, nunca por número de versión: [Descargas](../descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta).
|
||||
|
||||
## V5.5 — en campo { #v55 }
|
||||
|
||||
**Estado.** 15 tarjetas fabricadas el 22-ago-2026 (JLCPCB), en ensamble, taller y puertos. Es la tarjeta que hoy tienen los técnicos en la mano. Diseño congelado (`v5.5-as-fabbed`); **no se corrige ni se vuelve a fabricar**: sus defectos se arreglan en la V5.6.
|
||||
|
||||
| | Documentos |
|
||||
|---|---|
|
||||
| <span class="badge-v3">V3</span> | [Manual](../manuales/manual-tarjeta-V3.md) · [Pinout](../montaje/pinout-arneses.md) · [BOM](../montaje/bom-split.md) · [Ensamble y QC](../montaje/guia-ensamble-qc.md) · [Guía de ensamble (PDF)](../montaje/assembly-guide.md) |
|
||||
| <span class="badge-v4">V4</span> | [Manual](../manuales/manual-tarjeta-V4.md) · [Preparación y prueba](../procedimientos/v4-prueba-fw-0.5.6.md) |
|
||||
| <span class="badge-v55">V5.5</span> | [Manual V5](../manuales/manual-tarjeta-V5.md) |
|
||||
| <span class="badge-v56">V5.6</span> | [Manual V5 §9](../manuales/manual-tarjeta-V5.md) · [Hoja de datos](v56-datasheet.md) · [Pinout](v56-pinout.md) · [Layout](v56-layout-instrucciones.md) · [ECO](v56-eco.md) · [Cambios al esquemático](v56-cambios-esquematico.md) · [**Layout pasada 2 — correcciones antes del pedido (13-oct)**](v56-layout-pasada-2.md) · [Layout pasada 1 (12-oct)](v56-layout-pasada-1.md) · [Auditoría del layout 13-oct (inglés)](v56-auditoria-layout-13oct.md) |
|
||||
| <span class="badge-v57">V5.7</span> | [**ECO — fuente 24 V → 5 V a bordo**](v57-eco-fuente.md) · [Guía de layout (Leo → Michelle)](v57-layout-guia.md) — esquemático listo; PCB en trabajo. Pedido después del banco de V5.6. |
|
||||
| <span class="badge-v6">v6</span> | [Manual](../manuales/manual-tarjeta-v6.md) · [**Próximos pasos para ordenar**](v6-proximos-pasos.md) · [Plan de corrección y estado](v6-plan-correccion.md) · [Cambios — resumen de 3 pasadas](v6-cambios-resumen.md) · [Auditoría completa](v6-auditoria-completa.md) · [Cambios al esquemático](v6-cambios-esquematico.md) · [Revisión vs. lecciones V5.5](v6-revision-lecciones-v55.md) · [Mapa de pines](v6-pinmap.md) · [Auditoría pre-fab](v6-auditoria-prefab.md) ([inglés](v6-prefab-audit.md)) · [Plan de cobre](EinarBoard-v6_Plan-de-Cobre_ES.html) ([inglés](EinarBoard-v6_Copper-Plan_EN.html)) |
|
||||
| Vigentes | [Manual de la tarjeta V5 (V5.5 y V5.6)](../manuales/manual-tarjeta-V5.md) · [Pinout V5.6](v56-pinout.md) (sección «Diferencias V5.5 → V5.6» al final) · [Hoja de datos V5.6](v56-datasheet.md) (valores comunes) |
|
||||
| Procedimientos | [Prueba y flasheo](../procedimientos/v5-prueba-y-flasheo.md) · [Módem 4G en batería](../procedimientos/prueba-bateria-modem.md) · [Ethernet y puertos serie](../procedimientos/v5-prueba-ethernet-serie.md) |
|
||||
| Históricos | Auditoría EMC del 22-ago (`03-Architecture-Hardware/2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md`, repositorio): `/SW_BAT` 0.3 mm y `/BAT_PLUS` finos → corregidos en la V5.6 |
|
||||
| Lo que no hace | Ciclo de 10 min en batería (el módem no arranca en frío sobre el boost). Regla de campo: respaldo continuo, nunca ciclos de etapa en batería. |
|
||||
|
||||
**Siguiente paso.** Ninguno de diseño. En taller: [diagnóstico](../procedimientos/diagnostico-banco.md) y [reflasheo](../procedimientos/reflasheo-unidades.md) con el binario V5 vigente.
|
||||
|
||||
## V5.6 — en layout → pedido de 113 { #v56 }
|
||||
|
||||
**Estado.** Esquemático cerrado (ECO 27/28-sep + acelerómetro U26 el 8-oct − sonda P12 el 9-oct). Layout de Michelle del 12-oct auditado el 13-oct: **NO LISTA** — `/BAT_PLUS` por In1 a 0.5 mm, y MAIN5V/SYS5V/SW3 por capas internas heredados de la V5.5. Está en **pasada 2**. Lleva Mean Well SD-15B-05 externa (igual que V5.5).
|
||||
|
||||
| | Documentos |
|
||||
|---|---|
|
||||
| Vigentes | [ECO V5.6 — requisitos R1–R5](v56-eco.md) · [Cambios al esquemático V5.5 → V5.6](v56-cambios-esquematico.md) · [Reglas de layout esquemático → PCB](v56-layout-instrucciones.md) · [**▶ Pasada 2 — correcciones antes del pedido (13-oct)**](v56-layout-pasada-2.md) · [Auditoría del layout 13-oct (inglés)](v56-auditoria-layout-13oct.md) |
|
||||
| Referencia | [Hoja de datos V5.6](v56-datasheet.md) · [Pinout V5.6](v56-pinout.md) — generados de la netlist del 28-sep: **todavía listan P12 y no traen U26**; se regeneran al cerrar la pasada 2 |
|
||||
| Históricos | [Layout pasada 1 (12-oct)](v56-layout-pasada-1.md) — superada por la pasada 2 |
|
||||
| Firmware | 0.7.0 con `board_v56.h` (control del módem por MODEM_EN/RST, paquete LFP-HT). Ningún 0.6.x enciende su módem. |
|
||||
|
||||
**Siguiente paso.** Michelle termina la pasada 2 → Claude revisa pad a pad + Gerber/BOM/CPL → paquete de fabricación a cotización (113 pzas) con las reglas DFM del LTC4040. Después: regenerar hoja de datos y pinout; firmware 0.7.0.
|
||||
|
||||
## V5.7 — fuente 24 V → 5 V a bordo → pedido de 327 { #v57 }
|
||||
|
||||
**Estado.** Decidida el 12-oct, esquemático aplicado el 13-oct (ERC 0, netlist verificada): V5.6 + bloque de entrada de la v6 (U25 TPS54360B, 5.32 V fijos), fusible F2 SMD 3 A, fuera el buck del ventilador U2 y la entrada P2, JP1 puenteado. Proyecto KiCad separado `hbox-watchdog-v57`; el PCB arranca del layout V5.6 de Michelle (`d71098d`). El hueco del SD-15B queda para el paquete 26650 LFP.
|
||||
|
||||
| | Documentos |
|
||||
|---|---|
|
||||
| Vigentes | [ECO V5.7 — fuente a bordo (v0.2)](v57-eco-fuente.md) · [**▶ Guía de layout V5.7 (Leo)**](v57-layout-guia.md) |
|
||||
| Históricos | ECO v0.1 del 12-oct (`03-Architecture-Hardware/2026-10-12_Drogheda_EinarBoard-V5.7_ECO-Fuente-a-bordo_ES_v0.1.md`, repositorio) — superada por v0.2 |
|
||||
| Hereda de V5.6 | Todo lo demás: [ECO V5.6](v56-eco.md), [cambios al esquemático](v56-cambios-esquematico.md), acelerómetro, sin P12. Hoja de datos y pinout propios: pendientes. |
|
||||
|
||||
**Siguiente paso.** Leo coloca y rutea U25/F2 en la zona reservada (x 170–197, y 42–72 y alrededor de P1) → Michelle replica ahí las correcciones de la pasada 2 de V5.6 y cierra → auditoría pad a pad → paquete de fabricación (327 pzas). Se pide después del banco de la V5.6.
|
||||
|
||||
## v6 — GNSS y 4G a bordo → pedido de 130 + reemplazos { #v6 }
|
||||
|
||||
**Estado.** Diseño nuevo: ZED-F9P y módem A7670SA soldados, sin zócalos; fuente de 5 V a bordo. Esquemático **terminado y verificado** el 28-sep (4 pasadas, ERC 0, `d58a89ba…`). El **PCB no tiene ninguna corrección**: si se ordenara hoy saldría con los 11 bloqueantes de la auditoría. Decisión: **no habrá v6.1** — lo que se encuentre se corrige antes del pedido.
|
||||
|
||||
| | Documentos |
|
||||
|---|---|
|
||||
| Vigentes | [**▶ Próximos pasos para ordenar (29-sep)**](v6-proximos-pasos.md) · [Plan de corrección y lista de liberación](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 | [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**. |
|
||||
|
||||
**Siguiente paso.** Michelle ejecuta *Update PCB from Schematic* y el layout de las correcciones (orden y tiempos en [Próximos pasos](v6-proximos-pasos.md)); aplicar la regla DFM del LTC4040 y quitar P7/P8 (como P12 en V5.6); auditoría pad a pad; paquete de fabricación (130 pzas).
|
||||
|
||||
## V4 y V3 — tarjetas anteriores aún en campo
|
||||
|
||||
| Tarjeta | Estado | Documentos |
|
||||
|---|---|---|
|
||||
| <span class="badge-v4">V4</span> | Blackpill en zócalo + Ethernet y puertos serie; mismas redes que la V5. En prueba con 0.6.0; casos sin NMEA en oct (H0016, H0011, H0024). | [Manual V4](../manuales/manual-tarjeta-V4.md) · [Preparación y prueba con 0.6.0](../procedimientos/v4-prueba-fw-0.5.6.md) · [Riel SW_5V y arranque del F9P](../procedimientos/v4-prueba-riel-sw5v-f9p.md) |
|
||||
| <span class="badge-v3">V3</span> | EinarBoard 1.0 (jul-2026, 10 pzas); 3 unidades en campo (RTK-003/006/014). | [Manual V3](../manuales/manual-tarjeta-V3.md) · [Pinout V3](../montaje/pinout-arneses.md) · históricos: [BOM V3](../montaje/bom-split.md) · [bring-up](../procedimientos/einarboard-1.0-bringup.md) |
|
||||
|
||||
## Por necesidad
|
||||
|
||||
@ -23,8 +86,10 @@ Estado, firmware y cómo reconocer cada tarjeta: [Manuales por tarjeta](../manua
|
||||
| Pin, puente o LED de V5.5/V5.6 | [Pinout V5.6](v56-pinout.md) · [Manual V5](../manuales/manual-tarjeta-V5.md) |
|
||||
| Límites eléctricos, consumos, rieles | [Hoja de datos V5.6](v56-datasheet.md) |
|
||||
| Qué cambió de V5.5 a V5.6 | [ECO V5.6](v56-eco.md) · [Cambios al esquemático](v56-cambios-esquematico.md) |
|
||||
| Rutear la PCB V5.6 | [Instrucciones de layout](v56-layout-instrucciones.md) · [Pasada 2 — correcciones (13-oct)](v56-layout-pasada-2.md) |
|
||||
| Qué trae la V5.7 y cómo colocar la fuente | [ECO V5.7](v57-eco-fuente.md) · [Guía de layout V5.7](v57-layout-guia.md) |
|
||||
| Arneses de una V3 | [Pinout V3](../montaje/pinout-arneses.md) · [BOM V3](../montaje/bom-split.md) |
|
||||
| Qué debe corregir Michelle hoy | [Pasada 2 V5.6](v56-layout-pasada-2.md) · [Guía V5.7](v57-layout-guia.md) |
|
||||
| Qué trae la V5.7 y por qué | [ECO V5.7](v57-eco-fuente.md) |
|
||||
| Qué falta para pedir la v6 | [Próximos pasos](v6-proximos-pasos.md) · [Plan de corrección](v6-plan-correccion.md) |
|
||||
| Cuántas de cada versión y cuándo | [Tablero de producción](../tableros/produccion.html) |
|
||||
| El binario correcto | [Descargas](../descargas/index.md) |
|
||||
| KiCad o el repositorio | [Guía Git](../git/guia-git-es.md) |
|
||||
| Cómo llegamos aquí | [Historia del proyecto](../historia/index.md) |
|
||||
|
||||
@ -10,6 +10,10 @@ author: Helgen Technologies
|
||||
|
||||
# EinarBoard V5.6 (HBOX V5) — Hoja de datos de ingeniería
|
||||
|
||||
!!! warning "Generado de la netlist del 28-sep-2026"
|
||||
No incluye el acelerómetro **U26** (agregado el 8-oct) ni la eliminación de **P12/R42/C24** (9-oct). Se regenera cuando se congele la V5.6 (pasada 2 de layout). Los valores eléctricos siguen vigentes.
|
||||
|
||||
|
||||
!!! note "Alcance y fuentes"
|
||||
Evolución de la V5.5 fabricada (STM32F411 a bordo + zócalos simpleRTK2B y 4G NTRIP Master). No es la v6 (GNSS y 4G soldados, `hbox-watchdog-v6`, en diseño); "copiado de v6" se refiere solo al origen de un valor de componente. Fuentes: netlist `kicad/_to_delete/v56-work-20260928/p5ntc-after.xml` (SHA-256 `45d3f034…8e42a7`, exportada de `kicad/hbox-watchdog-v5.kicad_sch` el 2026-09-28), `2026-09-28_Drogheda_EinarBoard-V5.6_Schematic-Change-Report_v1.0.md` (§1, §9, §10), `2026-09-27_Drogheda_EinarBoard-V5.6_ECO_ES_v1.0.md`, `04-Source-Code/firmware/hbox_v5_bringup/board_v5.h`, `hbox_v5_bringup.ino` 0.6.0 y `README.md`. Lo no verificado va marcado `<!-- VERIFICAR -->`.
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
# Instrucciones para Michelle — V5.6 layout final (acelerómetro, sin P12, pendientes) · 12-oct-2026
|
||||
|
||||
!!! note "Documento histórico — superado por la pasada 2 (13-oct-2026)"
|
||||
Estas fueron las instrucciones de la **pasada 1** del layout V5.6 (resultado: commit `d71098d`). La auditoría del 13-oct la declaró **NO LISTA**; las correcciones vigentes están en [Layout — pasada 2](v56-layout-pasada-2.md) y el detalle en la [auditoría del 13-oct](v56-auditoria-layout-13oct.md).
|
||||
|
||||
|
||||
Repositorio: Gitea `Helgen-Services/HBox_PCB`, rama `main`, commit `7ceba18` (09-oct). El esquemático ya trae los cambios; el PCB no. Leo cierra KiCad en su Mac antes de que empieces y no toca `hbox-watchdog-v5.*` hasta que hagas push.
|
||||
|
||||
## 0. Antes de empezar
|
||||
|
||||
@ -8,6 +8,10 @@ tags: [project-drogheda, hardware, harness, pinout, einarboard, v5.6]
|
||||
|
||||
# HBOX EinarBoard V5.6 — Pinout de arneses y conectores
|
||||
|
||||
!!! warning "Generado de la netlist del 28-sep-2026"
|
||||
No incluye el acelerómetro **U26** (agregado el 8-oct) ni la eliminación de **P12/R42/C24** (9-oct). Se regenera cuando se congele la V5.6 (pasada 2 de layout). Los valores eléctricos siguen vigentes.
|
||||
|
||||
|
||||
!!! note "EinarBoard V5.6 (`hbox-watchdog-v5`), lote en pedido"
|
||||
Esta tarjeta es la evolución de la V5.5 fabricada (STM32F411 a bordo + zócalos simpleRTK2B y 4G NTRIP Master). **No es la v6** (GNSS y 4G soldados a bordo, `hbox-watchdog-v6`, en diseño): la v6 tiene su propia sección en el sitio. Cuando este documento dice «copiado de v6» se refiere solo al origen de un valor de componente.
|
||||
|
||||
|
||||
@ -11,6 +11,10 @@ audience: Leo, Michelle (layout), compras
|
||||
|
||||
# EinarBoard v6 — Próximos pasos para ordenar (29-sep-2026)
|
||||
|
||||
!!! note "Cantidad (13-oct-2026)"
|
||||
El pedido de la v6 ya no son 5 prototipos: son **130 unidades** del pedido de 570 (113 V5.6 · 327 V5.7 · 130 v6) más todos los reemplazos futuros. El orden de los pasos no cambia; sí conviene un lote piloto antes de las 130.
|
||||
|
||||
|
||||
!!! warning "EinarBoard v6 (`hbox-watchdog-v6`): en diseño, no se pide"
|
||||
Diseño nuevo con ZED-F9P y módem A7670SA soldados a bordo. **No es la V5.6** que se manda a fabricar esta semana (`hbox-watchdog-v5`, zócalos para simpleRTK2B y 4G NTRIP Master). Los hallazgos marcados «Impacto en V5.6» ya se aplicaron a la V5.6 y están en su informe de cambios.
|
||||
|
||||
|
||||
@ -8,6 +8,10 @@ author: Helgen Technologies
|
||||
|
||||
# EinarBoard v6 — Revisión contra las lecciones de la V5.5 (26–27 sep 2026)
|
||||
|
||||
!!! note "Documento histórico — superado por la auditoría completa v6 (28-sep-2026)"
|
||||
Revisión de solo lectura hecha la mañana del 28-sep. La [auditoría completa v6](v6-auditoria-completa.md) del mismo día la profundiza y supera; lo que se aplicó está en el [resumen de las 3 pasadas](v6-cambios-resumen.md) y la lista de liberación en el [plan de corrección](v6-plan-correccion.md).
|
||||
|
||||
|
||||
!!! warning "EinarBoard v6 (`hbox-watchdog-v6`): en diseño, no se pide"
|
||||
Diseño nuevo con ZED-F9P y módem A7670SA soldados a bordo. **No es la V5.6** que se manda a fabricar esta semana (`hbox-watchdog-v5`, zócalos para simpleRTK2B y 4G NTRIP Master). Los hallazgos marcados «Impacto en V5.6» ya se aplicaron a la V5.6 y están en su informe de cambios.
|
||||
|
||||
|
||||
130
docs/historia/index.md
Normal file
130
docs/historia/index.md
Normal file
@ -0,0 +1,130 @@
|
||||
---
|
||||
title: "Historia del proyecto"
|
||||
project: Project Drogheda
|
||||
date: 2026-10-13
|
||||
version: "1.0"
|
||||
tags: [project-drogheda, hbox, historia, cronologia, decisiones]
|
||||
author: Helgen Technologies
|
||||
---
|
||||
|
||||
# Historia del proyecto — jun → oct 2026
|
||||
|
||||
Cronología del HBOX RTK y de la tarjeta EinarBoard, armada **solo** con documentos fechados (nombre `AAAA-MM-DD_…` en el repositorio Drogheda o fecha en la cabecera de la página) y con las decisiones registradas en las notas del proyecto. Cuando el documento está en este sitio, la fila lo enlaza; si solo vive en el repositorio, va su nombre de archivo en `código`. Lo que ya no aplica va marcado **superado**.
|
||||
|
||||
Cómo leerla: cada mes abre con un resumen de 3–5 líneas; después, una fila por evento o documento. Para el estado de hoy, ve a [Tarjetas y hardware](../hardware/index.md) y a los [Tableros](../tableros/index.md).
|
||||
|
||||
| Tarjeta | Qué es |
|
||||
|---|---|
|
||||
| <span class="badge-v3">V3</span> | EinarBoard 1.0 — Blackpill en zócalo (jul-2026, 10 pzas) |
|
||||
| <span class="badge-v4">V4</span> | EinarBoard 2.0 — Blackpill + Ethernet y puertos serie (ago-2026) |
|
||||
| <span class="badge-v55">V5.5</span> | EinarBoard 4.0 / HBOX v5 — STM32F411 soldado (22-ago-2026, 15 pzas) |
|
||||
| <span class="badge-v56">V5.6</span> | V5.5 + ECO (módem controlado por el MCU, LFP-HT, acelerómetro) — en layout |
|
||||
| <span class="badge-v57">V5.7</span> | V5.6 + fuente 24 V → 5 V a bordo — en diseño |
|
||||
| <span class="badge-v6">v6</span> | GNSS ZED-F9P y módem A7670SA soldados — en diseño |
|
||||
|
||||
---
|
||||
|
||||
## Junio 2026 — las fallas de campo y el primer watchdog
|
||||
|
||||
Las unidades de primera generación (Blackpill + PowerBoost, firmware v1) instaladas en ICAVE fallan: muerte casi simultánea de la flota y desconexiones crónicas en Wialon. El cliente escala el 17-jun. Se retiran las unidades a Veracruz, se diagnostican a distancia y nace el proyecto de una tarjeta portadora con **watchdog** propio: primero en protoboard sobre el Blackpill, con la máquina de estados, el sensado y el lazo ventilador/temperatura.
|
||||
|
||||
| Fecha | Qué pasó / qué se decidió | Documento | Tarjeta |
|
||||
|---|---|---|---|
|
||||
| 17-jun | Escalación del cliente por las fallas en ICAVE (5 unidades: RTG, montacargas, camión eléctrico, 2 reachstackers). | `06-Testing-Validation/RCA-2026-06-field-failures.md` (RCA, hipótesis ordenadas) | v1 |
|
||||
| 19-jun | Todas las unidades fuera de las máquinas; 4 en mano en Veracruz (3 muertas, 1 a medias). Protocolo de diagnóstico remoto por niveles. | [Protocolo de diagnóstico remoto](../diagnostico/protocolo-diagnostico-remoto.md) — **histórico** | v1 |
|
||||
| 19-jun | BOM eléctrico del gabinete: el HBOX vive dentro de un gabinete IP67 con fuente DIN de 24 V (ICAVE, TIMSA, EIT, LCT). | `03-Architecture-Hardware/cabinet-electrical-bom.md` | — |
|
||||
| 20-jun | Análisis de logs de banco y hallazgos GNSS; traspaso de ingeniería y principios del proyecto (*ethos*). | `06-Testing-Validation/2026-06-20_Drogheda_Bench-Log-Analysis_v1.0.md` · `…GNSS-Findings-Summary.md` · `00-Project-Management/ethos.md` | — |
|
||||
| 21-jun | Diseño de la tarjeta portadora v3 (watchdog + ventilador inteligente) y guía KiCad → JLCPCB. | `03-Architecture-Hardware/watchdog-pcb-v3-design.md` · `…watchdog-pcb-v3-kicad-guide.md` | V3 |
|
||||
| 22-jun | Lista de compras del prototipo en protoboard; justificación de cada conexión del watchdog. | [Lista de compras del protoboard](../procedimientos/historico-protoboard.md) — **histórico** · `…watchdog-connections-rationale.md` | V3 (proto) |
|
||||
| 24/25-jun | Bring-up del prototipo en protoboard; especificación del esquemático de producción (Δ contra el prototipo). | [Bring-up del prototipo v3](../procedimientos/historico-proto-bench-v3.md) — **histórico** · `…watchdog-production-schematic-delta.md` | V3 (proto) |
|
||||
|
||||
## Julio 2026 — EinarBoard 1.0 (V3) se fabrica
|
||||
|
||||
Se congela el estado eléctrico del watchdog, se rutea la V3 y pasa su auditoría pre-fabricación. El 8-jul se coloca el pedido en JLCPCB (10 PCB + 10 PCBA). Llegan las tarjetas y se ejecuta el *runbook* de bring-up. A fin de mes se define la 1.1 (RS-485 + RS-232 por SC16IS752 y Ethernet W5500) y se revisa el firmware v2.3 como candidato de producción. La V3 sigue hoy en 3 unidades de campo (RTK-003/006/014).
|
||||
|
||||
| Fecha | Qué pasó / qué se decidió | Documento | Tarjeta |
|
||||
|---|---|---|---|
|
||||
| 2-jul | Estado eléctrico canónico del watchdog v3. | `03-Architecture-Hardware/watchdog-electrical-design-state.md` | V3 |
|
||||
| 3-jul | Ruteo de la V3 PROD al 98 %; lista de pendientes a fab-ready. | `…watchdog-pcb-v3-routing-status.md` | V3 |
|
||||
| 4-jul | Pinout de arneses y conectores de la V3 (netlist). | [Pinout V3](../montaje/pinout-arneses.md) — vigente solo para las V3 en campo | V3 |
|
||||
| 5-jul | BOM dividido para el pedido PCBA; módulo 4G con USB, reset y LED; serigrafía de zócalos. | [BOM V3 dividido](../montaje/bom-split.md) — **histórico** · `…4G-USB-Reset-LED-addition.md` · `…Socket-Pinout-Silkscreen.md` | V3 |
|
||||
| 6/7-jul | Auditoría pre-fab de la EinarBoard 1.0: 4 hallazgos HIGH corregidos y re-verificados; guía de pedido JLCPCB y lista LCSC. | `…2026-07-06_Drogheda_EinarBoard-1.0-PreFab-Audit_v1.0.md` · `…JLCPCB-Order-Guide.md` · `…LCSC-search-list.md` | V3 |
|
||||
| 8-jul | Pedido JLCPCB `W2026070815023249`: 10 PCB + 10 PCBA. *Runbook* de bring-up (J1/J2 soldados a mano). Alternativas de módem 4G. | [EinarBoard 1.0 — bring-up](../procedimientos/einarboard-1.0-bringup.md) — **histórico** · `…2026-07-08_Drogheda_4G-Modem-Alternatives_v1.0.md` | V3 |
|
||||
| 24-jul | Brief de la EinarBoard 1.1: RS-485 + RS-232 (SC16IS752), Ethernet W5500 en SPI2, LED RGB. Revisión del firmware v2.3 como producción. | `…2026-07-24_Drogheda_EinarBoard-1.1_Design-Brief_v1.0.md` · `…firmware-review-v2.3-EinarBoard.md` | V3 → V4 |
|
||||
|
||||
## Agosto 2026 — V4, el MCU a bordo (V5.5) y el arranque de la v6
|
||||
|
||||
Se fabrica la V4 (Blackpill + Ethernet/serie). El 8-ago se decide la telemetría en banda (Modo B, `$PHLG`). El 12-ago se integra el STM32F411RET6 a la tarjeta (EinarBoard 3.0 → V5): el 22-ago se piden **15 pzas de la V5.5**; su auditoría EMC deja dos críticos para la siguiente corrida (`/SW_BAT` 0.3 mm, `/BAT_PLUS` 0.3–0.5 mm). El 24-ago arranca el diseño de la **v6** con GNSS y 4G soldados. El KiCad pasa a submódulo git con espejo en Gitea.
|
||||
|
||||
| Fecha | Qué pasó / qué se decidió | Documento | Tarjeta |
|
||||
|---|---|---|---|
|
||||
| 2/3-ago | Pedido de la V4 preparado en JLCPCB (lote Y5). | [Manual V4](../manuales/manual-tarjeta-V4.md) (ficha de fabricación) | V4 |
|
||||
| 8-ago | Decisión de telemetría: `$PHLG` íntegra en banda por Modo B (firmware v3.1). | `…2026-08-08_Drogheda_Firmware-v3.1_ModoB-PHLG_Design-Brief_v1.0.md` | V3/V4 |
|
||||
| 12/14-ago | Brief de la EinarBoard 3.0: STM32F411RET6 soldado, DFU; mapa de colocación de capacitores. | `…2026-08-12_Drogheda_EinarBoard-3.0_MCU-Integration_Design-Brief_v1.0.md` · `…2026-08-14_…Capacitor-Placement-Map_v1.0.md` | V5 |
|
||||
| 18-ago | Prueba de banco de la política de ventilador por potencia (FW v3.0.3). | [FW v3.0.3 — ventilador](../procedimientos/fan-policy-bench-test.md) — **histórico** | V3/V4 |
|
||||
| 20-ago | Correcciones aplicadas a la V5.5 (Q1/Q2/Q4/Q10, DzG1, U13 SOIC angosto, EP de los MP1584) y pre-auditoría. | `…2026-08-20_Drogheda_EinarBoard-v5_Fixes-Applied_v1.0.md` · `…Pre-Audit-Checks_v1.0.md` | V5.5 |
|
||||
| 22-ago | **V5.5 ordenada: 15 pzas** (JLCPCB). Auditoría EMC: `/SW_BAT` y `/BAT_PLUS` finos = críticos para la segunda corrida. | `…2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md` | V5.5 |
|
||||
| 24-ago | Brief de la **v6** (ZED-F9P + A7670SA a bordo); Fase 1 (cliente NTRIP en el STM32) y Fase 2 (delta de esquemático). | `…2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md` · `…Phase-1_NTRIP-Firmware-Spec_v1.0.md` · `…Phase-2_Schematic-Delta_v1.0.md` | v6 |
|
||||
| 27-ago | KiCad → submódulo git + espejo Gitea (HBox_PCB). | `…2026-08-27_Drogheda_KiCad-Submodule-Migration_v1.0.md` | — |
|
||||
|
||||
## Septiembre 2026 — llegan las V5.5, nace el sitio y se decide la V5.6
|
||||
|
||||
El mes más denso. Se estudia el respaldo de bajo consumo y la química del paquete a 45 °C (9/10-sep). El 15-sep llegan las V5.5: procedimientos en español para técnicos y **este sitio**; del 17 al 19 se arma el sistema de actas y salen las primeras unidades (V5-006 se destruye por un cruce en el GX16). El 24-sep se fija el despliegue GPS-RTK por puerto. Del 26 al 29 se decide y documenta la **V5.6** (ECO, esquemático, hoja de datos, pinout, instrucciones de layout), se audita a fondo la **v6** ("no habrá v6.1") y se publican los manuales por tarjeta. El firmware pasa de 0.5.x a 0.6.0 CAMPO con gobernador de carga.
|
||||
|
||||
| Fecha | Qué pasó / qué se decidió | Documento | Tarjeta |
|
||||
|---|---|---|---|
|
||||
| 5-sep | Plan de cobre y colocación EMC de la v6 (leyes de EMC, tabla de proximidad). | [Plan de cobre v6](../hardware/EinarBoard-v6_Plan-de-Cobre_ES.html) · [inglés](../hardware/EinarBoard-v6_Copper-Plan_EN.html) | v6 |
|
||||
| 9-sep | Estudios: fuente de 5 V a bordo vs. Mean Well (costo), respaldo con ciclo de trabajo de bajo consumo, química de celda a 45 °C. | `…2026-09-09_Drogheda_5V-Source-OnBoard-vs-MeanWell_Cost_v1.0.md` · `…Backup-LowPower-DutyCycle_Design-Brief_v1.0.md` · `…Battery-Chemistry-HighTemp_v1.0.md` | V5/v6 |
|
||||
| 10-sep | Procedimiento de evaluación del respaldo (tres medidas y una decisión); consulta a Vectro sobre PSM/eDRX. | `…2026-09-10_Drogheda_Backup-Power_Evaluation-Procedure_v1.0.md` · `…Consulta-Vectro_PSM-eDRX_v1.0.md` | V5 |
|
||||
| 15-sep | **Llegan las V5.5.** Procedimiento de prueba y flasheo por tarjeta; guías Git para colaboradores; se publica **drogheda.helgen.online**. | [Prueba y flasheo](../procedimientos/v5-prueba-y-flasheo.md) · [Guía Git](../git/guia-git-es.md) | V5.5 |
|
||||
| 17-sep | Procedimientos de módulos e integración y de ensamble; sistema de actas con número de unidad `HBOX5-Exx-Axxx-Rxxxx`. Banco de pruebas ambiental (HALT) aprobado para construir, con lista de compras. | [Módulos e integración](../procedimientos/v5-modulos-integracion.md) · [Ensamble](../procedimientos/v5-ensamble.md) · `…2026-09-17_Drogheda_HALT-Rig_Thermal-Vibration-Impact_v1.1.md` · `…HALT-Rig_Lista-Compras_ES_v1.0.md` | V5.5 |
|
||||
| 18-sep | Prueba final de unidad (FW 0.4.0); diagnóstico en campo, diagnóstico en banco y reflasheo. Primeras unidades V5 embarcadas con 0.4.0 / 0.5.0 / 0.5.1. **V5-006 destruida** por un cruce en el GX16. | [Prueba final](../procedimientos/v5-prueba-final-unidad.md) · [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) · [Diagnóstico en banco](../procedimientos/diagnostico-banco.md) · [Reflasheo](../procedimientos/reflasheo-unidades.md) | V5.5 |
|
||||
| 19-sep | Ciclo de vida de una unidad (qué acta en qué momento); índice de actas; guía de ensamble y QC (borrador V3). | [Ciclo de vida](../procedimientos/ciclo-de-vida.md) · [Actas](../actas/index.md) · [Guía de ensamble y QC](../montaje/guia-ensamble-qc.md) — **superada** por el ensamble V5 | V5.5 / V3 |
|
||||
| 19/21-sep | **Regla de nombres del firmware:** un binario por tarjeta (`hbox-tarjetaV3_4.0.0`, `hbox-tarjetaV4_x.y.z`, `hbox-v5-bringup_x.y.z`); la versión no indica la tarjeta. | [Descargas](../descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) | todas |
|
||||
| 24-sep | Prueba de banco del reporte cada 10 min en batería (FW 0.5.2). **Despliegue GPS-RTK:** unidades RTK en los puertos a mediados de noviembre, instalación terminada a mediados de diciembre; gabinetes y bases RTK desde la semana del 28-sep en ICAVE, EIT, LZC y TIMSA. | `…2026-09-24_Drogheda_HBOX-FW-0.5.2_Prueba-Banco-Bateria_ES_v1.0.md` · notas del proyecto (24-sep) | V4/V5 |
|
||||
| 24-sep | Lote planeado: **30 unidades (15 V4 + 15 V5)** — **superado** por el plan de 570 unidades del 13-oct. | notas del proyecto (24-sep) | V4/V5 |
|
||||
| 25-sep | Primera auditoría pre-fabricación de la v6 (5 prototipos previstos). | [Auditoría pre-fab v6](../hardware/v6-auditoria-prefab.md) · [inglés](../hardware/v6-prefab-audit.md) — **superada** por la del 28-sep | v6 |
|
||||
| 26-sep | Auditoría del mapa de pines V4/V5 contra FW 0.5.4. **Decisión V5.6:** 500 ArduSimple ya pedidos → la arquitectura con zócalos se queda; nada de rework en tarjetas existentes. Hallazgo: el módem no arranca en batería (MP1584 en UVLO sobre el riel del boost). | `…2026-09-26_Drogheda_HBOX-V4-V5_Pinmap-Audit-FW-0.5.4_v1.0.md` · notas del proyecto (26-sep) | V4/V5.5 |
|
||||
| 27-sep | **ECO V5.6**: ciclo de 10 min en batería, módem controlado por el MCU (AP64350, MODEM_EN/RST/LED), LFP-HT (JP2 2-3, R55 5.6 k), RS1 10 mΩ, TMP102 U18 en 0x49, lista EMC. README del firmware. **0.6.0 CAMPO** (gobernador de carga); 0.5.8 retirada. | [ECO V5.6](../hardware/v56-eco.md) · [README firmware](../descargas/hbox-v5-bringup-README.md) | V5.6 |
|
||||
| 28-sep | Informe de cambios al esquemático V5.6 (58 diferencias, todas previstas). **v6:** auditoría completa, plan de corrección, cambios al esquemático en 3 pasadas, revisión contra lecciones V5.5, mapa de pines; **"no habrá v6.1"**. H0008: el cargador sí carga (vigilancia nocturna). | [Cambios V5.5 → V5.6](../hardware/v56-cambios-esquematico.md) · [Auditoría v6](../hardware/v6-auditoria-completa.md) · [Plan de corrección](../hardware/v6-plan-correccion.md) · [Resumen 3 pasadas](../hardware/v6-cambios-resumen.md) · [Lecciones V5.5](../hardware/v6-revision-lecciones-v55.md) · [Mapa de pines](../hardware/v6-pinmap.md) · `06-Testing-Validation/2026-09-28_H0008_charging-overnight-log.md` | V5.6 / v6 |
|
||||
| 29-sep | Hoja de datos, pinout e instrucciones de layout V5.6; manuales por tarjeta (V3, V4, V5, v6); manual del cliente v2.0 y ficha técnica (PDF); configuración de unidad, prueba Ethernet/serie, checklist de puesta en marcha; guía del ingeniero; próximos pasos para ordenar la v6. **0.6.1 retirada** (su `$PHLG` no pasaba por el módem). | [Hoja de datos](../hardware/v56-datasheet.md) · [Pinout](../hardware/v56-pinout.md) · [Layout](../hardware/v56-layout-instrucciones.md) · [Manuales](../manuales/index.md) · [Manual del cliente](../manuales/manual-cliente.md) · [Configuración](../procedimientos/v5-configuracion-unidad.md) · [Ethernet y serie](../procedimientos/v5-prueba-ethernet-serie.md) · [Checklist](../procedimientos/v5-checklist-puesta-en-marcha.md) · [Guía del ingeniero](../guia-ingeniero.md) · [Próximos pasos v6](../hardware/v6-proximos-pasos.md) | V5.6 / v6 |
|
||||
| 30-sep | Revisión del modo batería y del layout V5.6 (U4 junto a L6, DzG1 corregido); 0.6.2 a prueba de banco. | notas del proyecto (30-sep → 1-oct) | V5.6 |
|
||||
|
||||
## Octubre 2026 — 0.6.3, acelerómetro, V5.7 y el plan de 570
|
||||
|
||||
Firmware 0.6.3 a banco (1-oct). Se revisa el bus I²C de V5.6/v6 (5-oct), se diagnostica la V4 sin NMEA (riel SW_5V, 6/7-oct) y se escribe la regla DFM del LTC4040 (8-oct) tras las bolas de soldadura en U6. La V5.6 recibe el acelerómetro U26 (8-oct) y pierde la sonda P12 (9-oct). El 12-oct Michelle hace el layout; el 13-oct la auditoría lo declara **NO LISTA** y se abre la pasada 2. Ese mismo día nace la **V5.7** (fuente a bordo) y se fija el **pedido de 570 HBOX**: 113 V5.6 · 327 V5.7 · 130 v6.
|
||||
|
||||
| Fecha | Qué pasó / qué se decidió | Documento | Tarjeta |
|
||||
|---|---|---|---|
|
||||
| 1-oct | **0.6.3 en prueba de banco** (sustituye a 0.6.2): el módem vuelve solo tras un respaldo corto con paquete bajo; motivo de cada reset al servidor. Notas de versión, descargas v2.3, procedimiento de módem en batería v1.7. | [Notas de versión](../descargas/notas-de-version.md) · [Descargas](../descargas/index.md) · [Módem en batería](../procedimientos/prueba-bateria-modem.md) | V4/V5 |
|
||||
| 5-oct | Revisión del bus I²C de V5.6 y v6. Falla del cargador en H0013 (V5.5): pausa NTC después de C8. | documentos del Proyecto Drogheda (5-oct), no publicados en el sitio | V5.6 / v6 |
|
||||
| 6/7-oct | V4 sin NMEA (H0016, H0011, H0024): procedimiento de banco del riel SW_5V y arranque del F9P, con mapa de puntos de medición. | [V4 — riel SW_5V y F9P](../procedimientos/v4-prueba-riel-sw5v-f9p.md) | V4 |
|
||||
| 8-oct | **Regla DFM LTC4040 (U6, QFN 4×5):** bolas de soldadura recurrentes en V5.5 y V4 → pasta del EP ≤ 50 %, vías rellenas o fuera de las ventanas, X-ray/AOI en U6, lupa antes de energizar; aplica a toda revisión. **Acelerómetro U26 SC7A20TR** (I²C 0x18, INT1 → PB9) entra a la V5.6. | notas del proyecto (8-oct) | V5.6 / v6 |
|
||||
| 9-oct | **P12 (sonda NTC) sale de la V5.6** con R42/C24: redundante y se necesita el espacio; sin temperatura de celda en batería (aceptado). Aplica a P7/P8 de la v6. | notas del proyecto (9-oct) | V5.6 / v6 |
|
||||
| 12-oct | Instrucciones a Michelle para el layout final V5.6 (U26, sin P12). Borrador v0.1 de la ECO V5.7. | [Layout pasada 1](../hardware/v56-layout-pasada-1.md) — **superada** por la pasada 2 · `…2026-10-12_Drogheda_EinarBoard-V5.7_ECO-Fuente-a-bordo_ES_v0.1.md` — **superado** por v0.2 | V5.6 / V5.7 |
|
||||
| 13-oct | **Auditoría del layout de Michelle (`d71098d`): NO LISTA** — `/BAT_PLUS` por In1 a 0.5 mm (bloqueante); 18 piezas movidas (decisión: se quedan); MAIN5V/SYS5V/SW3 por capas internas desde V5.5. Pasada 2 con las correcciones. | [Auditoría 13-oct (inglés)](../hardware/v56-auditoria-layout-13oct.md) · [Layout pasada 2](../hardware/v56-layout-pasada-2.md) | V5.6 |
|
||||
| 13-oct | **V5.7 = V5.6 + fuente 24 V → 5 V a bordo** (U25 TPS54360B del bloque v6), sin SD-15B: esquemático aplicado (ERC 0), F2 fusible SMD 3 A, fuera U2 y P2, JP1 puenteado; proyecto KiCad separado `hbox-watchdog-v57`. Leo coloca la fuente; Michelle corrige V5.6 y después toma V5.7. | [ECO V5.7 v0.2](../hardware/v57-eco-fuente.md) · [Guía de layout V5.7](../hardware/v57-layout-guia.md) | V5.7 |
|
||||
| 13-oct | **Pedido de 570 HBOX:** 113 × V5.6 (con SD-15B) · 327 × V5.7 · 130 × v6 (+ reemplazos futuros en v6). Paquetes de fabricación V5.6/V5.7/v6 para cotizar. Caja plástica del HBOX: el cable Ethernet no cabe (cambio por definir). Sitios: ICAVE, LZC, EIT, TIMSA. | notas del proyecto (13-oct) · [Tablero de producción](../tableros/produccion.html) · [Kanban](../tableros/kanban.html) | V5.6 / V5.7 / v6 |
|
||||
| 13-oct | Reorganización de este sitio por propósito; tableros, inventario y sitios. | esta página | — |
|
||||
|
||||
---
|
||||
|
||||
## Qué quedó superado (resumen)
|
||||
|
||||
| Lo que decía | Desde | Lo sustituye |
|
||||
|---|---|---|
|
||||
| Lote de 30 unidades (15 V4 + 15 V5) para agosto/septiembre | 24-sep | Pedido de 570 HBOX (113 V5.6 · 327 V5.7 · 130 v6), 13-oct |
|
||||
| v6: "5 prototipos en JLCPCB" | 25/29-sep | v6 = 130 unidades + todos los reemplazos futuros, 13-oct |
|
||||
| Auditoría pre-fabricación v6 del 25-sep | 25-sep | Auditoría completa y plan de corrección del 28-sep |
|
||||
| Revisión v6 contra lecciones V5.5 | 28-sep | Auditoría completa del mismo día (la profundiza y supera) |
|
||||
| Layout V5.6 pasada 1 (12-oct) | 12-oct | Pasada 2 (13-oct), tras la auditoría NO LISTA |
|
||||
| ECO V5.7 v0.1 (borrador) | 12-oct | ECO V5.7 v0.2, esquemático aplicado (13-oct) |
|
||||
| V5.6 con sonda NTC P12 | 9-oct | P12/R42/C24 eliminados; acelerómetro U26 añadido (8-oct) |
|
||||
| Firmware 0.5.8 CAMPO · 0.6.1 · 0.6.2 | 27-sep · 29-sep · 1-oct | 0.6.0 CAMPO vigente; 0.6.3 en prueba de banco |
|
||||
| Guía de ensamble y QC (borrador V3) · BOM V3 · bring-up EinarBoard 1.0 · protocolo remoto v1 | jul–sep | Procedimientos V5 de Producción, Taller y Campo |
|
||||
|
||||
## Historial de revisiones
|
||||
|
||||
| Versión | Fecha | Cambios |
|
||||
|---|---|---|
|
||||
| 1.0 | 2026-10-13 | Primera versión, jun → oct 2026, con la reorganización del sitio. |
|
||||
154
docs/index.md
154
docs/index.md
@ -6,131 +6,49 @@ hide:
|
||||
|
||||
# HBOX — Documentación técnica
|
||||
|
||||
Sitio interno de Helgen para producir, probar, instalar y diagnosticar el rastreador **HBOX RTK** (tarjeta EinarBoard). Todo trabajo deja un **acta**; toda unidad tiene un **expediente**. Reglas y acceso: [Guía del ingeniero](guia-ingeniero.md).
|
||||
|
||||
<div class="buscar-aviso" markdown>
|
||||
La lupa busca por unidad (H00xx), conector (P15, J10), evento (MODEM_DARK) o procedimiento.
|
||||
La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J10), evento (MODEM_DARK) o procedimiento.
|
||||
</div>
|
||||
|
||||
## Tareas
|
||||
## ¿Qué necesitas hacer?
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
| Quiero… | Abre | Acta |
|
||||
|---|---|---|
|
||||
| **Probar una tarjeta nueva** (V5.5 / V5.6 desnuda) | [Prueba y flasheo](procedimientos/v5-prueba-y-flasheo.md) | [Tarjeta V5](formularios/v5.html) |
|
||||
| **Armar una unidad** (módulos → ensamble → prueba final → configuración) | [Producción](produccion/index.md) | [Ensamble](formularios/ensamble.html) |
|
||||
| **Diagnosticar** una unidad que falla en la máquina | [Diagnóstico en campo](diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](formularios/diagnostico.html) |
|
||||
| **Diagnosticar** una unidad en la mesa | [Diagnóstico en banco](procedimientos/diagnostico-banco.md) | [Banco](formularios/banco.html) |
|
||||
| **Instalar en sitio** (ICAVE, LZC, EIT, TIMSA) | [Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md) | [Instalación en campo](formularios/instalacion.html) |
|
||||
| **Ver los tableros** (kanban, ingeniería, producción, actas) | [Tableros](tableros/index.md) | — |
|
||||
| **Inventario** (existencias, entradas y salidas) | [Inventario](inventario/inventario.html) | — |
|
||||
| **Pedir una tarjeta o saber qué versión va** | [Tarjetas y hardware](hardware/index.md) | — |
|
||||
| **Descargar firmware** (por tarjeta, con SHA-256) | [Descargas](descargas/index.md) | [Reflasheo](formularios/reflasheo.html) |
|
||||
| Reflashear una unidad en taller | [Reflasheo de unidades](procedimientos/reflasheo-unidades.md) | [Reflasheo](formularios/reflasheo.html) |
|
||||
| Enviar, recibir o reportar un daño | [Ciclo de vida](procedimientos/ciclo-de-vida.md) | [Envío](formularios/envio.html) · [Recepción](formularios/recepcion.html) · [Daño](formularios/dano.html) |
|
||||
| Ver todo lo que le pasó a una unidad | [Expediente de una unidad](unidad.html) | — |
|
||||
| Saber qué tarjeta tengo en la mano | [Manuales por tarjeta](manuales/index.md) | — |
|
||||
| Entender cómo llegamos aquí | [Historia del proyecto](historia/index.md) | — |
|
||||
|
||||
- ### Llegaron tarjetas nuevas V5.6
|
||||
## Tarjetas — estado al 13-oct-2026
|
||||
|
||||
---
|
||||
| Tarjeta | Estado | Pedido | Firmware | Dónde leer |
|
||||
|---|---|---|---|---|
|
||||
| <span class="badge-v55">V5.5</span> | **En campo.** 15 pzas fabricadas el 22-ago; en ensamble, taller y puertos. No se fabrica más. | — | 0.6.0 CAMPO · 0.6.3 en prueba de banco | [Manual V5](manuales/manual-tarjeta-V5.md) |
|
||||
| <span class="badge-v56">V5.6</span> | **En layout** (pasada 2 de Michelle: BAT_PLUS, MAIN5V, SYS5V, SW3). Con Mean Well SD-15B externa. | **113** unidades | 0.7.0 (`board_v56.h`), pendiente | [V5.6](hardware/index.md#v56) |
|
||||
| <span class="badge-v57">V5.7</span> | **En diseño.** V5.6 + fuente 24 V → 5 V a bordo (U25). Esquemático ERC 0; Leo coloca la fuente, Michelle cierra. | **327** unidades | 0.7.x | [V5.7](hardware/index.md#v57) |
|
||||
| <span class="badge-v6">v6</span> | **En diseño.** GNSS ZED-F9P y módem A7670SA soldados. Esquemático verificado; PCB sin corregir. | **130** unidades + reemplazos | 0.7.x (`board_v6.h`) | [v6](hardware/index.md#v6) |
|
||||
| <span class="badge-v4">V4</span> | En prueba y campo (Blackpill + Ethernet/serie). Sin producción nueva. | — | 0.6.0 CAMPO · 0.6.3 en prueba | [Manual V4](manuales/manual-tarjeta-V4.md) |
|
||||
| <span class="badge-v3">V3</span> | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) |
|
||||
| <span class="badge-fw">firmware</span> | El binario se elige **por tarjeta**, nunca por número de versión. 0.6.3 en banco desde el 1-oct, no para campo. | | | [Descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) · [notas de versión](descargas/notas-de-version.md) |
|
||||
|
||||
Inspección, flasheo con el binario vigente y autoprueba, tarjeta por tarjeta.
|
||||
Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el plan: [Tablero de producción](tableros/produccion.html).
|
||||
|
||||
[**Acta de tarjeta V5**](formularios/v5.html){ .md-button .md-button--primary }
|
||||
[Procedimiento: prueba y flasheo](procedimientos/v5-prueba-y-flasheo.md)
|
||||
## Hoy — 13 de octubre de 2026
|
||||
|
||||
- ### Armar y configurar una unidad
|
||||
- **V5.6 no se pide todavía.** La auditoría del layout de Michelle (commit `d71098d`) salió **NO LISTA**: `/BAT_PLUS` pasa por In1 a 0.5 mm. Correcciones en la [pasada 2](hardware/v56-layout-pasada-2.md); la fila de conectores se queda como está.
|
||||
- **V5.7 arrancó.** Esquemático con la fuente a bordo aplicado (ERC 0, F2 fusible SMD 3 A, fuera U2 y P2). Leo coloca y rutea la fuente con la [guía de layout](hardware/v57-layout-guia.md); después Michelle replica ahí las correcciones de V5.6.
|
||||
- **Plan de 570 unidades** (113 / 327 / 130) y paquetes de fabricación V5.6, V5.7 y v6 para cotizar. Pendiente: la caja plástica del HBOX, donde el cable Ethernet no cabe. Tareas y responsables en el [Kanban](tableros/kanban.html).
|
||||
|
||||
---
|
||||
|
||||
Módulos RTK/4G, ensamble en gabinete, prueba final y parámetros de la unidad.
|
||||
|
||||
[Acta de ensamble](formularios/ensamble.html){ .md-button .md-button--primary }
|
||||
[Ensamble](procedimientos/v5-ensamble.md) · [Configuración](procedimientos/v5-configuracion-unidad.md) · [Flujo completo](produccion/index.md)
|
||||
|
||||
- ### El cliente usa Ethernet o puertos serie
|
||||
|
||||
---
|
||||
|
||||
Prueba de banco con el binario `-eth`, cable directo a la laptop.
|
||||
|
||||
[Acta: Ethernet y serie](formularios/ethernet-serie.html){ .md-button .md-button--primary }
|
||||
[Procedimiento](procedimientos/v5-prueba-ethernet-serie.md)
|
||||
|
||||
- ### Una unidad falla en la máquina
|
||||
|
||||
---
|
||||
|
||||
Diagnóstico en sitio con el gabinete montado. No reinicies nada antes de capturar la evidencia.
|
||||
|
||||
[Acta de diagnóstico en campo](formularios/diagnostico.html){ .md-button .md-button--primary }
|
||||
[Procedimiento](diagnostico/procedimiento-diagnostico-campo.md)
|
||||
|
||||
- ### Una unidad está en la mesa
|
||||
|
||||
---
|
||||
|
||||
Diagnóstico en banco: continuidad, rampa de corriente, rieles y capturas por USB.
|
||||
|
||||
[Acta de diagnóstico en banco](formularios/banco.html){ .md-button .md-button--primary }
|
||||
[Procedimiento](procedimientos/diagnostico-banco.md)
|
||||
|
||||
- ### Reflashear una unidad
|
||||
|
||||
---
|
||||
|
||||
Solo en taller y solo después de subir la captura del diagnóstico.
|
||||
|
||||
[Acta de reflasheo](formularios/reflasheo.html){ .md-button .md-button--primary }
|
||||
[Procedimiento](procedimientos/reflasheo-unidades.md)
|
||||
|
||||
- ### Instalar en el puerto
|
||||
|
||||
---
|
||||
|
||||
Checklist de puesta en marcha de una página: imprimir y llevar, una hoja por unidad.
|
||||
|
||||
[Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md){ .md-button .md-button--primary }
|
||||
[Manual del cliente](manuales/manual-cliente.md)
|
||||
|
||||
- ### Enviar, recibir o reportar un daño
|
||||
|
||||
---
|
||||
|
||||
Qué salió en la caja, en qué estado llegó y qué se dañó. La serie queda marcada.
|
||||
|
||||
[Acta de envío](formularios/envio.html){ .md-button .md-button--primary }
|
||||
[Recepción](formularios/recepcion.html) · [Daño](formularios/dano.html) · [Ciclo de vida](procedimientos/ciclo-de-vida.md)
|
||||
|
||||
- ### Consultar pinout u hoja de datos
|
||||
|
||||
---
|
||||
|
||||
Conectores, puentes, LEDs y límites eléctricos de cada tarjeta.
|
||||
|
||||
[**Pinout V5.6**](hardware/v56-pinout.md){ .md-button .md-button--primary }
|
||||
[Hoja de datos V5.6](hardware/v56-datasheet.md) · [Todas las tarjetas](hardware/index.md)
|
||||
|
||||
- ### Descargar firmware
|
||||
|
||||
---
|
||||
|
||||
Binario por tarjeta (nunca por número de versión), SHA-256, parámetros 4G y ZED-F9P.
|
||||
|
||||
[Descargas](descargas/index.md){ .md-button .md-button--primary }
|
||||
[README bring-up v5](descargas/hbox-v5-bringup-README.md)
|
||||
|
||||
- ### Todo sobre una unidad
|
||||
|
||||
---
|
||||
|
||||
Expediente de un número de serie: cada acta, foto y log en una línea de tiempo.
|
||||
|
||||
[Abrir expediente](unidad.html){ .md-button .md-button--primary }
|
||||
[Tablero de producción](tablero.html) · [Tablero de campo](diagnostico/tablero-campo.html)
|
||||
|
||||
- ### Primera vez en el sitio
|
||||
|
||||
---
|
||||
|
||||
Reglas de trabajo, acceso y cómo se llenan las actas.
|
||||
|
||||
[Guía del ingeniero](guia-ingeniero.md){ .md-button .md-button--primary }
|
||||
[Índice de actas](actas/index.md)
|
||||
|
||||
</div>
|
||||
|
||||
## Estado
|
||||
|
||||
| Tarjeta / firmware | Estado |
|
||||
|---|---|
|
||||
| <span class="badge-v55">V5.5</span> | 15 unidades en campo · fw 0.6.0 |
|
||||
| <span class="badge-v56">V5.6</span> | Esquemático cerrado · PCB en layout · pedido pendiente |
|
||||
| <span class="badge-v6">v6</span> | En diseño (GNSS + 4G a bordo) — no pedir |
|
||||
| <span class="badge-fw">firmware 0.6.3</span> | En prueba de banco desde el 1-oct (sustituye a 0.6.2), no para campo ([descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta)); en campo sigue 0.6.0; 0.6.1 retirada el 29-sep (su telemetría no pasaba por el módem) · [notas de versión](descargas/notas-de-version.md#063-en-prueba) |
|
||||
|
||||
Reglas de trabajo: [Guía del ingeniero](guia-ingeniero.md). Fuente: repositorio Drogheda. Contacto: leo@helgen.tech
|
||||
Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
|
||||
|
||||
58
docs/inventario/index.md
Normal file
58
docs/inventario/index.md
Normal file
@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "Inventario"
|
||||
date: 2026-10-13
|
||||
tags: [project-drogheda, hbox, inventario, produccion, compras]
|
||||
---
|
||||
|
||||
# Inventario de materiales
|
||||
|
||||
Qué hay en el taller para producir e instalar HBOX: tarjetas, módulos, gabinetes, cajas, fuentes, packs, antenas, cables y los componentes de entrega larga. No es una lista de BOM: aquí solo entra lo que se cuenta a mano.
|
||||
|
||||
**[Abrir el inventario →](inventario.html)**
|
||||
|
||||
## Qué hay aquí
|
||||
|
||||
| Página | Para qué |
|
||||
|---|---|
|
||||
| [Inventario — existencias y movimientos](inventario.html) | Tabla de existencias por ítem con mínimo y alerta; alta de ítem; movimiento de **entrada** (compra, devolución) o **salida** (ensamble, envío, baja) con motivo y liga a acta/unidad; exportar CSV |
|
||||
| [Tablero de producción](../tableros/produccion.html) | Lo que el inventario tiene que cubrir: 113 V5.6 · 327 V5.7 · 130 v6 |
|
||||
| [Kanban — pista Compras](../tableros/kanban.html) | Pedidos en curso (SD-15B-05 ×113, paquetes LFP, gabinetes, cajas HBOX, antenas) |
|
||||
|
||||
## Qué se registra y qué no
|
||||
|
||||
| Sí | No |
|
||||
|---|---|
|
||||
| Tarjetas EinarBoard por versión (V5.5, V5.6, V5.7, v6) | Resistencias, capacitores y componentes de la BOM normal |
|
||||
| Módulos: ArduSimple simpleRTK2B Pro, 4G NTRIP Master | Herramienta y consumibles del banco |
|
||||
| Gabinetes IP67, cajas plásticas HBOX, fuentes SD-15B, packs LFP | Unidades ya ensambladas (esas viven en el [tablero](../tablero.html) por su acta) |
|
||||
| Antenas GNSS y LTE, cable Ethernet, conectores GX16, fusibles DIN | — |
|
||||
| Componentes críticos de entrega larga: LTC4040, TPS54360B, SRP1265A | — |
|
||||
|
||||
## Quién registra y cuándo
|
||||
|
||||
| Qué pasó | Quién | Movimiento |
|
||||
|---|---|---|
|
||||
| Llega material (paquetería, Aldair, Mouser) | Quien recibe (Jesús o Leo) | **Entrada**, con la orden o guía como referencia, el mismo día |
|
||||
| Se arma una unidad | Quien ensambla, al cerrar el acta de ensamble | **Salida** de cada pieza, con el número de unidad `HBOX5-Exx-Axxx-Rxxxx` como referencia |
|
||||
| Sale un kit al puerto | Quien llena el acta de envío | **Salida** de lo que va en la caja (fuente, antenas, gabinete) |
|
||||
| El conteo no cuadra | Jesús, al contar | **Ajuste** con el conteo físico y el motivo («conteo mensual», «se encontró en el otro anaquel») |
|
||||
| Pieza dañada o de baja | Quien llena el acta de daño | **Salida** con el motivo y la serie |
|
||||
|
||||
El stock es la **suma de los movimientos**; nadie lo escribe a mano. Cada movimiento queda con usuario, fecha, cantidad, motivo y referencia, y se puede ver desde el código del ítem.
|
||||
|
||||
## Mínimos y alertas
|
||||
|
||||
- El **mínimo** lo fija quien compra (Leo), pensando en el tiempo de entrega: chips y antenas RTK tardan ~4 semanas; LTC4040 y la fuente a bordo son de entrega larga.
|
||||
- Para el pedido de 570, la fuente Mean Well SD-15B-05 tiene mínimo **113** (solo la usa V5.6).
|
||||
- Por debajo del mínimo el ítem se marca en rojo aquí y en el [tablero de producción](../tableros/produccion.html). La alerta no manda correos: hay que mirar el tablero.
|
||||
- Mínimo 0 = sin alerta (consumibles que no vale la pena vigilar).
|
||||
|
||||
## Conteo inicial (pendiente del 13-oct)
|
||||
|
||||
Los ítems ya existen con stock 0. Jesús cuenta y registra **un ajuste por ítem** con motivo «conteo inicial». Mientras no se haga, el tablero mostrará alertas que no son reales.
|
||||
|
||||
## Para ingeniería
|
||||
|
||||
- Datos en `/srv/projects/drogheda/inventario/{items,movimientos}/` (un JSON por registro + `_indice.csv`).
|
||||
- API: `GET /api/inventario`, `POST /api/inventario/movimiento`, `GET /api/inventario/movimientos?item_id=`, `GET /api/inventario/export.csv`. Ejemplos en `backend/README.md`.
|
||||
- Nada se borra. Un ítem que ya no aplica se marca «(obsoleto)» en la descripción y con mínimo 0.
|
||||
241
docs/inventario/inventario.html
Normal file
241
docs/inventario/inventario.html
Normal file
@ -0,0 +1,241 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Inventario de materiales — HBOX</title>
|
||||
<link rel="stylesheet" href="../tableros/tablero.css">
|
||||
<style>
|
||||
table.t td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
table.t th.num{text-align:right}
|
||||
table.t tr.alerta td.num.stock{color:var(--falla);font-weight:700}
|
||||
table.t td .cod{font-weight:700;cursor:pointer;color:var(--naranja)}
|
||||
table.t td .cat{font-size:11px;color:var(--tinta2);display:block}
|
||||
table.t td button{padding:5px 9px;font-size:12.5px;margin:1px 2px 1px 0}
|
||||
.acciones{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 12px}
|
||||
.acciones .cnt{color:var(--tinta2);font-size:13px;margin-left:auto}
|
||||
.mov-fila{display:grid;grid-template-columns:120px 70px 70px 1fr;gap:6px 10px;padding:6px 0;border-bottom:1px dashed var(--linea);font-size:13px;align-items:baseline}
|
||||
.mov-fila .d{font-variant-numeric:tabular-nums}
|
||||
.mov-fila .pos{color:var(--ok);font-weight:700} .mov-fila .neg{color:var(--falla);font-weight:700}
|
||||
@media(max-width:560px){.mov-fila{grid-template-columns:1fr 1fr}}
|
||||
.ayuda{font-size:12.5px;color:var(--tinta2);margin-top:3px}
|
||||
.mini{font-size:12.5px;color:var(--tinta2)}
|
||||
@media(max-width:760px){#cont table.t{min-width:780px}} /* en teléfono: desliza de lado, no aplastes columnas */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<div><h1>Inventario de materiales</h1><div class="meta" id="meta">Cargando…</div></div>
|
||||
<nav>
|
||||
<a href="../">← Documentación</a>
|
||||
<a href="../tableros/produccion.html">Producción</a>
|
||||
<a href="../tableros/kanban.html">Pendientes</a>
|
||||
<a href="./">Política de inventario</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<details class="como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>El <b>stock no se escribe a mano</b>: es la suma de los movimientos. Llegó material → <b>Entrada</b>. Se usó en un ensamble o se envió → <b>Salida</b> con la unidad o el acta como referencia.</li>
|
||||
<li>Contaste y no cuadra → <b>Ajuste</b> con el conteo físico; el sistema calcula la diferencia y la deja registrada con tu nombre.</li>
|
||||
<li>Toca el <b>código</b> para ver los movimientos de ese ítem. Cada uno dice quién, cuándo, cuánto y por qué.</li>
|
||||
<li><b>Alerta</b> (rojo) = stock por debajo del mínimo. El mínimo lo pone quien compra; el tablero de producción lo repite.</li>
|
||||
<li><b>Nuevo ítem</b> solo para cosas que se cuentan: tarjetas, módulos, gabinetes, cajas, fuentes, packs, antenas, cables y los componentes de entrega larga. Nada de resistencias.</li>
|
||||
<li><b>Exportar CSV</b> baja la tabla para Excel. Nada se borra: si un ítem ya no aplica, ponle «(obsoleto)» en la descripción.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div class="acciones">
|
||||
<select id="fCat"><option value="">Todas las categorías</option></select>
|
||||
<select id="fVer"><option value="">Aplica a: todas</option></select>
|
||||
<label style="margin:0;display:flex;gap:6px;align-items:center;font-weight:500"><input type="checkbox" id="fAlerta" style="width:auto"> solo alertas</label>
|
||||
<input type="search" id="fBuscar" placeholder="Buscar código o descripción…" style="flex:1;min-width:150px">
|
||||
<button id="bMov">+ Movimiento</button>
|
||||
<button id="bNuevo" class="sec">+ Nuevo ítem</button>
|
||||
<a href="/api/inventario/export.csv" class="sec" style="padding:8px 12px;border:1px solid var(--linea);border-radius:9px;text-decoration:none;font-size:14px;font-weight:600;background:var(--tarjeta);color:var(--tinta2)">⬇ Exportar CSV</a>
|
||||
<span class="cnt" id="cnt"></span>
|
||||
</div>
|
||||
<div class="msg" id="aviso"></div>
|
||||
<div id="cont" class="tblwrap"></div>
|
||||
</div>
|
||||
|
||||
<dialog id="dlg"></dialog>
|
||||
<script>
|
||||
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
||||
const 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 CAT = { tarjeta: 'Tarjeta', modulo: 'Módulo', gabinete: 'Gabinete', caja: 'Caja', fuente: 'Fuente', bateria: 'Batería', antena: 'Antena', cable: 'Cable', conector: 'Conector', 'componente-critico': 'Componente crítico', otro: 'Otro' };
|
||||
const VERS = ['V5.5', 'V5.6', 'V5.7', 'v6'];
|
||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||
let ITEMS = [], ADMIN = false, USER = '';
|
||||
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); }
|
||||
async function api(url, opts) { const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {})); 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; }
|
||||
const json = (m, b) => ({ method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
|
||||
function campo(l, i, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { class: 'ayuda', text: ayuda })); return d; }
|
||||
|
||||
Object.entries(CAT).forEach(([k, v]) => $('fCat').append(el('option', { value: k, text: v })));
|
||||
VERS.forEach(v => $('fVer').append(el('option', { value: v, text: 'Aplica a: ' + v })));
|
||||
|
||||
function visibles() {
|
||||
const cat = $('fCat').value, ver = $('fVer').value, solo = $('fAlerta').checked, q = $('fBuscar').value.trim().toLowerCase();
|
||||
return ITEMS.filter(i => (!cat || i.categoria === cat) && (!ver || (i.aplica_a || []).includes(ver)) && (!solo || i.alerta)
|
||||
&& (!q || (i.codigo + ' ' + i.descripcion + ' ' + (i.proveedor || '') + ' ' + (i.ubicacion || '')).toLowerCase().includes(q)));
|
||||
}
|
||||
function pintar() {
|
||||
const cont = $('cont'); cont.innerHTML = '';
|
||||
const vis = visibles();
|
||||
$('cnt').textContent = vis.length + ' ítem(s)' + (vis.length !== ITEMS.length ? ' de ' + ITEMS.length : '') + ' · ' + vis.filter(i => i.alerta).length + ' en alerta';
|
||||
if (!vis.length) { cont.append(el('div', { class: 'vacio', text: ITEMS.length ? 'Nada coincide con el filtro.' : 'Todavía no hay ítems. Crea el primero con «Nuevo ítem» o corre la siembra.' })); return; }
|
||||
const t = el('table', { class: 't' });
|
||||
t.innerHTML = '<tr><th>Código</th><th>Descripción</th><th>Aplica a</th><th class="num">Stock</th><th class="num">Mínimo</th><th>Ubicación</th><th>Proveedor</th><th></th></tr>';
|
||||
vis.forEach(i => {
|
||||
const tr = el('tr', { class: i.alerta ? 'alerta' : '' });
|
||||
const cod = el('span', { class: 'cod', text: i.codigo, title: 'Ver movimientos' }); cod.addEventListener('click', () => verMovimientos(i));
|
||||
tr.append(el('td', null, cod, el('span', { class: 'cat', text: CAT[i.categoria] || i.categoria })));
|
||||
tr.append(el('td', null, document.createTextNode(i.descripcion), i.notas ? el('div', { class: 'mini', text: i.notas.length > 90 ? i.notas.slice(0, 89) + '…' : i.notas }) : null));
|
||||
const ap = el('td'); (i.aplica_a || []).forEach(v => ap.append(el('span', { class: BADGE[v] || 'pill pend', text: v, style: 'margin-right:3px' }))); tr.append(ap);
|
||||
tr.append(el('td', { class: 'num stock', html: esc(i.stock) + ' <span class="mini">' + esc(i.unidad) + '</span>' + (i.alerta ? ' <span class="pill falla">alerta</span>' : '') }));
|
||||
tr.append(el('td', { class: 'num', text: i.minimo || '—' }));
|
||||
tr.append(el('td', { text: i.ubicacion || '—' }));
|
||||
tr.append(el('td', { html: esc(i.proveedor || '—') + (i.codigo_proveedor ? '<div class="mini">' + esc(i.codigo_proveedor) + '</div>' : '') }));
|
||||
const acc = el('td', { style: 'white-space:nowrap' });
|
||||
const bm = el('button', { text: 'Movimiento' }); bm.addEventListener('click', () => movimiento(i));
|
||||
const be = el('button', { class: 'sec', text: 'Editar' }); be.addEventListener('click', () => editar(i));
|
||||
acc.append(bm, be); tr.append(acc);
|
||||
t.append(tr);
|
||||
});
|
||||
cont.append(t);
|
||||
}
|
||||
async function cargar() {
|
||||
$('meta').textContent = 'Actualizando…';
|
||||
try {
|
||||
const j = await api('/api/inventario');
|
||||
ITEMS = j.items || []; ADMIN = !!j.admin; USER = j.user || '';
|
||||
pintar();
|
||||
$('meta').textContent = ITEMS.length + ' ítems · ' + (j.alertas || 0) + ' en alerta · actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }) + (USER ? ' · ' + USER : '');
|
||||
} catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo cargar el inventario: ' + e.message })); $('meta').textContent = 'Error'; }
|
||||
}
|
||||
['fCat', 'fVer', 'fAlerta'].forEach(id => $(id).addEventListener('change', pintar));
|
||||
$('fBuscar').addEventListener('input', pintar);
|
||||
|
||||
// ---------- diálogos ----------
|
||||
function abrirDialogo(titulo, cuerpoFn, botones) {
|
||||
dlg.innerHTML = '';
|
||||
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: titulo }));
|
||||
const x = el('button', { type: 'button', text: '✕' }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
||||
const m = el('div', { class: 'msg' });
|
||||
cuerpoFn(cuerpo, m); cuerpo.append(m);
|
||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||
const cancelar = el('button', { type: 'button', class: 'sec', text: botones ? 'Cancelar' : 'Cerrar' }); cancelar.addEventListener('click', () => dlg.close()); pie.append(cancelar);
|
||||
(botones || []).forEach(([txt, fn]) => { const b = el('button', { type: 'button', text: txt }); b.addEventListener('click', async () => { b.disabled = true; try { await fn(m); } catch (e) { m.className = 'msg err'; m.textContent = e.message; } b.disabled = false; }); pie.append(b); });
|
||||
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
||||
}
|
||||
function selItems(sel) {
|
||||
const s = el('select'); s.append(el('option', { value: '', text: '— elegir ítem —' }));
|
||||
ITEMS.forEach(i => s.append(el('option', { value: i.id, text: i.codigo + ' — ' + i.descripcion + ' (' + i.stock + ' ' + i.unidad + ')' })));
|
||||
if (sel) s.value = sel.id; return s;
|
||||
}
|
||||
function movimiento(item) {
|
||||
abrirDialogo('Registrar movimiento', (c) => {
|
||||
const fItem = selItems(item);
|
||||
const fTipo = el('select'); [['entrada', 'Entrada — llegó material (+)'], ['salida', 'Salida — se usó o se envió (−)'], ['ajuste', 'Ajuste — conteo físico (fija el stock)']].forEach(([v, t]) => fTipo.append(el('option', { value: v, text: t })));
|
||||
const fCant = el('input', { type: 'number', min: '0', step: '1', inputmode: 'numeric', placeholder: '0' });
|
||||
const fMot = el('input', { type: 'text', maxlength: '200', placeholder: 'llegó pedido Mouser · ensamble · envío a ICAVE · conteo físico' });
|
||||
const fRef = el('input', { type: 'text', maxlength: '120', placeholder: 'HBOX5-E07-A010-R0006 · OC-0042 · acta de envío' });
|
||||
const ayudaCant = el('div', { class: 'ayuda', text: 'Cantidad que entra.' });
|
||||
fTipo.addEventListener('change', () => { ayudaCant.textContent = fTipo.value === 'entrada' ? 'Cantidad que entra.' : fTipo.value === 'salida' ? 'Cantidad que sale. No puede dejar el stock en negativo.' : 'Lo que contaste físicamente: el stock quedará exactamente en ese número.'; });
|
||||
c.append(campo('Ítem', fItem), campo('Tipo', fTipo));
|
||||
const g = el('div', { class: 'dos-col' }); const cc = campo('Cantidad', fCant); cc.append(ayudaCant); g.append(cc, campo('Referencia (unidad, acta u orden)', fRef)); c.append(g);
|
||||
c.append(campo('Motivo (obligatorio)', fMot));
|
||||
c._leer = () => ({ item_id: fItem.value, tipo: fTipo.value, cantidad: fCant.value === '' ? null : +fCant.value, motivo: fMot.value.trim(), referencia: fRef.value.trim() });
|
||||
setTimeout(() => (item ? fCant : fItem).focus(), 50);
|
||||
}, [['Registrar', async (m) => {
|
||||
const d = dlg.querySelector('.dcuerpo')._leer();
|
||||
if (!d.item_id) throw new Error('Elige el ítem.');
|
||||
if (d.cantidad == null || isNaN(d.cantidad)) throw new Error('Escribe la cantidad.');
|
||||
if (!d.motivo) throw new Error('Escribe el motivo.');
|
||||
const j = await api('/api/inventario/movimiento', json('POST', d));
|
||||
dlg.close(); mensaje('Movimiento registrado: ' + j.item.codigo + ' ahora tiene ' + j.item.stock + ' ' + j.item.unidad + '.', true); cargar();
|
||||
}]]);
|
||||
}
|
||||
function formItem(c, i) {
|
||||
i = i || {};
|
||||
const f = {};
|
||||
f.codigo = el('input', { type: 'text', maxlength: '40', value: i.codigo || '', placeholder: 'EB-V56 · SD-15B-05', style: 'text-transform:uppercase' });
|
||||
f.descripcion = el('input', { type: 'text', maxlength: '200', value: i.descripcion || '' });
|
||||
f.categoria = el('select'); Object.entries(CAT).forEach(([k, v]) => f.categoria.append(el('option', { value: k, text: v }))); f.categoria.value = i.categoria || 'otro';
|
||||
f.unidad = el('select'); [['pza', 'pza (piezas)'], ['m', 'm (metros)']].forEach(([v, t]) => f.unidad.append(el('option', { value: v, text: t }))); f.unidad.value = i.unidad || 'pza';
|
||||
f.aplica = el('div', { class: 'lista-chk', style: 'max-height:none;display:flex;gap:14px;flex-wrap:wrap' });
|
||||
VERS.forEach(v => { const l = el('label', { style: 'margin:0' }); const ck = el('input', { type: 'checkbox', value: v }); ck.checked = (i.aplica_a || []).includes(v); l.append(ck, document.createTextNode(v)); f.aplica.append(l); });
|
||||
f.minimo = el('input', { type: 'number', min: '0', step: '1', value: i.minimo || 0 });
|
||||
f.ubicacion = el('input', { type: 'text', maxlength: '120', value: i.ubicacion || '', placeholder: 'Anaquel A2 · taller Veracruz' });
|
||||
f.proveedor = el('input', { type: 'text', maxlength: '120', value: i.proveedor || '' });
|
||||
f.codigo_proveedor = el('input', { type: 'text', maxlength: '60', value: i.codigo_proveedor || '', placeholder: 'LCSC C2057152 · Mouser 709-SD15B05' });
|
||||
f.notas = el('textarea', { rows: '2' }); f.notas.value = i.notas || '';
|
||||
const g = el('div', { class: 'dos-col' });
|
||||
g.append(campo('Código (único)', f.codigo), campo('Categoría', f.categoria));
|
||||
c.append(g, campo('Descripción', f.descripcion), campo('Aplica a (versión de tarjeta)', f.aplica));
|
||||
const g2 = el('div', { class: 'dos-col' }); g2.append(campo('Unidad', f.unidad), campo('Mínimo (0 = sin alerta)', f.minimo), campo('Ubicación', f.ubicacion), campo('Proveedor', f.proveedor), campo('Código del proveedor', f.codigo_proveedor)); c.append(g2);
|
||||
if (!i.id) { f.stock_inicial = el('input', { type: 'number', min: '0', step: '1', value: '0' }); c.append(campo('Stock inicial (queda registrado como entrada)', f.stock_inicial)); }
|
||||
c.append(campo('Notas', f.notas));
|
||||
return () => {
|
||||
const d = { codigo: f.codigo.value.trim().toUpperCase(), descripcion: f.descripcion.value.trim(), categoria: f.categoria.value, unidad: f.unidad.value,
|
||||
aplica_a: [...f.aplica.querySelectorAll('input:checked')].map(x => x.value), minimo: +f.minimo.value || 0, ubicacion: f.ubicacion.value.trim(),
|
||||
proveedor: f.proveedor.value.trim(), codigo_proveedor: f.codigo_proveedor.value.trim(), notas: f.notas.value.trim() };
|
||||
if (f.stock_inicial) d.stock_inicial = +f.stock_inicial.value || 0;
|
||||
return d;
|
||||
};
|
||||
}
|
||||
function nuevo() {
|
||||
let leer;
|
||||
abrirDialogo('Nuevo ítem', c => { leer = formItem(c); }, [['Crear ítem', async () => {
|
||||
const d = leer(); if (!d.codigo) throw new Error('Escribe el código.'); if (!d.descripcion) throw new Error('Escribe la descripción.');
|
||||
await api('/api/inventario', json('POST', d)); dlg.close(); mensaje('Ítem ' + d.codigo + ' creado.', true); cargar();
|
||||
}]]);
|
||||
}
|
||||
function editar(i) {
|
||||
let leer;
|
||||
abrirDialogo('Editar ' + i.codigo, c => {
|
||||
leer = formItem(c, i);
|
||||
const h = el('div', { class: 'hist' });
|
||||
h.append(el('div', { html: '<b>Creado</b> por ' + esc(i.creado_por) + ' el ' + esc(fmt(i.creado_en)) + ' · stock actual <b>' + esc(i.stock) + ' ' + esc(i.unidad) + '</b> (se cambia con movimientos, no aquí)' }));
|
||||
(i.historial || []).slice().reverse().slice(0, 12).forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
||||
c.append(h);
|
||||
}, [['Guardar', async () => {
|
||||
const d = leer(); const cambios = {};
|
||||
Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(i[k] == null ? (Array.isArray(d[k]) ? [] : (typeof d[k] === 'number' ? 0 : '')) : i[k])) cambios[k] = d[k]; });
|
||||
if (Object.keys(cambios).length) await api('/api/inventario/item?id=' + encodeURIComponent(i.id), json('PATCH', cambios));
|
||||
dlg.close(); mensaje('Ítem guardado.', true); cargar();
|
||||
}]]);
|
||||
}
|
||||
async function verMovimientos(i) {
|
||||
abrirDialogo('Movimientos de ' + i.codigo, async c => {
|
||||
c.append(el('p', { class: 'sub', html: esc(i.descripcion) + ' · stock <b>' + esc(i.stock) + ' ' + esc(i.unidad) + '</b>' + (i.minimo ? ' · mínimo ' + esc(i.minimo) : '') }));
|
||||
const lista = el('div', { text: 'Cargando…' }); c.append(lista);
|
||||
const b = el('button', { text: '+ Movimiento', style: 'margin-top:10px' }); b.addEventListener('click', () => movimiento(i)); c.append(b);
|
||||
try {
|
||||
const j = await api('/api/inventario/movimientos?item_id=' + encodeURIComponent(i.id));
|
||||
lista.innerHTML = '';
|
||||
if (!j.total) { lista.append(el('div', { class: 'vacio', text: 'Sin movimientos. El stock es 0 hasta que registres una entrada o un ajuste.' })); return; }
|
||||
lista.append(el('div', { class: 'mov-fila', style: 'font-weight:700;color:var(--tinta2);font-size:11.5px;text-transform:uppercase' }, el('span', { text: 'Cuándo · quién' }), el('span', { text: 'Tipo' }), el('span', { text: 'Δ · stock' }), el('span', { text: 'Motivo · referencia' })));
|
||||
j.movimientos.forEach(m => {
|
||||
lista.append(el('div', { class: 'mov-fila' },
|
||||
el('span', { html: esc(fmt(m.cuando)) + '<br><b>' + esc(m.quien) + '</b>' }),
|
||||
el('span', { text: m.tipo + (m.tipo === 'ajuste' ? ' → ' + m.cantidad : '') }),
|
||||
el('span', { class: 'd', html: '<span class="' + (m.delta >= 0 ? 'pos' : 'neg') + '">' + (m.delta >= 0 ? '+' : '') + esc(m.delta) + '</span> · ' + esc(m.stock_despues) }),
|
||||
el('span', { html: esc(m.motivo) + (m.referencia ? '<br><span class="mini">ref. ' + esc(m.referencia) + '</span>' : '') })));
|
||||
});
|
||||
} catch (e) { lista.textContent = 'No se pudieron leer los movimientos: ' + e.message; }
|
||||
});
|
||||
}
|
||||
$('bMov').addEventListener('click', () => movimiento(null));
|
||||
$('bNuevo').addEventListener('click', nuevo);
|
||||
cargar();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -11,12 +11,13 @@ author: Helgen Technologies
|
||||
|
||||
Cada manual cubre cómo reconocer la tarjeta, conectores, puentes, botones, LEDs, flasheo y limitaciones. El binario lleva la tarjeta en el nombre; reglas en [Descargas](../descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta); qué cambia entre versiones, en las [notas de versión](../descargas/notas-de-version.md).
|
||||
|
||||
| Tarjeta | Nombre | Estado (sep-2026) | Firmware | Proyecto KiCad | Manual |
|
||||
| Tarjeta | Nombre | Estado (oct-2026) | Firmware | Proyecto KiCad | Manual |
|
||||
|---|---|---|---|---|---|
|
||||
| V3 | EinarBoard 1.0 (Blackpill en zócalo) | 10 pzas jul-2026; 3 unidades en campo (RTK-003/006/014); registro congelado | `hbox-tarjetaV3_4.0.0.bin` | `hbox-watchdog-v3-PROD` | [Manual V3](manual-tarjeta-V3.md) |
|
||||
| V4 | EinarBoard 2.0 (Blackpill + Ethernet + serie) | Fabricada ago-2026; en prueba con 0.6.0 | `hbox-tarjetaV4_0.6.0.bin` | `hbox-watchdog-v4` | [Manual V4](manual-tarjeta-V4.md) |
|
||||
| V5.5 | EinarBoard 4.0 (serigrafía) / HBOX v5, MCU soldado | 15 pzas 22-ago-2026; en ensamble y campo | `hbox-v5-bringup_0.6.0.bin` | `hbox-watchdog-v5` (tag `v5.5-as-fabbed`) | [Manual V5](manual-tarjeta-V5.md) |
|
||||
| V5.6 | HBOX v5 + ECO 27/28-sep (módem controlado por MCU, LFP, sensores) | Esquemático cerrado; PCB en layout; pedido pendiente | 0.7.0 (0.6.x no enciende su módem: no maneja `MODEM_EN`) | `hbox-watchdog-v5` | [Manual V5](manual-tarjeta-V5.md) §9 |
|
||||
| V5.6 | HBOX v5 + ECO 27/28-sep (módem controlado por MCU, LFP, sensores) + acelerómetro U26, sin P12 | Layout en pasada 2 (Michelle, 13-oct); pedido de **113** con fuente SD-15B | 0.7.0 (0.6.x no enciende su módem: no maneja `MODEM_EN`) | `hbox-watchdog-v5` | [Manual V5](manual-tarjeta-V5.md) §9 |
|
||||
| V5.7 | V5.6 + fuente 24 V → 5 V a bordo (U25 TPS54360B, fusible F2); sin SD-15B ni P2 | En diseño: esquemático listo (13-oct), layout en curso (Leo → Michelle); pedido de **327** | 0.7.x (igual que V5.6; sin política «ventilador solo con 24 V») | `hbox-watchdog-v57` | [Manual V5](manual-tarjeta-V5.md) §9 + [ECO V5.7](../hardware/v57-eco-fuente.md) |
|
||||
| v6 | GNSS + 4G a bordo | En diseño; no pedir | — | `hbox-watchdog-v6` | [Manual v6](manual-tarjeta-v6.md) |
|
||||
|
||||
Documentos de ingeniería: [Tarjetas y hardware](../hardware/index.md).
|
||||
|
||||
@ -8,6 +8,10 @@ author: Helgen Technologies
|
||||
|
||||
# Guía de ensamble del gabinete (PDF)
|
||||
|
||||
!!! warning "Documento histórico — archivo equivocado"
|
||||
El PDF 0007-1000 publicado aquí es la guía de ensamble de un brazo robótico, no del gabinete HBOX (detectado el 17-sep-2026). Se conserva solo como registro hasta sustituirlo por la guía real del gabinete. Para armar una unidad V5 usa [Ensamble de la unidad](../procedimientos/v5-ensamble.md).
|
||||
|
||||
|
||||
[0007-1000_Assembly_Guide.pdf](0007-1000_Assembly_Guide.pdf){:target="_blank"} (≈ 9 MB). Descárgala en la tablet o laptop del banco antes de empezar. Orden de trabajo y reglas de calidad: [guía de ensamble y QC](guia-ensamble-qc.md).
|
||||
|
||||
## Historial de revisiones
|
||||
|
||||
@ -9,6 +9,10 @@ version: 0.9.2
|
||||
|
||||
# Guía de ensamble y control de calidad — HBOX
|
||||
|
||||
!!! note "Documento histórico — superado por el ensamble V5 (17-sep-2026)"
|
||||
Borrador escrito para la V3 a partir del BOM de ensamble y el pinout de arneses; nunca se validó. Para V5.5/V5.6 el procedimiento vigente es [Ensamble de la unidad](../procedimientos/v5-ensamble.md) con su [acta](../formularios/ensamble.html).
|
||||
|
||||
|
||||
!!! abstract "Para quién · Cuándo · Tiempo · Necesitas"
|
||||
- Estado: borrador V3 basado en el BOM de ensamble y el pinout de arneses; ingeniería debe validarlo. Para V5.5/V5.6: [Ensamble de la unidad](../procedimientos/v5-ensamble.md) y [pinout V5.6](../hardware/v56-pinout.md).
|
||||
- Para quién: técnicos de banco.
|
||||
|
||||
@ -1,19 +1,45 @@
|
||||
---
|
||||
title: "Producción — lote V5.6"
|
||||
title: "Producción"
|
||||
---
|
||||
|
||||
# Producción — lote V5.6
|
||||
# Producción
|
||||
|
||||
| Paso | Procedimiento | Acta |
|
||||
|---|---|---|
|
||||
| 1 | [Prueba y flasheo de tarjetas](../procedimientos/v5-prueba-y-flasheo.md) | [Tarjeta V5](../formularios/v5.html) |
|
||||
| 2 | [Módulos RTK y 4G e integración](../procedimientos/v5-modulos-integracion.md) | [ArduSimple](../formularios/ardusimple.html) · [Módem 4G](../formularios/modem4g.html) · [Integración](../formularios/integracion.html) |
|
||||
| 3 | [Ensamble de la unidad](../procedimientos/v5-ensamble.md) | [Ensamble](../formularios/ensamble.html) |
|
||||
| 4 | [Prueba final antes de embarcar](../procedimientos/v5-prueba-final-unidad.md) | En el acta de ensamble |
|
||||
| 5 | [Configuración de la unidad](../procedimientos/v5-configuracion-unidad.md) | [Ensamble](../formularios/ensamble.html) · [Ethernet y serie](../formularios/ethernet-serie.html) si aplica |
|
||||
| 6 | [Checklist de puesta en marcha en el puerto](../procedimientos/v5-checklist-puesta-en-marcha.md) | Impreso, al expediente |
|
||||
De la tarjeta desnuda a la unidad lista para embarcar. Una unidad a la vez; cada paso deja su acta y la unidad no avanza si falla un paso. Plan y avance real: [Tablero de producción](../tableros/produccion.html). Existencias: [Inventario](../inventario/inventario.html).
|
||||
|
||||
- [Tablero de producción](../tablero.html): qué tarjeta, módulo y unidad pasó cada paso.
|
||||
- [Expediente de una unidad](../unidad.html)
|
||||
- [Índice de actas](../actas/index.md)
|
||||
- [Descargas](../descargas/index.md): firmware vigente y SHA-256.
|
||||
## Plan de 570 unidades (13-oct-2026)
|
||||
|
||||
| Versión | Cantidad | Fuente de 5 V | Tarjeta | Estado del diseño |
|
||||
|---|---|---|---|---|
|
||||
| <span class="badge-v56">V5.6</span> | **113** | Mean Well SD-15B-05 externa (×113) | `hbox-watchdog-v5` | En layout — pasada 2; no se pide todavía |
|
||||
| <span class="badge-v57">V5.7</span> | **327** | A bordo (U25) | `hbox-watchdog-v57` | En diseño — se pide después del banco de V5.6 |
|
||||
| <span class="badge-v6">v6</span> | **130** + reemplazos | A bordo (U25) | `hbox-watchdog-v6` | En diseño — PCB sin corregir |
|
||||
|
||||
Hoy se producen unidades con las **15 tarjetas V5.5** y se prueban las **V4**. Los procedimientos de abajo valen para V5.5 y V5.6 (la V5.6 cambia el firmware a 0.7.0 y el paquete a LFP-HT; se marcará en cada página cuando llegue el lote). Paquetes de fabricación V5.6/V5.7/v6: los prepara ingeniería ([Tarjetas y hardware](../hardware/index.md)).
|
||||
|
||||
## Pipeline — paso, procedimiento, acta
|
||||
|
||||
| Paso | Qué entra → qué sale | Procedimiento | Acta |
|
||||
|---|---|---|---|
|
||||
| 1 | Tarjeta desnuda V5-NN → tarjeta aprobada y flasheada | [Prueba y flasheo de tarjetas](../procedimientos/v5-prueba-y-flasheo.md) | [Tarjeta V5](../formularios/v5.html) |
|
||||
| 2 | simpleRTK2B ARD-NNN y módem RTK-NNNN → pareja integrada con RTK FIX | [Módulos RTK y 4G e integración](../procedimientos/v5-modulos-integracion.md) | [ArduSimple](../formularios/ardusimple.html) · [Módem 4G](../formularios/modem4g.html) · [Integración](../formularios/integracion.html) |
|
||||
| 3 | Tarjeta + pareja → unidad cerrada `HBOX5-Exx-Axxx-Rxxxx`, energizada y reportando | [Ensamble de la unidad](../procedimientos/v5-ensamble.md) | [Ensamble](../formularios/ensamble.html) |
|
||||
| 4 | Unidad ensamblada → unidad probada (P1–P6) | [Prueba final antes de embarcar](../procedimientos/v5-prueba-final-unidad.md) | En el acta de ensamble |
|
||||
| 5 | Unidad probada → unidad configurada (4G, F9P, ID, Ethernet/serie si aplica) | [Configuración de la unidad](../procedimientos/v5-configuracion-unidad.md) | [Ensamble](../formularios/ensamble.html) · [Ethernet y serie](../formularios/ethernet-serie.html) si aplica |
|
||||
| 6 | Unidad configurada → unidad en caja, rumbo al puerto | [Ciclo de vida §6](../procedimientos/ciclo-de-vida.md#6-envio) | [Envío](../formularios/envio.html) |
|
||||
| 7 | Unidad en el puerto → instalada y reportando | [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md) (en **Campo**) | [Instalación en campo](../formularios/instalacion.html) |
|
||||
|
||||
Antes de montar módulos revisa **U6 (LTC4040) con lupa**: bolas de soldadura o puentes son el defecto recurrente de V5.5 y V4 (regla DFM del 8-oct). Una tarjeta con U6 dudosa no se energiza; va al [acta de tarjeta](../formularios/v5.html) como falla con foto.
|
||||
|
||||
## Tarjetas V4
|
||||
|
||||
La V4 no entra al pipeline de unidades V5; se prepara y prueba en [Taller](../taller/index.md): [Preparación y prueba con 0.6.0](../procedimientos/v4-prueba-fw-0.5.6.md) · [acta V4](../formularios/v4.html).
|
||||
|
||||
## Dónde ver el avance
|
||||
|
||||
- [Tablero de producción](../tableros/produccion.html): plan 113 / 327 / 130 contra las unidades reales por etapa; bloqueos.
|
||||
- [Estado de actas](../tablero.html): qué tarjeta, módulo y unidad pasó cada paso (series V5-NN, ARD-NNN, RTK-NNNN).
|
||||
- [Expediente de una unidad](../unidad.html): la historia completa de una serie.
|
||||
- [Inventario](../inventario/index.md): tarjetas, módulos, gabinetes, cajas, paquetes, fuentes; mínimos y alertas.
|
||||
- [Kanban](../tableros/kanban.html): tareas de la pista Producción y Compras.
|
||||
- [Descargas](../descargas/index.md): firmware vigente por tarjeta y SHA-256. Hoy: 0.6.0 CAMPO; 0.6.3 solo banco.
|
||||
- [Índice de actas](../actas/index.md): cómo se llenan y qué registra cada una.
|
||||
|
||||
64
docs/sitios/index.md
Normal file
64
docs/sitios/index.md
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
title: "Sitios"
|
||||
date: 2026-10-13
|
||||
tags: [project-drogheda, hbox, sitios, campo, instalacion]
|
||||
---
|
||||
|
||||
# Sitios de instalación
|
||||
|
||||
Un sitio es un puerto o terminal donde hay unidades HBOX instaladas (hoy: ICAVE, LZC, EIT, TIMSA). Cada sitio tiene su página con contactos, unidades, instalaciones, diagnósticos y daños, armada a partir de las actas.
|
||||
|
||||
<div id="lista-sitios" class="buscar-aviso" markdown="0">Cargando sitios…</div>
|
||||
<script>
|
||||
(function(){
|
||||
var caja=document.getElementById('lista-sitios');
|
||||
fetch('/api/sitios?todos=1',{cache:'no-store'}).then(function(r){return r.json();}).then(function(j){
|
||||
var ss=j.sitios||[]; if(!ss.length){caja.innerHTML='Todavía no hay sitios dados de alta. '+(j.admin?'<a href="sitio.html?nuevo=1">Crear el primero</a>.':'Pide a Leo que los cree.');return;}
|
||||
var h='<ul style="margin:0 0 .4em">';
|
||||
ss.forEach(function(s){var ub=s.ubicacion&&s.ubicacion!=='por confirmar'?' — '+s.ubicacion:' — ubicación por confirmar';
|
||||
h+='<li><a href="sitio.html?id='+encodeURIComponent(s.id)+'"><b>'+s.nombre+'</b></a>'+(s.cliente?' · '+s.cliente:'')+ub+(s.activo===false?' <em>(inactivo)</em>':'')+'</li>';});
|
||||
h+='</ul>'+(j.admin?'<a class="md-button md-button--primary" href="sitio.html?nuevo=1">+ Nuevo sitio</a>':'<small>Para dar de alta un sitio, pide a Leo (administrador).</small>');
|
||||
caja.innerHTML=h;
|
||||
}).catch(function(){caja.innerHTML='No se pudo leer la lista de sitios (¿backend apagado?). Entra directo: <a href="sitio.html?id=icave">ICAVE</a> · <a href="sitio.html?id=lzc">LZC</a> · <a href="sitio.html?id=eit">EIT</a> · <a href="sitio.html?id=timsa">TIMSA</a>';});
|
||||
})();
|
||||
</script>
|
||||
|
||||
## Sitios actuales
|
||||
|
||||
| Sitio | Puerto | Operador | Página |
|
||||
|---|---|---|---|
|
||||
| **ICAVE** | Veracruz | Hutchison Ports | [Abrir](sitio.html?id=icave) |
|
||||
| **LZC** | Lázaro Cárdenas | Hutchison Ports | [Abrir](sitio.html?id=lzc) |
|
||||
| **EIT** | Ensenada | Hutchison Ports | [Abrir](sitio.html?id=eit) |
|
||||
| **TIMSA** | Manzanillo | Hutchison Ports | [Abrir](sitio.html?id=timsa) |
|
||||
|
||||
En notas antiguas Lázaro Cárdenas aparece como «LCT»; el nombre oficial en el sitio es **LZC**.
|
||||
|
||||
## Cómo se relaciona con las actas
|
||||
|
||||
- La **acta de instalación en campo** ([abrir](../formularios/instalacion.html)) registra sitio, unidad, vehículo, antena, alimentación, RTK FIX y fotos. Es la que da de alta la unidad en el sitio.
|
||||
- Las actas de **envío**, **recepción** y **diagnóstico** llevan un campo *sitio* desde oct-2026, para que el expediente muestre dónde estaba la unidad en cada momento.
|
||||
- Procedimiento de instalación: [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md). Procedimiento de falla: [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md).
|
||||
|
||||
## Cómo se usa
|
||||
|
||||
1. Abre el sitio para ver **qué unidades están ahí**, en qué máquina y desde cuándo. Toca una serie para ir a su expediente.
|
||||
2. Las unidades aparecen solas cuando un acta trae el campo **Sitio**: [instalación](../formularios/instalacion.html), [envío](../formularios/envio.html), [recepción](../formularios/recepcion.html), [diagnóstico](../formularios/diagnostico.html) y [daño](../formularios/dano.html). Elige siempre el sitio en el formulario.
|
||||
3. Las actas anteriores al 13-oct no traen sitio: se buscan por el nombre del puerto y se marcan «por texto». Si una falta, no se corrige el acta; se registra la siguiente con el sitio correcto.
|
||||
4. **Contactos y ubicación** los edita el administrador desde la página del sitio («Editar sitio»). Los datos «por confirmar» son los que faltan.
|
||||
|
||||
## Dar de alta un sitio (administrador)
|
||||
|
||||
1. En la página de cualquier sitio, elige **+ Nuevo sitio…** en el selector, o entra a [`sitio.html?nuevo=1`](sitio.html?nuevo=1).
|
||||
2. Id corto en minúsculas (`icave`, `lzc`), nombre como lo dice el cliente, cliente, ubicación.
|
||||
3. **Alias**: los nombres con los que ese sitio aparece en actas viejas (por ejemplo `LCT` para LZC), para que la búsqueda por texto las encuentre.
|
||||
4. Contactos: nombre, rol, teléfono, correo. Guarda. El sitio aparece de inmediato en los formularios.
|
||||
|
||||
Un sitio no se borra: se marca **inactivo** y deja de salir en los formularios, pero su historial sigue visible.
|
||||
|
||||
## Relacionados
|
||||
|
||||
- [Acta de instalación en campo](../formularios/instalacion.html) — una por unidad y vehículo.
|
||||
- [Tablero de campo](../diagnostico/tablero-campo.html) — diagnósticos por unidad.
|
||||
- [Expediente de una unidad](../unidad.html) — producción → envío → instalación → campo.
|
||||
- [Pendientes de campo](../tableros/kanban.html?pista=campo)
|
||||
204
docs/sitios/sitio.html
Normal file
204
docs/sitios/sitio.html
Normal file
@ -0,0 +1,204 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sitio — HBOX</title>
|
||||
<link rel="stylesheet" href="../tableros/tablero.css">
|
||||
<style>
|
||||
.ficha{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-bottom:14px}
|
||||
.ficha .c{background:var(--tarjeta);border:1px solid var(--linea);border-radius:10px;padding:10px 12px}
|
||||
.ficha .l{font-size:11px;color:var(--tinta2);text-transform:uppercase;letter-spacing:.05em}
|
||||
.ficha .v{font-weight:600;font-size:15px}
|
||||
.contacto{padding:6px 0;border-top:1px dashed var(--linea);font-size:14px}
|
||||
.contacto:first-of-type{border-top:0}
|
||||
.contacto .r{color:var(--tinta2);font-size:12.5px}
|
||||
.linea{position:relative;padding-left:18px;border-left:2px solid var(--linea);margin-left:6px}
|
||||
.ev{position:relative;padding:9px 0 9px 4px;border-bottom:1px solid var(--linea);font-size:14px}
|
||||
.ev:last-child{border-bottom:0}
|
||||
.ev::before{content:"";position:absolute;left:-25px;top:15px;width:11px;height:11px;border-radius:50%;background:var(--pend);border:2px solid var(--papel)}
|
||||
.ev.instalacion-campo::before{background:var(--ok)} .ev.diagnostico-campo::before{background:var(--naranja)}
|
||||
.ev.dano::before{background:var(--falla)} .ev.envio::before,.ev.recepcion::before{background:var(--info)}
|
||||
.ev .f{color:var(--tinta2);font-size:12.5px}
|
||||
.sitio-sel{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
|
||||
.contactos-ed .fila{display:grid;grid-template-columns:1.2fr 1fr 1fr 1.2fr auto;gap:6px;margin-bottom:6px}
|
||||
@media(max-width:700px){.contactos-ed .fila{grid-template-columns:1fr 1fr}}
|
||||
.contactos-ed .fila button{padding:6px 9px;font-size:13px}
|
||||
.por-texto{font-size:11px;color:var(--tinta2);border:1px dashed var(--linea);border-radius:99px;padding:0 7px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<div><h1 id="titulo">Sitio</h1><div class="meta" id="meta">Cargando…</div></div>
|
||||
<nav>
|
||||
<a href="../">← Documentación</a>
|
||||
<a href="./">Sitios</a>
|
||||
<a href="../formularios/instalacion.html">Nueva instalación</a>
|
||||
<a href="../formularios/diagnostico.html">Diagnóstico</a>
|
||||
<a href="../tableros/kanban.html?pista=campo">Pendientes de campo</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<details class="como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>Esta página junta todo lo que pasó en un sitio: unidades instaladas, envíos, recepciones, diagnósticos y daños. Sale de las actas; aquí no se captura nada de campo.</li>
|
||||
<li>Para que una unidad aparezca, el acta debe traer el <b>sitio</b> elegido en el formulario (instalación, envío, recepción, diagnóstico, daño). Las actas viejas sin sitio se buscan por el nombre del puerto y se marcan «por texto».</li>
|
||||
<li>Toca una serie para abrir su expediente completo. Rojo = unidad con marca de daño o baja.</li>
|
||||
<li>Contactos y ubicación los edita solo el administrador (botón «Editar sitio»). Si falta un dato, díselo a Leo.</li>
|
||||
<li>Para dar de alta otro sitio: <a href="./">lista de sitios</a> → «Nuevo sitio» (administrador).</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div class="sitio-sel"><label style="margin:0">Sitio</label><select id="sel"></select><button id="bEditar" class="sec" style="display:none">Editar sitio</button></div>
|
||||
<div class="msg" id="aviso"></div>
|
||||
<div id="cont"></div>
|
||||
</div>
|
||||
<dialog id="dlg"></dialog>
|
||||
<script>
|
||||
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
||||
const 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' };
|
||||
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); }
|
||||
async function api(url, opts) { const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {})); 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; }
|
||||
const json = (m, b) => ({ method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
|
||||
function campo(l, i) { const d = el('div'); d.append(el('label', { text: l }), i); return d; }
|
||||
|
||||
function render(j) {
|
||||
const s = j.sitio, cont = $('cont'); cont.innerHTML = '';
|
||||
$('titulo').textContent = s.nombre + (s.cliente ? ' — ' + s.cliente : '');
|
||||
$('meta').textContent = (s.ubicacion || 'ubicación por confirmar') + ' · ' + j.total_unidades + ' unidad(es) · ' + j.total_actas + ' acta(s)' + (s.activo === false ? ' · INACTIVO' : '');
|
||||
document.title = s.nombre + ' — sitio HBOX';
|
||||
const ficha = el('div', { class: 'ficha' });
|
||||
const c = (l, v) => ficha.append(el('div', { class: 'c' }, el('div', { class: 'l', text: l }), el('div', { class: 'v', html: v })));
|
||||
c('Cliente', esc(s.cliente || '—')); c('Ubicación', esc(s.ubicacion || 'por confirmar'));
|
||||
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);
|
||||
if (s.notas) cont.append(el('p', { class: 'sub', style: 'margin:-6px 0 12px', text: s.notas }));
|
||||
|
||||
// contactos
|
||||
const b0 = el('section', { class: 'bloque' });
|
||||
b0.append(el('h2', { text: 'Contactos' }));
|
||||
if (!(s.contactos || []).length) b0.append(el('div', { class: 'vacio', text: 'Sin contactos registrados. Pide a Leo que los agregue.' }));
|
||||
(s.contactos || []).forEach(k => b0.append(el('div', { class: 'contacto', html: '<b>' + esc(k.nombre) + '</b>' + (k.rol ? ' <span class="r">· ' + esc(k.rol) + '</span>' : '') + (k.telefono ? ' · <a href="tel:' + esc(k.telefono) + '">' + esc(k.telefono) + '</a>' : '') + (k.correo ? ' · <a href="mailto:' + esc(k.correo) + '">' + esc(k.correo) + '</a>' : '') })));
|
||||
cont.append(b0);
|
||||
|
||||
// unidades
|
||||
const b1 = el('section', { class: 'bloque' });
|
||||
b1.append(el('h2', { text: 'Unidades en este sitio' }), el('p', { class: 'sub', text: 'Toda serie con alguna acta que apunte a este sitio. «Instalada» = fecha de la última acta de instalación aprobada.' }));
|
||||
if (!j.unidades.length) b1.append(el('div', { class: 'vacio', text: 'Todavía no hay unidades con actas en este sitio. Llena un acta de instalación o de envío con este sitio.' }));
|
||||
else {
|
||||
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Unidad</th><th>Máquina</th><th>Instalada</th><th>Última acta</th><th>Actas</th><th>Estado</th></tr>';
|
||||
j.unidades.forEach(u => {
|
||||
const tr = el('tr');
|
||||
tr.innerHTML = '<td><a href="../unidad.html?serie=' + encodeURIComponent(u.serie) + '"><b>' + esc(u.serie) + '</b></a>' + (u.coincidencia === 'texto' ? ' <span class="por-texto" title="el acta no trae el sitio; coincide por el nombre del puerto">por texto</span>' : '') + '</td>'
|
||||
+ '<td>' + esc(u.maquina || '—') + '</td><td>' + (u.instalada ? '<span class="pill ok">' + esc(u.instalada) + '</span>' : '<span class="pill pend">sin acta</span>') + '</td>'
|
||||
+ '<td>' + esc(NOMBRE[(u.ultima || {}).tipo] || (u.ultima || {}).tipo || '—') + (u.ultima && u.ultima.fecha ? ' · ' + esc(u.ultima.fecha) : '') + (u.ultima && u.ultima.accion ? '<div style="font-size:12px;color:var(--tinta2)">' + esc(u.ultima.accion) + '</div>' : '') + '</td>'
|
||||
+ '<td>' + u.actas + '</td><td>' + (u.marca ? '<span class="pill ' + (u.marca === 'BAJA' ? 'falla' : 'aviso') + '">' + (u.marca === 'BAJA' ? 'BAJA' : 'DAÑADA') + '</span>' : '<span class="pill ok">sin marca</span>') + '</td>';
|
||||
t.append(tr);
|
||||
});
|
||||
b1.append(el('div', { class: 'tblwrap' }, t));
|
||||
}
|
||||
cont.append(b1);
|
||||
|
||||
// daños abiertos
|
||||
if (j.danos.length) {
|
||||
const b2 = el('section', { class: 'bloque' });
|
||||
b2.append(el('h2', { text: 'Daños abiertos' }), el('p', { class: 'sub', text: 'Unidades de este sitio con marca vigente. Se levanta con un acta de daño «Reparada».' }));
|
||||
j.danos.forEach(u => { const a = u.marca_acta || {}; b2.append(el('div', { class: 'ev dano', style: 'padding-left:0', html: '<b><a href="../unidad.html?serie=' + encodeURIComponent(u.serie) + '">' + esc(u.serie) + '</a></b> · ' + esc(a.clasificacion || '') + (a.resumen ? ' — ' + esc(a.resumen) : '') + '<div class="f">' + esc(a.fecha || fmt(a.recibido)) + ' · ' + esc(a.tecnico || '') + (a.accion ? ' · ' + esc(a.accion) : '') + '</div>' })); });
|
||||
cont.append(b2);
|
||||
}
|
||||
|
||||
// actas
|
||||
const b3 = el('section', { class: 'bloque' });
|
||||
b3.append(el('h2', { text: 'Actas de instalación y campo' }), el('p', { class: 'sub', text: 'De la más reciente a la más vieja: instalaciones, diagnósticos, envíos, recepciones y daños.' }));
|
||||
if (!j.actas.length) b3.append(el('div', { class: 'vacio', text: 'Sin actas.' }));
|
||||
else {
|
||||
const lt = el('div', { class: 'linea' });
|
||||
j.actas.forEach(a => {
|
||||
const r = String(a.resultado || '');
|
||||
lt.append(el('div', { class: 'ev ' + a.tipo, html: '<b>' + esc(NOMBRE[a.tipo] || a.tipo) + '</b> — <a href="../unidad.html?serie=' + encodeURIComponent(a.serie) + '">' + esc(a.serie) + '</a>'
|
||||
+ (a.maquina ? ' · ' + esc(a.maquina) : '') + ' <span class="pill ' + (r === 'FALLA' ? 'falla' : r ? 'ok' : 'pend') + '">' + esc(r || '—') + '</span>'
|
||||
+ (a.coincidencia === 'texto' ? ' <span class="por-texto">por texto</span>' : '')
|
||||
+ '<div class="f">' + esc(a.fecha || '') + ' · recibida ' + esc(fmt(a.recibido)) + ' · ' + esc(a.tecnico || '—') + (a.fotos ? ' · ' + a.fotos + ' foto(s)' : '') + '</div>'
|
||||
+ (a.accion ? '<div>→ ' + esc(a.accion) + '</div>' : '') + (a.motivo ? '<div>Motivo: ' + esc(a.motivo) + '</div>' : '') + (a.resumen ? '<div>' + esc(a.resumen) + '</div>' : '') }));
|
||||
});
|
||||
b3.append(lt);
|
||||
}
|
||||
cont.append(b3);
|
||||
}
|
||||
|
||||
async function cargar(id) {
|
||||
$('cont').innerHTML = '<p class="vacio">Cargando…</p>';
|
||||
try { DATOS = await api('/api/sitio/unidades?id=' + encodeURIComponent(id)); ACTUAL = DATOS.sitio; render(DATOS); $('bEditar').style.display = ADMIN ? '' : 'none'; }
|
||||
catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo leer el sitio: ' + e.message })); $('meta').textContent = 'Error'; }
|
||||
}
|
||||
|
||||
// ---------- alta / edición (admin) ----------
|
||||
function formSitio(c, s) {
|
||||
s = s || {}; const f = {};
|
||||
f.id = el('input', { type: 'text', maxlength: '30', value: s.id || '', placeholder: 'icave (minúsculas, sin espacios)' }); if (s.id) f.id.disabled = true;
|
||||
f.nombre = el('input', { type: 'text', maxlength: '80', value: s.nombre || '', placeholder: 'ICAVE' });
|
||||
f.cliente = el('input', { type: 'text', maxlength: '120', value: s.cliente || '', placeholder: 'Hutchison Ports' });
|
||||
f.ubicacion = el('input', { type: 'text', maxlength: '200', value: s.ubicacion || '', placeholder: 'Veracruz, Ver.' });
|
||||
f.alias = el('input', { type: 'text', value: (s.alias || []).join(', '), placeholder: 'LCT, Lázaro Cárdenas (nombres con los que aparece en actas viejas)' });
|
||||
f.notas = el('textarea', { rows: '2' }); f.notas.value = s.notas || '';
|
||||
f.activo = el('select'); [['true', 'Activo'], ['false', 'Inactivo (ya no se muestra en los formularios)']].forEach(([v, t]) => f.activo.append(el('option', { value: v, text: t }))); f.activo.value = s.activo === false ? 'false' : 'true';
|
||||
const g = el('div', { class: 'dos-col' }); g.append(campo('Id (slug)', f.id), campo('Nombre', f.nombre), campo('Cliente', f.cliente), campo('Ubicación', f.ubicacion)); c.append(g);
|
||||
c.append(campo('Alias (separados por coma)', f.alias));
|
||||
const ced = el('div', { class: 'contactos-ed' });
|
||||
function fila(k) { const r = el('div', { class: 'fila' }); r.append(el('input', { type: 'text', value: k.nombre || '', placeholder: 'Nombre' }), el('input', { type: 'text', value: k.rol || '', placeholder: 'Rol' }), el('input', { type: 'text', value: k.telefono || '', placeholder: 'Teléfono' }), el('input', { type: 'text', value: k.correo || '', placeholder: 'Correo' })); const q = el('button', { type: 'button', class: 'sec', text: '✕' }); q.addEventListener('click', () => r.remove()); r.append(q); ced.append(r); }
|
||||
(s.contactos || []).forEach(fila);
|
||||
const add = el('button', { type: 'button', class: 'sec', text: '+ contacto', style: 'padding:6px 10px;font-size:13px' }); add.addEventListener('click', () => fila({}));
|
||||
c.append(campo('Contactos', ced), add, campo('Notas', f.notas), campo('Estado', f.activo));
|
||||
return () => ({ id: f.id.value.trim().toLowerCase(), nombre: f.nombre.value.trim(), cliente: f.cliente.value.trim(), ubicacion: f.ubicacion.value.trim(),
|
||||
alias: f.alias.value.split(',').map(x => x.trim()).filter(Boolean), notas: f.notas.value.trim(), activo: f.activo.value === 'true',
|
||||
contactos: [...ced.querySelectorAll('.fila')].map(r => ({ nombre: r.children[0].value.trim(), rol: r.children[1].value.trim(), telefono: r.children[2].value.trim(), correo: r.children[3].value.trim() })).filter(k => k.nombre) });
|
||||
}
|
||||
function dialogo(titulo, s) {
|
||||
dlg.innerHTML = '';
|
||||
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: titulo })); const x = el('button', { type: 'button', text: '✕' }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
||||
const leer = formSitio(cuerpo, s);
|
||||
const m = el('div', { class: 'msg' }); cuerpo.append(m);
|
||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); cancelar.addEventListener('click', () => dlg.close());
|
||||
const guardar = el('button', { type: 'button', text: s ? 'Guardar' : 'Crear sitio' });
|
||||
guardar.addEventListener('click', async () => {
|
||||
const d = leer();
|
||||
if (!d.nombre) { m.className = 'msg err'; m.textContent = 'Escribe el nombre.'; return; }
|
||||
guardar.disabled = true;
|
||||
try {
|
||||
if (s) { delete d.id; await api('/api/sitio?id=' + encodeURIComponent(s.id), json('PATCH', d)); dlg.close(); mensaje('Sitio guardado.', true); await cargarLista(); cargar(s.id); }
|
||||
else { const j = await api('/api/sitios', json('POST', d)); dlg.close(); mensaje('Sitio ' + j.sitio.nombre + ' creado.', true); await cargarLista(); history.replaceState(null, '', '?id=' + encodeURIComponent(j.sitio.id)); $('sel').value = j.sitio.id; cargar(j.sitio.id); }
|
||||
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
||||
guardar.disabled = false;
|
||||
});
|
||||
pie.append(cancelar, guardar);
|
||||
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
||||
}
|
||||
$('bEditar').addEventListener('click', () => ACTUAL && dialogo('Editar sitio ' + ACTUAL.nombre, ACTUAL));
|
||||
|
||||
async function cargarLista() {
|
||||
const j = await api('/api/sitios?todos=1'); SITIOS = j.sitios || []; ADMIN = !!j.admin;
|
||||
const sel = $('sel'); sel.innerHTML = ''; sel.append(el('option', { value: '', text: '— elegir sitio —' }));
|
||||
SITIOS.forEach(s => sel.append(el('option', { value: s.id, text: s.nombre + (s.activo === false ? ' (inactivo)' : '') })));
|
||||
if (ADMIN) sel.append(el('option', { value: '__nuevo__', text: '+ Nuevo sitio…' }));
|
||||
}
|
||||
$('sel').addEventListener('change', () => { const v = $('sel').value; if (v === '__nuevo__') { $('sel').value = ACTUAL ? ACTUAL.id : ''; dialogo('Nuevo sitio', null); } else if (v) { history.replaceState(null, '', '?id=' + encodeURIComponent(v)); cargar(v); } });
|
||||
(async () => {
|
||||
try { await cargarLista(); } catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo leer la lista de sitios: ' + e.message })); return; }
|
||||
const q = new URLSearchParams(location.search);
|
||||
if (q.get('nuevo') === '1') { if (ADMIN) dialogo('Nuevo sitio', null); else mensaje('Solo el administrador da de alta sitios. Pide a Leo que lo cree.', false); }
|
||||
const id = q.get('id') || (SITIOS[0] || {}).id;
|
||||
if (id) { $('sel').value = id; cargar(id); } else { $('meta').textContent = 'No hay sitios dados de alta.'; $('cont').innerHTML = '<p class="vacio">Pide a Leo que cree el primer sitio.</p>'; }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
35
docs/tableros/index.md
Normal file
35
docs/tableros/index.md
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "Tableros"
|
||||
---
|
||||
|
||||
# Tableros
|
||||
|
||||
Las páginas que abres para saber **dónde está el proyecto hoy**: qué tareas hay y de quién son, en qué estado va cada versión de tarjeta, cuántas unidades llevamos contra el plan de 570 y qué pasó con cada serie. Se alimentan solas de las actas y de los registros del servidor (`/api/…`); no hay nada que copiar a mano.
|
||||
|
||||
## ¿Qué quiero ver?
|
||||
|
||||
| Quiero… | Tablero | Qué muestra |
|
||||
|---|---|---|
|
||||
| Las **tareas** del equipo y quién las tiene | [Kanban](kanban.html) | Columnas *Por hacer · En curso · Bloqueado · Hecho*; pistas Ingeniería / Producción / Mecánica / Compras / Campo; responsable, fecha límite, ligas a documentos y actas; historial de movimientos |
|
||||
| **Dónde está el hardware** (V5.5 → V5.6 → V5.7 → v6) | [Ingeniería](ingenieria.html) | Una tarjeta por versión con su estado, pendientes con responsable y fecha, últimas decisiones y filosofía de diseño; las tareas de la pista Ingeniería |
|
||||
| **Cuántas unidades** llevamos y cuántas faltan | [Producción](produccion.html) | Plan 113 V5.6 · 327 V5.7 · 130 v6 contra las unidades reales por etapa (tarjeta → módulos → ensamble → envío → instalación); embudo por paso; bloqueos (caja plástica / cable Ethernet) |
|
||||
| Qué **tarjeta, módulo o unidad** pasó cada acta | [Estado de actas](../tablero.html) | Series V5-NN, ARD-NNN, RTK-NNNN, V4-NN; parejas integradas; unidades ensambladas. Pendiente = liga al acta |
|
||||
| Las **visitas de diagnóstico** en los puertos | [Tablero de campo](../diagnostico/tablero-campo.html) | Todas las actas de diagnóstico en campo, con fotos |
|
||||
| **Todo sobre una serie** | [Expediente de una unidad](../unidad.html) | Cada acta, foto y log de una unidad en una línea de tiempo, de la tarjeta al sitio |
|
||||
|
||||
## Cómo se usan
|
||||
|
||||
1. Las tareas se mueven arrastrando entre columnas. Cualquier usuario autenticado crea y mueve; **borrar y reabrir** es de administradores (`ACTAS_ADMINS`).
|
||||
2. Cada tarjeta del Kanban lleva **pista**, **responsable** (leo, michelle, aldair, jesús, alan, daniel, claude), **fecha límite** y ligas. Si una tarea depende de otra, escríbelo en las notas y déjala en *Bloqueado*.
|
||||
3. Los tableros de Ingeniería y Producción son **vistas** del mismo Kanban filtradas por pista, más sus bloques fijos (versiones, cantidades).
|
||||
4. Las cantidades del plan de producción (versión → objetivo → fecha) las edita un administrador en el propio tablero; los conteos reales salen de las actas.
|
||||
5. El Kanban sustituye al *Task Tracker* de Notion, que queda como histórico (sus tareas se importaron una vez).
|
||||
|
||||
## Relacionados
|
||||
|
||||
- [Detalle por tarjeta y módulo](../tablero.html) — qué serie pasó cada acta del lote actual.
|
||||
- [Inventario](../inventario/index.md) — stock, mínimos y movimientos.
|
||||
- [Sitios](../sitios/index.md) — unidades instaladas por puerto.
|
||||
- [Expediente de una unidad](../unidad.html) — todo lo registrado para una serie.
|
||||
- [Historia del proyecto](../historia/index.md) — jun → oct 2026 con ligas.
|
||||
|
||||
118
docs/tableros/ingenieria.html
Normal file
118
docs/tableros/ingenieria.html
Normal file
@ -0,0 +1,118 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Tablero de ingeniería — EinarBoard</title>
|
||||
<link rel="stylesheet" href="tablero.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<div><h1>Tablero de ingeniería — ¿dónde está el hardware?</h1><div class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6 — estado al 13-oct-2026</div></div>
|
||||
<nav>
|
||||
<a href="../">← Documentación</a>
|
||||
<a href="kanban.html">Pendientes</a>
|
||||
<a class="activo" href="ingenieria.html">Ingeniería</a>
|
||||
<a href="produccion.html">Producción</a>
|
||||
<a href="../hardware/">Tarjetas y hardware</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<details class="como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>Arriba, una tarjeta por versión de EinarBoard: estado, siguiente paso, responsable y sus documentos. Es texto fijo: cuando cambie una decisión, pide a Leo o a Claude que lo actualice aquí.</li>
|
||||
<li>En medio, los <b>pendientes de la pista Ingeniería</b>: son los mismos del <a href="kanban.html">tablero de pendientes</a>, filtrados. Puedes moverlos desde aquí.</li>
|
||||
<li>Abajo, las últimas decisiones con fecha y la filosofía de diseño. Antes de proponer un cambio, revisa que no contradiga una decisión ya tomada.</li>
|
||||
<li>Para una tarjeta nueva o un cambio de pedido, primero el pendiente, luego el trabajo: así queda el rastro de quién y cuándo.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div class="versiones" id="versiones"></div>
|
||||
|
||||
<section class="bloque">
|
||||
<h2>Pendientes de ingeniería</h2>
|
||||
<p class="sub">Pista Ingeniería del tablero. Arrastra o usa «Mover a…»; el historial anota quién lo hizo. <a href="kanban.html">Ver todas las pistas →</a></p>
|
||||
<div id="kb"></div>
|
||||
</section>
|
||||
|
||||
<section class="bloque">
|
||||
<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>13-oct</b> — Pedido de <b>570 HBOX</b>: 113 × V5.6 (con SD-15B), 327 × V5.7, 130 × v6; todos los reemplazos futuros en v6. Paquetes de fabricación V5.6 / V5.7 / v6 a <b>Aldair</b> (fabricación/ensamble, rol por confirmar). Caja plástica HBOX: el cable Ethernet no cabe — cambio por definir (pista Mecánica).</li>
|
||||
<li><b>13-oct</b> — Auditoría del layout V5.6 de Michelle (d71098d): <b>NO LISTA</b> — /BAT_PLUS por In1 (bloqueante), MAIN5V/SYS5V/SW3 por capas internas; las 18 piezas movidas se quedan (opción A). Esquemático V5.7 aplicado con ERC 0 (F2 SMD 3 A, Q10/D10 reusados, fuera U2 y P2, JP1 puenteado). Reparto: <b>Leo</b> coloca la fuente en V5.7; <b>Michelle</b> corrige V5.6 y después toma V5.7. <a href="../hardware/v56-auditoria-layout-13oct/">Auditoría</a> · <a href="../hardware/v56-layout-pasada-2/">Pasada 2</a></li>
|
||||
<li><b>12-oct</b> — <b>V5.7 = V5.6 + fuente conmutada 24 V → 5 V a bordo</b> (U25 TPS54360B, bloque de la v6), sin SD-15B externa. Proyecto KiCad separado <code>hbox-watchdog-v57</code>; V5.6 y V5.7 conviven. <a href="../hardware/v57-eco-fuente/">ECO V5.7</a></li>
|
||||
<li><b>09-oct</b> — <b>P12 (sonda NTC) eliminada</b> de V5.6 con R42/C24: redundante y se necesita el espacio. Sin lectura de temperatura de celda en batería (aceptado). Aplica a P7/P8 de la v6.</li>
|
||||
<li><b>08-oct</b> — Acelerómetro <b>U26 SC7A20TR</b> (I²C 0x18, INT1 → PB9) añadido a V5.6. <b>Regla DFM LTC4040 (U6)</b>: pasta del EP ≤ 50 %, vías tapadas (epoxy filled & capped) o fuera de las ventanas de pasta, X-ray/AOI en U6, lupa en cada tarjeta antes de energizar. Vale para toda revisión, incluida la v6.</li>
|
||||
<li><b>05-oct</b> — Revisión del bus I²C V5.6 / v6 (direcciones, pull-ups, TMP102 U18 en 0x49).</li>
|
||||
<li><b>30-sep → 1-oct</b> — V5.6: revisión de modo batería y layout (U4 junto a L6, DzG1 corregido); estado hacia producción.</li>
|
||||
<li><b>28-sep</b> — v6 auditada con las lecciones de V5.5 y plan de corrección. <b>«No habrá v6.1»</b>: lo que falte se corrige antes de pedir. <a href="../hardware/v6-plan-correccion/">Plan de corrección</a> · <a href="../hardware/v6-proximos-pasos/">Próximos pasos</a></li>
|
||||
<li><b>27-sep</b> — Requisito V5.6: ciclo de 10 min en batería y mejor manejo térmico. ECO V5.6: LFP de alta temperatura, JP2 2-3, R55 5.6 k → flotación 3.50 V, RS1 10 mΩ, TMP102 U18 0x49. Regla de campo V5.5/V4 con 0.6.0: respaldo continuo en batería, nunca ciclos de etapa en batería. <a href="../hardware/v56-eco/">ECO V5.6</a></li>
|
||||
<li><b>26-sep</b> — Decisión V5.6: los 500 ArduSimple ya pedidos se quedan (arquitectura con zócalos); nada de rework en tarjetas existentes; buck del módem AP64350 desde SYS5V, MODEM_EN/RST/LED al MCU, lista EMC. Hallazgo: el módem no arrancaba en batería porque el MP1584 (4.5 V mín.) caía en UVLO sobre el riel de 4.6–4.79 V del boost.</li>
|
||||
<li><b>19/21-sep</b> — Un solo firmware por tarjeta; la versión del firmware no indica la tarjeta: V3 → <code>blackpillrtkcode_v4</code>, V4 → <code>hbox-tarjetaV4_x.y.z.bin</code>, V5 → <code>hbox-v5-bringup_x.y.z.bin</code>. V5.6 necesita 0.7.0 (<code>board_v56.h</code>).</li>
|
||||
<li><b>22-ago</b> — V5.5 ordenada, 15 pzas (JLC). Auditoría EMC: /SW_BAT 0.3 mm y /BAT_PLUS 0.3–0.5 mm = críticos para la siguiente corrida (origen de la regla «nada de potencia por capas internas»).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="bloque">
|
||||
<h2>Filosofía de diseño</h2>
|
||||
<p class="sub">Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en <a href="../hardware/">Tarjetas y hardware</a>.</p>
|
||||
<ul class="filosofia">
|
||||
<li><b>Primero la seguridad en campo.</b> La unidad vive en un puerto a más de 45 °C y en batería: respaldo continuo, nunca ciclos de etapa en batería; se mide antes de energizar; una tarjeta con marca de daño no entra a un ensamble.</li>
|
||||
<li><b>Una etapa probada vale más que una nueva.</b> Los zócalos para ArduSimple y módem se quedan (500 ya pedidos); nada de rework en tarjetas existentes; la fuente a bordo de V5.7 reutiliza el bloque ya auditado de la v6.</li>
|
||||
<li><b>Redundancia solo cuando un modo de falla la justifica.</b> Por eso salió P12 (NTC redundante, estorbaba) y entró el acelerómetro U26 (detecta golpes y movimiento, que sí nos han fallado).</li>
|
||||
<li><b>Las reglas de fabricación son órdenes permanentes.</b> LTC4040: pasta ≤ 50 %, vías tapadas, X-ray; ninguna red de potencia por capas internas (lección de V5.5); lupa en U6 antes de energizar. Se aplican a toda revisión, sin excepciones por prisa.</li>
|
||||
<li><b>Cambios con prueba y rastro.</b> Esquemático por etiquetas y ediciones con script verificadas contra la netlist (ERC 0, paridad 100 %); git en dos pasos (submódulo KiCad y luego Drogheda); una tarjeta por proyecto KiCad (V5.7 separado); «no habrá v6.1»: se corrige antes de pedir, no después.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="kanban.js"></script>
|
||||
<script>
|
||||
const VERSIONES = [
|
||||
{ v: 'V5.5', cls: 'v55', badge: 'badge-v55', estado: 'En campo — 15 pzas (lote 22-ago), fw 0.6.x',
|
||||
siguiente: 'Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a V5.6. Inspeccionar U6 con lupa en cada tarjeta.',
|
||||
quien: 'Jesús (pruebas) · Alan y Daniel (campo)',
|
||||
docs: [['Manual V5', '../manuales/manual-tarjeta-V5/'], ['Lecciones V5.5 → v6', '../hardware/v6-revision-lecciones-v55/'], ['Acta de tarjeta V5', '../formularios/v5.html']] },
|
||||
{ v: 'V5.6', cls: 'v56', badge: 'badge-v56', estado: 'Layout en pasada 2 — NO LISTA para pedir (BAT_PLUS por In1)',
|
||||
siguiente: 'Michelle corrige la pasada 2 → Claude revisa pad a pad y genera Gerber/BOM/CPL → paquete a Aldair. Pedido: 113 (con SD-15B). Firmware 0.7.0.',
|
||||
quien: 'Michelle (layout) · Claude (auditoría) · Leo (pedido)',
|
||||
docs: [['▶ Pasada 2 — correcciones', '../hardware/v56-layout-pasada-2/'], ['Auditoría 13-oct (inglés)', '../hardware/v56-auditoria-layout-13oct/'], ['ECO V5.6', '../hardware/v56-eco/'], ['Hoja de datos', '../hardware/v56-datasheet/'], ['Pinout', '../hardware/v56-pinout/']] },
|
||||
{ v: 'V5.7', cls: 'v57', badge: 'badge-v57', estado: 'Esquemático aplicado (ERC 0); PCB retomado de V5.6 d71098d',
|
||||
siguiente: 'Leo coloca y rutea la fuente a bordo (U25, F2) en la zona acordada → Michelle replica las correcciones de V5.6 y cierra → auditoría → pedido 327.',
|
||||
quien: 'Leo (fuente) · Michelle (cierre) · Claude (auditoría)',
|
||||
docs: [['▶ Guía de layout V5.7', '../hardware/v57-layout-guia/'], ['ECO V5.7 — fuente a bordo', '../hardware/v57-eco-fuente/']] },
|
||||
{ v: 'v6', cls: 'v6', badge: 'badge-v6', estado: 'Auditada (28-sep) con plan de corrección — no pedir todavía',
|
||||
siguiente: 'Cierre para pedido según «Próximos pasos»: aplicar a P7/P8 la decisión de P12, regla DFM U6, plan de cobre EMC. Pedido 130 + todos los reemplazos. No habrá v6.1.',
|
||||
quien: 'Leo · Claude',
|
||||
docs: [['▶ Próximos pasos para ordenar', '../hardware/v6-proximos-pasos/'], ['Plan de corrección', '../hardware/v6-plan-correccion/'], ['Manual v6', '../manuales/manual-tarjeta-v6/'], ['Plan de cobre', '../hardware/EinarBoard-v6_Plan-de-Cobre_ES.html']] },
|
||||
];
|
||||
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
||||
const cont = document.getElementById('versiones');
|
||||
const contadores = {};
|
||||
VERSIONES.forEach(x => {
|
||||
const c = el('div', { class: 'ver ' + x.cls });
|
||||
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.v }), el('span', { class: x.badge, text: x.v })));
|
||||
c.append(el('div', { class: 'est', text: x.estado }));
|
||||
c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' + x.siguiente }));
|
||||
c.append(el('div', { class: 'quien', text: '👤 ' + x.quien }));
|
||||
const d = el('div', { class: 'docs' }); x.docs.forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
|
||||
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.v] = cnt; c.append(cnt);
|
||||
cont.append(c);
|
||||
});
|
||||
Kanban.montar(document.getElementById('kb'), {
|
||||
pista: 'ingenieria', filtros: true,
|
||||
onCargado: tareas => {
|
||||
VERSIONES.forEach(x => {
|
||||
const mios = tareas.filter(t => t.version_tarjeta === x.v && t.estado !== 'hecho');
|
||||
const bloq = mios.filter(t => t.estado === 'bloqueado').length, venc = mios.filter(t => t.vencida).length;
|
||||
const a = el('a', { href: 'kanban.html?pista=ingenieria', text: mios.length + ' pendiente(s) abierto(s)' + (bloq ? ' · ' + bloq + ' bloqueado(s)' : '') + (venc ? ' · ' + venc + ' vencido(s)' : '') });
|
||||
contadores[x.v].innerHTML = ''; contadores[x.v].append(a);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
53
docs/tableros/kanban.html
Normal file
53
docs/tableros/kanban.html
Normal file
@ -0,0 +1,53 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Tablero de pendientes (kanban) — HBOX</title>
|
||||
<link rel="stylesheet" href="tablero.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<div><h1>Tablero de pendientes</h1><div class="meta" id="meta">Ingeniería · Producción · Mecánica · Compras · Campo</div></div>
|
||||
<nav>
|
||||
<a href="../">← Documentación</a>
|
||||
<a class="activo" href="kanban.html">Pendientes</a>
|
||||
<a href="ingenieria.html">Ingeniería</a>
|
||||
<a href="produccion.html">Producción</a>
|
||||
<a href="../inventario/inventario.html">Inventario</a>
|
||||
<a href="../sitios/">Sitios</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<details class="como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>Cada tarjeta del tablero es un <b>pendiente</b> (no confundir con la tarjeta EinarBoard). Léelo antes de empezar el trabajo.</li>
|
||||
<li>Arrastra el pendiente a la columna que corresponda; en el teléfono usa <b>Mover a…</b>. El tablero anota quién lo movió y cuándo.</li>
|
||||
<li>Toca el título para ver el detalle: descripción, enlaces, de qué depende y su historial. Ahí mismo edita y guarda.</li>
|
||||
<li><b>Nuevo pendiente</b>: título corto, pista, responsable y fecha límite. Si depende de otro, márcalo en «Bloqueado por».</li>
|
||||
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.</li>
|
||||
<li>Al terminar, muévelo a <b>Hecho</b>. No lo borres: solo el administrador borra, y el historial sirve de bitácora.</li>
|
||||
<li>Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div id="kb"></div>
|
||||
</div>
|
||||
<script src="kanban.js"></script>
|
||||
<script>
|
||||
Kanban.montar(document.getElementById('kb'), {
|
||||
filtros: true,
|
||||
onCargado: (tareas, meta) => {
|
||||
const abiertas = tareas.filter(t => t.estado !== 'hecho').length;
|
||||
const venc = tareas.filter(t => t.vencida).length;
|
||||
const bloq = tareas.filter(t => t.estado === 'bloqueado').length;
|
||||
document.getElementById('meta').textContent = abiertas + ' abiertos · ' + bloq + ' bloqueados · ' + venc + ' vencidos'
|
||||
+ ' · actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' })
|
||||
+ (meta.user ? ' · ' + meta.user + (meta.admin ? ' (admin)' : '') : '');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
284
docs/tableros/kanban.js
Normal file
284
docs/tableros/kanban.js
Normal file
@ -0,0 +1,284 @@
|
||||
/* Tablero kanban HBOX — módulo compartido (kanban.html, ingenieria.html, produccion.html).
|
||||
Vocabulario: la tarjeta del tablero se llama "pendiente" en pantalla para no confundirla con la
|
||||
tarjeta EinarBoard. Columnas fijas: Por hacer · En curso · Bloqueado · Hecho.
|
||||
|
||||
Uso: const kb = Kanban.montar(document.getElementById('kb'), {
|
||||
pista: 'ingenieria', // opcional: fija la pista (oculta su filtro y la precarga en "Nuevo")
|
||||
filtros: true, // barra de filtros (responsable / versión / buscar / pista)
|
||||
compacto: false, // tarjetas más pequeñas (tableros que solo muestran un extracto)
|
||||
onCargado: (tareas, meta) => {} // callback después de cada carga
|
||||
});
|
||||
kb.recargar();
|
||||
|
||||
Servidor: GET /api/tareas · POST /api/tareas · PATCH /api/tarea?id= · POST /api/tareas/orden ·
|
||||
DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial. */
|
||||
(function () {
|
||||
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo' };
|
||||
const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']];
|
||||
const VERSIONES = ['—', 'V5.5', 'V5.6', 'V5.7', 'v6'];
|
||||
const RESPONSABLES = ['leo', 'michelle', 'aldair', 'jesús', 'alan', 'daniel', 'claude'];
|
||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||
|
||||
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
||||
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };
|
||||
const hoyISO = () => { const d = new Date(); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); };
|
||||
const fmt = iso => (iso || '').replace('T', ' ').slice(0, 16);
|
||||
const fechaCorta = f => { if (!f) return ''; const [y, m, d] = f.split('-'); return d + '-' + ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'][+m - 1] + (y !== hoyISO().slice(0, 4) ? '-' + y.slice(2) : ''); };
|
||||
|
||||
async function api(url, opts) {
|
||||
const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {}));
|
||||
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;
|
||||
}
|
||||
const json = (method, body) => ({ method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
|
||||
function montar(root, opts) {
|
||||
opts = opts || {};
|
||||
let tareas = [], admin = false, user = '', porId = {};
|
||||
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes' };
|
||||
root.classList.add('kanban-root');
|
||||
|
||||
// ---------- barra de filtros ----------
|
||||
const barra = el('div', { class: 'kb-barra' });
|
||||
const selPista = el('select', { title: 'Pista' }); selPista.append(el('option', { value: '', text: 'Todas las pistas' }));
|
||||
Object.entries(PISTAS).forEach(([k, v]) => selPista.append(el('option', { value: k, text: v })));
|
||||
const selResp = el('select', { title: 'Responsable' }); selResp.append(el('option', { value: '', text: 'Todos los responsables' }));
|
||||
const selVer = el('select', { title: 'Versión de tarjeta' }); selVer.append(el('option', { value: '', text: 'Todas las versiones' }));
|
||||
VERSIONES.filter(v => v !== '—').forEach(v => selVer.append(el('option', { value: v, text: v })));
|
||||
const selHechas = el('select', { title: 'Hechas' });
|
||||
[['recientes', 'Hechas: últimos 14 días'], ['todas', 'Hechas: todas'], ['ninguna', 'Hechas: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
|
||||
const buscar = el('input', { type: 'search', placeholder: 'Buscar…', style: 'min-width:140px;flex:1' });
|
||||
const btnNuevo = el('button', { text: '+ Nuevo pendiente' });
|
||||
const cnt = el('span', { class: 'cnt' });
|
||||
if (opts.filtros !== false) {
|
||||
if (!opts.pista) barra.append(selPista);
|
||||
barra.append(selResp, selVer, selHechas, buscar, btnNuevo, cnt);
|
||||
root.append(barra);
|
||||
}
|
||||
const aviso = el('div', { class: 'msg' }); root.append(aviso);
|
||||
function mensaje(txt, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = txt; clearTimeout(aviso._t); aviso._t = setTimeout(() => { aviso.className = 'msg'; }, bien ? 3500 : 8000); }
|
||||
|
||||
// ---------- columnas ----------
|
||||
const kb = el('div', { class: 'kb' + (opts.compacto ? ' compacto' : '') });
|
||||
if (!('draggable' in document.createElement('div')) || matchMedia('(pointer:coarse)').matches) kb.classList.add('movil');
|
||||
const cols = {};
|
||||
ESTADOS.forEach(([k, nombre]) => {
|
||||
const c = el('div', { class: 'col e-' + k, 'data-estado': k });
|
||||
c.append(el('h3', null, el('span', { text: nombre }), el('span', { class: 'n', text: '0' })));
|
||||
c.addEventListener('dragover', e => { e.preventDefault(); c.classList.add('sobre'); });
|
||||
c.addEventListener('dragleave', () => c.classList.remove('sobre'));
|
||||
c.addEventListener('drop', e => {
|
||||
e.preventDefault(); c.classList.remove('sobre');
|
||||
const id = e.dataTransfer.getData('text/plain'); if (!id) return;
|
||||
// posición: antes de la primera tarjeta cuyo centro queda debajo del cursor
|
||||
const cards = [...c.querySelectorAll('.card')].filter(x => x.dataset.id !== id);
|
||||
let idx = cards.length;
|
||||
for (let i = 0; i < cards.length; i++) { const r = cards[i].getBoundingClientRect(); if (e.clientY < r.top + r.height / 2) { idx = i; break; } }
|
||||
mover(id, k, idx);
|
||||
});
|
||||
cols[k] = c; kb.append(c);
|
||||
});
|
||||
root.append(kb);
|
||||
|
||||
// ---------- filtrado y pintado ----------
|
||||
function visibles() {
|
||||
const q = filtro.buscar.trim().toLowerCase();
|
||||
const corte = new Date(Date.now() - 14 * 86400000).toISOString().slice(0, 10);
|
||||
return tareas.filter(t => {
|
||||
if (filtro.pista && t.pista !== filtro.pista) return false;
|
||||
if (filtro.responsable && !String(t.responsable || '').toLowerCase().includes(filtro.responsable)) return false;
|
||||
if (filtro.version && t.version_tarjeta !== filtro.version) return false;
|
||||
if (t.estado === 'hecho') {
|
||||
if (filtro.hechas === 'ninguna') return false;
|
||||
if (filtro.hechas === 'recientes' && (t.modificado_en || '').slice(0, 10) < corte) return false;
|
||||
}
|
||||
if (q) { const txt = [t.titulo, t.descripcion, t.responsable, (t.etiquetas || []).join(' '), t.id].join(' ').toLowerCase(); if (!txt.includes(q)) return false; }
|
||||
return true;
|
||||
});
|
||||
}
|
||||
function bloqueosAbiertos(t) { return (t.bloqueada_por || []).map(id => porId[id]).filter(b => b && b.estado !== 'hecho'); }
|
||||
|
||||
function tarjeta(t) {
|
||||
const c = el('div', { class: 'card p-' + t.pista + (t.vencida ? ' vencida' : ''), draggable: 'true', 'data-id': t.id });
|
||||
c.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', t.id); e.dataTransfer.effectAllowed = 'move'; c.classList.add('arrastrando'); });
|
||||
c.addEventListener('dragend', () => c.classList.remove('arrastrando'));
|
||||
const tit = el('div', { class: 'tit', text: t.titulo }); tit.addEventListener('click', () => abrir(t)); c.append(tit);
|
||||
const meta = el('div', { class: 'meta' });
|
||||
if (!opts.pista) meta.append(el('span', { class: 'chip p-' + t.pista, text: PISTAS[t.pista] || t.pista }));
|
||||
if (t.version_tarjeta && t.version_tarjeta !== '—') meta.append(el('span', { class: BADGE[t.version_tarjeta] || 'chip', text: t.version_tarjeta }));
|
||||
if (t.responsable) meta.append(el('span', { text: '👤 ' + t.responsable }));
|
||||
if (t.fecha_limite) meta.append(el('span', { class: 'fecha' + (t.vencida ? ' mal' : ''), text: (t.vencida ? '⚠ venció ' : '📅 ') + fechaCorta(t.fecha_limite) }));
|
||||
(t.etiquetas || []).slice(0, 3).forEach(x => meta.append(el('span', { class: 'chip', text: x })));
|
||||
c.append(meta);
|
||||
const bl = bloqueosAbiertos(t);
|
||||
if (bl.length) c.append(el('div', { class: 'bloq', text: '⛓ espera: ' + bl.map(b => b.titulo.length > 38 ? b.titulo.slice(0, 37) + '…' : b.titulo).join(' · ') }));
|
||||
if ((t.enlaces || []).length) {
|
||||
const e = el('div', { class: 'enl' });
|
||||
t.enlaces.slice(0, 3).forEach(x => e.append(el('a', { href: x.url, text: '🔗 ' + (x.titulo || x.url) })));
|
||||
c.append(e);
|
||||
}
|
||||
// "Mover a…": teléfonos y quien no arrastra
|
||||
const mv = el('div', { class: 'mover' });
|
||||
const sel = el('select');
|
||||
sel.append(el('option', { value: '', text: 'Mover a…' }));
|
||||
ESTADOS.filter(([k]) => k !== t.estado).forEach(([k, n]) => sel.append(el('option', { value: k, text: n })));
|
||||
sel.addEventListener('change', () => { if (sel.value) mover(t.id, sel.value, 0); });
|
||||
mv.append(sel); c.append(mv);
|
||||
return c;
|
||||
}
|
||||
|
||||
function pintar() {
|
||||
porId = {}; tareas.forEach(t => porId[t.id] = t);
|
||||
const vis = visibles();
|
||||
ESTADOS.forEach(([k]) => {
|
||||
const c = cols[k]; c.querySelectorAll('.card').forEach(x => x.remove());
|
||||
const lst = vis.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
|
||||
c.querySelector('.n').textContent = lst.length;
|
||||
lst.forEach(t => c.append(tarjeta(t)));
|
||||
});
|
||||
// responsables presentes en los datos (más los conocidos)
|
||||
selResp.querySelectorAll('option:not(:first-child)').forEach(o => o.remove());
|
||||
const set = new Set(RESPONSABLES);
|
||||
tareas.forEach(t => String(t.responsable || '').split(/[,/]+/).map(s => s.trim().toLowerCase()).filter(Boolean).forEach(s => set.add(s)));
|
||||
[...set].sort().forEach(r => selResp.append(el('option', { value: r, text: r })));
|
||||
selResp.value = filtro.responsable;
|
||||
const venc = vis.filter(t => t.vencida).length;
|
||||
cnt.textContent = vis.length + ' pendiente(s)' + (venc ? ' · ' + venc + ' vencido(s)' : '') + (vis.length !== tareas.length ? ' · de ' + tareas.length : '');
|
||||
}
|
||||
|
||||
async function recargar() {
|
||||
try {
|
||||
const j = await api('/api/tareas');
|
||||
tareas = j.tareas || []; admin = !!j.admin; user = j.user || '';
|
||||
pintar();
|
||||
if (opts.onCargado) opts.onCargado(tareas, { admin, user });
|
||||
} catch (e) { mensaje('No se pudo cargar el tablero: ' + e.message, false); }
|
||||
}
|
||||
|
||||
// ---------- mover (arrastre o "Mover a…") ----------
|
||||
async function mover(id, estado, idx) {
|
||||
const t = porId[id]; if (!t) return;
|
||||
const mismaCol = t.estado === estado;
|
||||
const columna = tareas.filter(x => x.estado === estado && x.id !== id).sort((a, b) => a.orden - b.orden);
|
||||
columna.splice(Math.max(0, Math.min(idx, columna.length)), 0, t);
|
||||
const ids = columna.map(x => x.id);
|
||||
// pintado optimista
|
||||
const prev = { estado: t.estado, orden: t.orden };
|
||||
t.estado = estado; columna.forEach((x, i) => x.orden = i); pintar();
|
||||
try {
|
||||
if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', { estado, orden: ids.indexOf(id) }));
|
||||
await api('/api/tareas/orden', json('POST', { estado, ids }));
|
||||
if (!mismaCol) mensaje('«' + t.titulo.slice(0, 40) + (t.titulo.length > 40 ? '…' : '') + '» → ' + ESTADOS.find(e => e[0] === estado)[1], true);
|
||||
await recargar();
|
||||
} catch (e) { t.estado = prev.estado; t.orden = prev.orden; pintar(); mensaje('No se pudo mover: ' + e.message, false); }
|
||||
}
|
||||
|
||||
// ---------- diálogo: alta y detalle ----------
|
||||
const dlg = el('dialog');
|
||||
document.body.append(dlg);
|
||||
function campo(lbl, input) { const d = el('div'); d.append(el('label', { text: lbl }), input); return d; }
|
||||
function abrir(t) {
|
||||
const nuevo = !t;
|
||||
t = t || { titulo: '', descripcion: '', pista: opts.pista || 'produccion', estado: 'por-hacer', responsable: '', fecha_limite: null, enlaces: [], bloqueada_por: [], etiquetas: [], version_tarjeta: '—' };
|
||||
dlg.innerHTML = '';
|
||||
const cab = el('div', { class: 'dcab' });
|
||||
cab.append(el('h2', { text: nuevo ? 'Nuevo pendiente' : 'Pendiente ' + t.id }));
|
||||
const cerrar = el('button', { type: 'button', text: '✕', title: 'Cerrar' }); cerrar.addEventListener('click', () => dlg.close()); cab.append(cerrar);
|
||||
dlg.append(cab);
|
||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
||||
|
||||
const fTit = el('input', { type: 'text', value: t.titulo, maxlength: '200', placeholder: 'Qué hay que hacer, en una línea' });
|
||||
const fDesc = el('textarea', { rows: '3', placeholder: 'Detalle, criterio de terminado, notas' }); fDesc.value = t.descripcion || '';
|
||||
const fPista = el('select'); Object.entries(PISTAS).forEach(([k, v]) => fPista.append(el('option', { value: k, text: v }))); fPista.value = t.pista;
|
||||
const fEst = el('select'); ESTADOS.forEach(([k, v]) => fEst.append(el('option', { value: k, text: v }))); fEst.value = t.estado;
|
||||
const fResp = el('input', { type: 'text', value: t.responsable || '', list: 'kb-resp', placeholder: 'leo, michelle, aldair, jesús…' });
|
||||
let dl = document.getElementById('kb-resp'); if (!dl) { dl = el('datalist', { id: 'kb-resp' }); document.body.append(dl); }
|
||||
dl.innerHTML = ''; RESPONSABLES.forEach(r => dl.append(el('option', { value: r })));
|
||||
const fFecha = el('input', { type: 'date', value: t.fecha_limite || '' });
|
||||
const fVer = el('select'); VERSIONES.forEach(v => fVer.append(el('option', { value: v, text: v === '—' ? '— (no aplica)' : v }))); fVer.value = t.version_tarjeta || '—';
|
||||
const fEtq = el('input', { type: 'text', value: (t.etiquetas || []).join(', '), placeholder: 'layout, pedido 570, bloqueante' });
|
||||
|
||||
cuerpo.append(campo('Título', fTit), campo('Descripción', fDesc));
|
||||
const g1 = el('div', { class: 'dos-col' }); g1.append(campo('Pista', fPista), campo('Estado', fEst), campo('Responsable', fResp), campo('Fecha límite', fFecha), campo('Versión de tarjeta', fVer), campo('Etiquetas (separadas por coma)', fEtq)); cuerpo.append(g1);
|
||||
|
||||
// enlaces
|
||||
const enlBox = el('div', { class: 'enlaces-ed' });
|
||||
function filaEnlace(e) {
|
||||
const f = el('div', { class: 'fila' });
|
||||
const a = el('input', { type: 'text', value: e.titulo || '', placeholder: 'Título' });
|
||||
const b = el('input', { type: 'text', value: e.url || '', placeholder: '/hardware/… o https://…' });
|
||||
const q = el('button', { type: 'button', class: 'sec', text: '✕' }); q.addEventListener('click', () => f.remove());
|
||||
f.append(a, b, q); enlBox.append(f);
|
||||
}
|
||||
(t.enlaces || []).forEach(filaEnlace);
|
||||
const addEnl = el('button', { type: 'button', class: 'sec', text: '+ enlace', style: 'padding:6px 10px;font-size:13px' }); addEnl.addEventListener('click', () => filaEnlace({}));
|
||||
cuerpo.append(campo('Enlaces (documentos, actas, expediente de unidad)', enlBox), addEnl);
|
||||
|
||||
// bloqueada por (otros pendientes abiertos)
|
||||
const lst = el('div', { class: 'lista-chk' });
|
||||
const otros = tareas.filter(x => x.id !== t.id && (x.estado !== 'hecho' || (t.bloqueada_por || []).includes(x.id)));
|
||||
if (!otros.length) lst.append(el('span', { class: 'vacio', text: 'No hay otros pendientes.' }));
|
||||
otros.forEach(x => { const l = el('label'); const ck = el('input', { type: 'checkbox', value: x.id }); ck.checked = (t.bloqueada_por || []).includes(x.id); l.append(ck, document.createTextNode(x.titulo + ' (' + (PISTAS[x.pista] || x.pista) + ')')); lst.append(l); });
|
||||
cuerpo.append(campo('Bloqueado por (espera a que terminen)', lst));
|
||||
|
||||
if (!nuevo) {
|
||||
const h = el('div', { class: 'hist' });
|
||||
h.append(el('div', { html: '<b>Creado</b> por ' + esc(t.creado_por) + ' el ' + esc(fmt(t.creado_en)) + ' · última modificación ' + esc(t.modificado_por) + ' el ' + esc(fmt(t.modificado_en)) }));
|
||||
(t.historial || []).slice().reverse().forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
||||
cuerpo.append(h);
|
||||
}
|
||||
const m = el('div', { class: 'msg' }); cuerpo.append(m);
|
||||
|
||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||
if (!nuevo && admin) {
|
||||
const del = el('button', { type: 'button', class: 'peligro izq', text: 'Borrar' });
|
||||
del.addEventListener('click', async () => {
|
||||
if (!confirm('¿Borrar el pendiente «' + t.titulo + '»? No se puede deshacer. Si ya no aplica, mejor muévelo a Hecho.')) return;
|
||||
try { await api('/api/tarea?id=' + encodeURIComponent(t.id), { method: 'DELETE' }); dlg.close(); mensaje('Pendiente borrado.', true); recargar(); }
|
||||
catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
||||
});
|
||||
pie.append(del);
|
||||
} else if (!nuevo) pie.append(el('span', { class: 'izq vacio', text: 'Solo el administrador borra; si ya no aplica, muévelo a Hecho.' }));
|
||||
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); cancelar.addEventListener('click', () => dlg.close());
|
||||
const guardar = el('button', { type: 'button', text: nuevo ? 'Crear pendiente' : 'Guardar cambios' });
|
||||
guardar.addEventListener('click', async () => {
|
||||
const d = { titulo: fTit.value.trim(), descripcion: fDesc.value.trim(), pista: fPista.value, estado: fEst.value, responsable: fResp.value.trim(),
|
||||
fecha_limite: fFecha.value || null, version_tarjeta: fVer.value, etiquetas: fEtq.value.split(',').map(s => s.trim()).filter(Boolean),
|
||||
enlaces: [...enlBox.querySelectorAll('.fila')].map(f => ({ titulo: f.children[0].value.trim(), url: f.children[1].value.trim() })).filter(e => e.url),
|
||||
bloqueada_por: [...lst.querySelectorAll('input:checked')].map(c => c.value) };
|
||||
if (!d.titulo) { m.className = 'msg err'; m.textContent = 'Escribe el título.'; return; }
|
||||
guardar.disabled = true;
|
||||
try {
|
||||
if (nuevo) await api('/api/tareas', json('POST', d));
|
||||
else {
|
||||
const cambios = {}; Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(t[k] == null ? (Array.isArray(d[k]) ? [] : (k === 'fecha_limite' ? null : '')) : t[k])) cambios[k] = d[k]; });
|
||||
if (Object.keys(cambios).length) await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', cambios));
|
||||
}
|
||||
dlg.close(); mensaje(nuevo ? 'Pendiente creado.' : 'Cambios guardados.', true); recargar();
|
||||
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
||||
guardar.disabled = false;
|
||||
});
|
||||
pie.append(cancelar, guardar);
|
||||
if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', '');
|
||||
setTimeout(() => fTit.focus(), 50);
|
||||
}
|
||||
|
||||
// ---------- eventos de filtros ----------
|
||||
selPista.addEventListener('change', () => { filtro.pista = selPista.value; pintar(); });
|
||||
selResp.addEventListener('change', () => { filtro.responsable = selResp.value; pintar(); });
|
||||
selVer.addEventListener('change', () => { filtro.version = selVer.value; pintar(); });
|
||||
selHechas.addEventListener('change', () => { filtro.hechas = selHechas.value; pintar(); });
|
||||
buscar.addEventListener('input', () => { filtro.buscar = buscar.value; pintar(); });
|
||||
btnNuevo.addEventListener('click', () => abrir(null));
|
||||
const q = new URLSearchParams(location.search);
|
||||
if (q.get('pista') && !opts.pista) { selPista.value = q.get('pista'); filtro.pista = q.get('pista'); }
|
||||
if (q.get('responsable')) { filtro.responsable = q.get('responsable').toLowerCase(); }
|
||||
if (q.get('id')) setTimeout(() => { const t = porId[q.get('id')]; if (t) abrir(t); }, 600);
|
||||
|
||||
recargar();
|
||||
return { recargar, abrir, get tareas() { return tareas; }, get admin() { return admin; } };
|
||||
}
|
||||
|
||||
window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES };
|
||||
})();
|
||||
234
docs/tableros/produccion.html
Normal file
234
docs/tableros/produccion.html
Normal file
@ -0,0 +1,234 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Tablero de producción — pedido 570</title>
|
||||
<link rel="stylesheet" href="tablero.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<div><h1>Tablero de producción — pedido de 570 HBOX</h1><div class="meta" id="meta">Cargando…</div></div>
|
||||
<nav>
|
||||
<a href="../">← Documentación</a>
|
||||
<a href="kanban.html">Pendientes</a>
|
||||
<a href="ingenieria.html">Ingeniería</a>
|
||||
<a class="activo" href="produccion.html">Producción</a>
|
||||
<a href="../tablero.html">Detalle por tarjeta</a>
|
||||
<a href="../inventario/inventario.html">Inventario</a>
|
||||
<button id="refrescar" style="padding:6px 12px">Actualizar</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<details class="como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>Arriba: el <b>plan</b> (cuántas unidades de cada versión y para cuándo) contra lo <b>real</b>, que sale solo de las actas enviadas. Si una unidad no tiene acta, aquí no existe.</li>
|
||||
<li>El <b>embudo</b> cuenta series distintas por etapa: tarjeta probada → módulos → integración → ensamble → prueba final → configuración → envío → instalación. Toca una etapa para ir a su acta.</li>
|
||||
<li><b>Bloqueos</b>: pendientes en Bloqueado de las pistas Producción, Mecánica y Compras. Resuélvelos en el <a href="kanban.html">tablero de pendientes</a>.</li>
|
||||
<li><b>Alertas de inventario</b>: ítems por debajo del mínimo. Registra las entradas al llegar el material.</li>
|
||||
<li>Solo el administrador edita el plan (botón «Editar plan»). Las fechas son objetivo, no promesa.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div id="cont"></div>
|
||||
</div>
|
||||
|
||||
<dialog id="dlgPlan"></dialog>
|
||||
<script>
|
||||
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
||||
const 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 esPrueba = s => String(s || '').toUpperCase().includes('PRUEBA');
|
||||
const esUnidad = s => /^HBOX/i.test(String(s || ''));
|
||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||
const FORM = { 'prueba-v5': '../formularios/v5.html', 'prueba-v4': '../formularios/v4.html', 'modulo-ard': '../formularios/ardusimple.html', 'modulo-4g': '../formularios/modem4g.html',
|
||||
'integracion': '../formularios/integracion.html', 'ensamble': '../formularios/ensamble.html', 'reflasheo': '../formularios/reflasheo.html',
|
||||
'prueba-ethernet-serie': '../formularios/ethernet-serie.html', 'envio': '../formularios/envio.html', 'instalacion-campo': '../formularios/instalacion.html' };
|
||||
const PISTAS = { produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', ingenieria: 'Ingeniería', campo: 'Campo' };
|
||||
const cont = document.getElementById('cont'), meta = document.getElementById('meta');
|
||||
let PLAN = null, ADMIN = false;
|
||||
|
||||
async function api(url, opts) {
|
||||
const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {}));
|
||||
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;
|
||||
}
|
||||
|
||||
function seriesEtapa(et, actas) {
|
||||
const s = new Set();
|
||||
actas.forEach(a => {
|
||||
if (!et.tipos.includes(a.tipo) || esPrueba(a.serie)) return;
|
||||
if (et.ok && a.resultado === 'FALLA') return;
|
||||
if (et.solo_unidades && !esUnidad(a.serie)) return;
|
||||
s.add(a.serie);
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
function render(plan, actas, series, danos, bloqueos, inv) {
|
||||
cont.innerHTML = '';
|
||||
const reales = actas.filter(a => !esPrueba(a.serie));
|
||||
const ens = reales.filter(a => a.tipo === 'ensamble' && a.resultado !== 'FALLA');
|
||||
const unidades = new Set(ens.map(a => a.serie));
|
||||
const enviadas = new Set(reales.filter(a => a.tipo === 'envio' && esUnidad(a.serie)).map(a => a.serie));
|
||||
const instaladas = new Set(reales.filter(a => a.tipo === 'instalacion-campo' && a.resultado !== 'FALLA').map(a => a.serie));
|
||||
const total = plan.versiones.reduce((n, v) => n + (v.objetivo || 0), 0);
|
||||
|
||||
// ---- KPIs
|
||||
const kp = el('div', { class: 'kpis' });
|
||||
const kpi = (n, l, cls) => kp.append(el('div', { class: 'kpi ' + (cls || '') }, el('div', { class: 'n', text: String(n) }), el('div', { class: 'l', text: l })));
|
||||
kpi(plan.pedido_total || total, 'Pedido total');
|
||||
kpi(unidades.size, 'Unidades HBOX ensambladas (acta OK)', 'bien');
|
||||
kpi(enviadas.size, 'Unidades enviadas');
|
||||
kpi(instaladas.size, 'Instaladas en sitio');
|
||||
kpi(danos.length, 'Series dañadas o de baja', danos.length ? 'mal' : '');
|
||||
kpi(bloqueos.length, 'Bloqueos abiertos', bloqueos.length ? 'mal' : '');
|
||||
kpi(inv.length, 'Alertas de inventario', inv.length ? 'mal' : '');
|
||||
cont.append(kp);
|
||||
|
||||
// ---- plan vs real por versión
|
||||
const b1 = el('section', { class: 'bloque' });
|
||||
const cab = el('div', { style: 'display:flex;justify-content:space-between;gap:10px;align-items:baseline;flex-wrap:wrap' });
|
||||
cab.append(el('h2', { text: 'Plan por versión vs. real' }));
|
||||
if (ADMIN) { const b = el('button', { class: 'sec', text: 'Editar plan', style: 'padding:6px 12px' }); b.addEventListener('click', editarPlan); cab.append(b); }
|
||||
b1.append(cab);
|
||||
b1.append(el('p', { class: 'sub', html: esc(plan.nota || '') + (plan.actualizado_en ? ' · plan actualizado por <b>' + esc(plan.actualizado_por) + '</b> el ' + esc(fmt(plan.actualizado_en)) : (plan._default ? ' · <b>plan por omisión</b>: aún no se ha guardado (siembra o «Editar plan»)' : '')) }));
|
||||
const grid = el('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px' });
|
||||
plan.versiones.forEach(v => {
|
||||
const pref = (v.serie_prefijo || '').toUpperCase();
|
||||
const hechas = pref ? ens.filter(a => String((a.componentes || {}).einar || '').toUpperCase().startsWith(pref + '-')).length : 0;
|
||||
const pct = v.objetivo ? Math.min(100, Math.round(100 * hechas / v.objetivo)) : 0;
|
||||
const c = el('div', { class: 'etapa', style: 'text-align:left' });
|
||||
c.append(el('div', { style: 'display:flex;justify-content:space-between;align-items:center' }, el('b', { text: 'EinarBoard ' + v.version }), el('span', { class: BADGE[v.version] || 'pill pend', text: v.version })));
|
||||
c.append(el('div', { class: 'n', style: 'font-size:22px;margin-top:4px', html: hechas + ' <span style="font-size:13px;color:var(--tinta2)">/ ' + v.objetivo + ' unidades</span>' }));
|
||||
const pr = el('div', { class: 'prog' }); pr.append(el('span', { style: 'width:' + pct + '%' })); c.append(pr);
|
||||
c.append(el('div', { class: 'obj', html: '<span>' + pct + ' %</span><span>' + (v.fecha ? 'objetivo ' + esc(v.fecha) : 'sin fecha objetivo') + '</span>' }));
|
||||
if (v.nota) c.append(el('div', { class: 'l', style: 'text-align:left;margin-top:5px', text: v.nota }));
|
||||
if (pref) c.append(el('div', { class: 'f', style: 'text-align:left', text: 'cuenta ensambles OK cuya tarjeta sea ' + pref + '-nn' }));
|
||||
grid.append(c);
|
||||
});
|
||||
// lote V5.5 actual (fuera del pedido)
|
||||
const v55 = ens.filter(a => /^V5-\d/i.test(String((a.componentes || {}).einar || ''))).length;
|
||||
const c55 = el('div', { class: 'etapa', style: 'text-align:left;opacity:.9' });
|
||||
c55.append(el('div', { style: 'display:flex;justify-content:space-between;align-items:center' }, el('b', { text: 'Lote actual V5.5' }), el('span', { class: 'badge-v55', text: 'V5.5' })));
|
||||
c55.append(el('div', { class: 'n', style: 'font-size:22px;margin-top:4px', html: v55 + ' <span style="font-size:13px;color:var(--tinta2)">unidades ensambladas</span>' }));
|
||||
c55.append(el('div', { class: 'l', style: 'text-align:left;margin-top:5px', text: 'Tarjetas V5-nn (15 fabricadas el 22-ago). No forman parte del pedido de 570; se cuentan aparte.' }));
|
||||
grid.append(c55);
|
||||
b1.append(grid);
|
||||
if (total !== (plan.pedido_total || total)) b1.append(el('p', { class: 'sub', style: 'margin-top:8px;color:var(--falla)', text: '⚠ La suma de objetivos (' + total + ') no coincide con el pedido total (' + plan.pedido_total + ').' }));
|
||||
cont.append(b1);
|
||||
|
||||
// ---- embudo
|
||||
const b2 = el('section', { class: 'bloque' });
|
||||
b2.append(el('h2', { text: 'Embudo de producción (series distintas con acta)' }), el('p', { class: 'sub', text: 'Cada etapa cuenta las series que ya tienen acta de ese tipo. Las tres primeras son por pieza (tarjeta, módulo, pareja); de ensamble en adelante, por unidad HBOX. Toca una etapa para abrir su acta.' }));
|
||||
const emb = el('div', { class: 'embudo' });
|
||||
(plan.etapas || []).forEach(et => {
|
||||
const s = seriesEtapa(et, reales);
|
||||
const a = el('a', { class: 'etapa' + (s.size ? '' : ' sin'), href: FORM[et.tipos[0]] || '../actas/', style: 'text-decoration:none;color:inherit' });
|
||||
a.append(el('div', { class: 'n', text: String(s.size) }), el('div', { class: 'l', text: et.nombre }));
|
||||
a.append(el('div', { class: 'f', text: et.nota || ('acta: ' + et.tipos.join(', ')) }));
|
||||
emb.append(a);
|
||||
});
|
||||
b2.append(emb);
|
||||
cont.append(b2);
|
||||
|
||||
// ---- bloqueos + inventario
|
||||
const dos = el('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px' });
|
||||
const b3 = el('section', { class: 'bloque bloqueos', style: 'margin:0' });
|
||||
b3.append(el('h2', { text: 'Bloqueos (Producción · Mecánica · Compras)' }), el('p', { class: 'sub', text: 'Pendientes en la columna Bloqueado. Quien lo destrabe, que lo mueva.' }));
|
||||
if (!bloqueos.length) b3.append(el('div', { class: 'vacio', text: 'Sin bloqueos registrados.' }));
|
||||
bloqueos.forEach(t => {
|
||||
const c = el('div', { class: 'card p-' + t.pista });
|
||||
c.append(el('div', { class: 'tit', html: '<a href="kanban.html?id=' + encodeURIComponent(t.id) + '" style="color:inherit;text-decoration:none">' + esc(t.titulo) + '</a>' }));
|
||||
c.append(el('div', { class: 'meta', html: '<span class="chip p-' + t.pista + '">' + esc(PISTAS[t.pista] || t.pista) + '</span>' + (t.responsable ? '<span>👤 ' + esc(t.responsable) + '</span>' : '<span>sin responsable</span>') + (t.fecha_limite ? '<span class="fecha' + (t.vencida ? ' mal' : '') + '">📅 ' + esc(t.fecha_limite) + '</span>' : '') }));
|
||||
if (t.descripcion) c.append(el('div', { style: 'font-size:13px;color:var(--tinta2);margin-top:4px', text: t.descripcion.length > 160 ? t.descripcion.slice(0, 159) + '…' : t.descripcion }));
|
||||
b3.append(c);
|
||||
});
|
||||
dos.append(b3);
|
||||
const b4 = el('section', { class: 'bloque', style: 'margin:0' });
|
||||
b4.append(el('h2', { text: 'Alertas de inventario' }), el('p', { class: 'sub', html: 'Ítems con stock por debajo del mínimo. <a href="../inventario/inventario.html">Abrir inventario →</a>' }));
|
||||
if (!inv.length) b4.append(el('div', { class: 'vacio', text: 'Sin alertas: todo por encima del mínimo (o sin mínimo definido).' }));
|
||||
else {
|
||||
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Código</th><th>Descripción</th><th>Stock</th><th>Mínimo</th></tr>';
|
||||
inv.forEach(i => { const tr = el('tr'); tr.innerHTML = '<td><b>' + esc(i.codigo) + '</b></td><td>' + esc(i.descripcion) + '</td><td style="color:var(--falla);font-weight:700">' + esc(i.stock) + ' ' + esc(i.unidad) + '</td><td>' + esc(i.minimo) + '</td>'; t.append(tr); });
|
||||
b4.append(el('div', { class: 'tblwrap' }, t));
|
||||
}
|
||||
dos.append(b4);
|
||||
cont.append(dos);
|
||||
|
||||
// ---- daños
|
||||
if (danos.length) {
|
||||
const b5 = el('section', { class: 'bloque' });
|
||||
b5.append(el('h2', { text: 'Series con marca de daño o baja' }), el('p', { class: 'sub', text: 'Lo último registrado en un acta de daño manda. «Reparada» levanta la marca.' }));
|
||||
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Serie</th><th>Marca</th><th>Qué pasó</th><th>Disposición</th><th>Fecha</th></tr>';
|
||||
danos.forEach(d => { const a = d.acta || {}; const tr = el('tr'); tr.innerHTML = '<td><a href="../unidad.html?serie=' + encodeURIComponent(d.serie) + '"><b>' + esc(d.serie) + '</b></a></td><td><span class="pill ' + (d.marca === 'BAJA' ? 'falla' : 'aviso') + '">' + esc(d.marca === 'BAJA' ? 'BAJA' : 'DAÑADA') + '</span></td><td>' + esc(a.clasificacion || '') + (a.resumen ? ' — ' + esc(a.resumen) : '') + '</td><td>' + esc(a.accion || '') + '</td><td>' + esc(a.fecha || fmt(a.recibido)) + '</td>'; t.append(tr); });
|
||||
b5.append(el('div', { class: 'tblwrap' }, t));
|
||||
cont.append(b5);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- edición del plan (solo admin)
|
||||
function editarPlan() {
|
||||
const dlg = document.getElementById('dlgPlan');
|
||||
dlg.innerHTML = '';
|
||||
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: 'Editar plan de producción' }));
|
||||
const x = el('button', { type: 'button', text: '✕' }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
||||
const fTot = el('input', { type: 'number', min: '0', value: PLAN.pedido_total || 0 });
|
||||
const fNota = el('input', { type: 'text', value: PLAN.nota || '', maxlength: '500' });
|
||||
const d1 = el('div', { class: 'dos-col' }); d1.append(campo('Pedido total (unidades)', fTot), campo('Nota', fNota)); cuerpo.append(d1);
|
||||
cuerpo.append(el('label', { text: 'Versiones: objetivo, nota, prefijo de serie de la tarjeta (para contar ensambles) y fecha objetivo' }));
|
||||
const filas = el('div');
|
||||
function fila(v) {
|
||||
const f = el('div', { style: 'display:grid;grid-template-columns:70px 80px 1fr 70px 140px auto;gap:6px;margin-bottom:6px;align-items:center' });
|
||||
f.append(el('input', { type: 'text', value: v.version || '', placeholder: 'V5.6', maxlength: '10' }),
|
||||
el('input', { type: 'number', min: '0', value: v.objetivo || 0 }),
|
||||
el('input', { type: 'text', value: v.nota || '', placeholder: 'nota' }),
|
||||
el('input', { type: 'text', value: v.serie_prefijo || '', placeholder: 'V56', maxlength: '10' }),
|
||||
el('input', { type: 'date', value: v.fecha || '' }));
|
||||
const q = el('button', { type: 'button', class: 'sec', text: '✕', style: 'padding:6px 9px' }); q.addEventListener('click', () => f.remove()); f.append(q);
|
||||
filas.append(f);
|
||||
}
|
||||
(PLAN.versiones || []).forEach(fila); cuerpo.append(filas);
|
||||
const add = el('button', { type: 'button', class: 'sec', text: '+ versión', style: 'padding:6px 10px;font-size:13px' }); add.addEventListener('click', () => fila({})); cuerpo.append(add);
|
||||
cuerpo.append(el('p', { class: 'sub', style: 'margin-top:10px', text: 'El embudo (etapas) se conserva como está; se edita por API si hace falta.' }));
|
||||
const m = el('div', { class: 'msg' }); cuerpo.append(m);
|
||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); cancelar.addEventListener('click', () => dlg.close());
|
||||
const guardar = el('button', { type: 'button', text: 'Guardar plan' });
|
||||
guardar.addEventListener('click', async () => {
|
||||
const versiones = [...filas.children].map(f => ({ version: f.children[0].value.trim(), objetivo: +f.children[1].value || 0, nota: f.children[2].value.trim(), serie_prefijo: f.children[3].value.trim(), fecha: f.children[4].value || null })).filter(v => v.version);
|
||||
if (!versiones.length) { m.className = 'msg err'; m.textContent = 'Deja al menos una versión.'; return; }
|
||||
guardar.disabled = true;
|
||||
try {
|
||||
await api('/api/plan', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pedido_total: +fTot.value || 0, nota: fNota.value.trim(), versiones }) });
|
||||
dlg.close(); cargar();
|
||||
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
||||
guardar.disabled = false;
|
||||
});
|
||||
pie.append(cancelar, guardar);
|
||||
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
||||
}
|
||||
function campo(l, i) { const d = el('div'); d.append(el('label', { text: l }), i); return d; }
|
||||
|
||||
async function cargar() {
|
||||
meta.textContent = 'Actualizando…';
|
||||
try {
|
||||
const [p, a, s, d, t, i] = await Promise.all([
|
||||
api('/api/plan'), api('/api/actas'), api('/api/series').catch(() => ({ series: [] })), api('/api/danos').catch(() => ({ series: [] })),
|
||||
api('/api/tareas?estado=bloqueado').catch(() => ({ tareas: [] })), api('/api/inventario?alertas=1').catch(() => ({ items: [] }))]);
|
||||
PLAN = p.plan; ADMIN = !!p.admin;
|
||||
const bloqueos = (t.tareas || []).filter(x => ['produccion', 'mecanica', 'compras'].includes(x.pista));
|
||||
render(PLAN, a.actas || [], s.series || [], d.series || [], bloqueos, i.items || []);
|
||||
meta.textContent = 'Actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }) + ' · ' + (a.total || 0) + ' actas · ' + (s.series || []).length + ' series con registro';
|
||||
} catch (e) { cont.innerHTML = ''; cont.append(el('div', { class: 'err', text: 'No se pudo cargar el tablero: ' + e.message })); meta.textContent = 'Error'; }
|
||||
}
|
||||
document.getElementById('refrescar').addEventListener('click', cargar);
|
||||
cargar();
|
||||
setInterval(cargar, 60000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
141
docs/tableros/tablero.css
Normal file
141
docs/tableros/tablero.css
Normal file
@ -0,0 +1,141 @@
|
||||
/* Tableros HBOX (kanban, ingeniería, producción) — misma paleta que tablero.html y acta.css */
|
||||
:root{
|
||||
--naranja:#e8590c; --naranja-suave:#fff1e8; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
|
||||
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
|
||||
--ok:#2f9e44; --ok-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
|
||||
--pend:#adb5bd; --pend-bg:#f4f2ee; --info:#1971c2; --info-bg:#e7f1fb;
|
||||
--aviso:#f08c00; --aviso-bg:#fff9db; --morado:#7048e8; --morado-bg:#f3f0ff;
|
||||
}
|
||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
|
||||
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
|
||||
--naranja-suave:#2e2218;--ok:#69db7c;--ok-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;
|
||||
--info:#74c0fc;--info-bg:#182734;--aviso:#ffc078;--aviso-bg:#2e2712;--morado:#b197fc;--morado-bg:#241d33;}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
||||
.wrap{max-width:1240px;margin:0 auto;padding:16px 16px 70px}
|
||||
header.top{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;
|
||||
background:var(--tinta);color:#fff;padding:14px 16px;border-radius:12px;margin-bottom:14px}
|
||||
header.top h1{margin:0;font-size:20px}
|
||||
header.top .meta{color:#d7d2c8;font-size:13px}
|
||||
header.top nav{display:flex;flex-wrap:wrap;gap:4px 14px;align-items:center}
|
||||
header.top nav a{color:#fff;font-size:13px;text-decoration:none;opacity:.85}
|
||||
header.top nav a.activo{opacity:1;font-weight:700;border-bottom:2px solid var(--naranja)}
|
||||
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
|
||||
button:disabled{opacity:.5;cursor:not-allowed}
|
||||
button.sec{background:var(--tarjeta);color:var(--tinta2);border:1px solid var(--linea)}
|
||||
button.sec:hover{border-color:var(--naranja);color:var(--naranja)}
|
||||
button.peligro{background:var(--tarjeta);color:var(--falla);border:1px solid var(--falla)}
|
||||
select,input,textarea{padding:9px 11px;border:1px solid var(--linea);border-radius:9px;font-size:15px;background:var(--tarjeta);color:var(--tinta);max-width:100%}
|
||||
input:focus,select:focus,textarea:focus{outline:2px solid var(--naranja);border-color:var(--naranja)}
|
||||
label{display:block;font-weight:600;font-size:13px;margin:10px 0 4px;color:var(--tinta2)}
|
||||
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
|
||||
section.bloque h2{margin:0 0 4px;font-size:17px}
|
||||
section.bloque .sub{color:var(--tinta2);font-size:13.5px;margin:0 0 10px}
|
||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:14px}
|
||||
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:12px 14px}
|
||||
.kpi .n{font-size:28px;font-weight:700;line-height:1}
|
||||
.kpi .l{color:var(--tinta2);font-size:12.5px;margin-top:4px}
|
||||
.kpi.mal .n{color:var(--falla)} .kpi.bien .n{color:var(--ok)}
|
||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px;margin:10px 0}
|
||||
.vacio{color:var(--tinta2);font-style:italic;font-size:14px}
|
||||
.pill{display:inline-block;font-size:11.5px;font-weight:700;padding:2px 9px;border-radius:99px;white-space:nowrap;vertical-align:middle}
|
||||
.pill.ok{background:var(--ok-bg);color:var(--ok)} .pill.falla{background:var(--falla-bg);color:var(--falla)}
|
||||
.pill.info{background:var(--info-bg);color:var(--info)} .pill.pend{background:var(--pend-bg);color:var(--tinta2)}
|
||||
.pill.aviso{background:var(--aviso-bg);color:#7a4a00}
|
||||
table.t{border-collapse:collapse;width:100%;font-size:13.5px}
|
||||
table.t th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
||||
table.t td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea);vertical-align:top}
|
||||
.tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
a{color:var(--naranja)}
|
||||
|
||||
/* ---- "Cómo se usa": bloque plegable al inicio de cada página ---- */
|
||||
details.como{background:var(--info-bg);border:1px solid #c9dcec;border-radius:10px;padding:8px 14px;margin:0 0 14px;font-size:14px}
|
||||
details.como summary{cursor:pointer;font-weight:700;color:var(--info)}
|
||||
details.como ol{margin:8px 0 4px;padding-left:20px} details.como li{margin:3px 0}
|
||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]) details.como{border-color:#2d4760}}
|
||||
|
||||
/* ---- Kanban ---- */
|
||||
.kb-barra{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 12px}
|
||||
.kb-barra select,.kb-barra input{padding:8px 10px;font-size:14px}
|
||||
.kb-barra .cnt{color:var(--tinta2);font-size:13px;margin-left:auto}
|
||||
.kb{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;align-items:start}
|
||||
@media(max-width:980px){.kb{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:560px){.kb{grid-template-columns:1fr}}
|
||||
.col{background:var(--pend-bg);border:1px solid var(--linea);border-radius:12px;padding:8px;min-height:120px}
|
||||
.col.sobre{outline:2px dashed var(--naranja);outline-offset:-4px;background:var(--naranja-suave)}
|
||||
.col h3{margin:2px 4px 8px;font-size:13px;text-transform:uppercase;letter-spacing:.05em;color:var(--tinta2);display:flex;justify-content:space-between}
|
||||
.col h3 .n{background:var(--tarjeta);border:1px solid var(--linea);border-radius:99px;padding:0 8px;font-size:12px}
|
||||
.col.e-bloqueado h3{color:var(--falla)} .col.e-hecho h3{color:var(--ok)} .col.e-en-curso h3{color:var(--info)}
|
||||
.card{background:var(--tarjeta);border:1px solid var(--linea);border-left:4px solid var(--pend);border-radius:10px;padding:9px 10px;margin:0 0 8px;cursor:grab;font-size:14px;position:relative}
|
||||
.card:active{cursor:grabbing}
|
||||
.card.arrastrando{opacity:.45}
|
||||
.card.p-ingenieria{border-left-color:var(--morado)} .card.p-produccion{border-left-color:var(--ok)}
|
||||
.card.p-mecanica{border-left-color:var(--aviso)} .card.p-compras{border-left-color:var(--info)} .card.p-campo{border-left-color:var(--naranja)}
|
||||
.card.vencida{box-shadow:inset 0 0 0 1px var(--falla)}
|
||||
.card .tit{font-weight:700;line-height:1.3;margin-bottom:5px;cursor:pointer}
|
||||
.card .tit:hover{color:var(--naranja)}
|
||||
.card .meta{display:flex;flex-wrap:wrap;gap:4px 8px;font-size:12px;color:var(--tinta2);align-items:center}
|
||||
.card .chip{display:inline-block;font-size:11px;font-weight:700;padding:1px 7px;border-radius:99px;background:var(--pend-bg);color:var(--tinta2)}
|
||||
.card .chip.p-ingenieria{background:var(--morado-bg);color:var(--morado)} .card .chip.p-produccion{background:var(--ok-bg);color:var(--ok)}
|
||||
.card .chip.p-mecanica{background:var(--aviso-bg);color:#7a4a00} .card .chip.p-compras{background:var(--info-bg);color:var(--info)}
|
||||
.card .chip.p-campo{background:var(--naranja-suave);color:var(--naranja)}
|
||||
.card .fecha.mal{color:var(--falla);font-weight:700}
|
||||
.card .bloq{margin-top:5px;font-size:12px;color:var(--falla)}
|
||||
.card .enl{margin-top:5px;display:flex;flex-wrap:wrap;gap:4px 10px;font-size:12px}
|
||||
.card .mover{margin-top:7px;display:flex;gap:6px;align-items:center}
|
||||
.card .mover select{padding:5px 7px;font-size:12.5px;flex:1}
|
||||
@media(min-width:981px){.card .mover{display:none} .kb.movil .card .mover{display:flex}}
|
||||
.badge-v3,.badge-v4,.badge-v55,.badge-v56,.badge-v57,.badge-v6{display:inline-block;padding:0 7px;border-radius:999px;font-size:11px;font-weight:700;line-height:1.7;white-space:nowrap}
|
||||
.badge-v56{background:#2C3C5B;color:#fff} .badge-v55{background:#17A08C;color:#fff}
|
||||
.badge-v57{background:#e3f5e8;color:#1c5c2e;border:1px solid #8fd1a3} .badge-v6{background:#fff3cd;color:#7a5200;border:1px solid #f0c36d}
|
||||
.badge-v4{background:#EDEFF4;color:#2C3C5B;border:1px solid #c9d0e0} .badge-v3{background:#e9e9e9;color:#555;border:1px solid #ccc}
|
||||
|
||||
/* ---- diálogo de detalle / alta ---- */
|
||||
dialog{border:1px solid var(--linea);border-radius:14px;padding:0;max-width:720px;width:calc(100vw - 24px);background:var(--tarjeta);color:var(--tinta)}
|
||||
dialog::backdrop{background:rgba(0,0,0,.45)}
|
||||
dialog .dcab{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--linea);background:var(--tinta);color:#fff;border-radius:14px 14px 0 0}
|
||||
dialog .dcab h2{margin:0;font-size:17px}
|
||||
dialog .dcab button{background:transparent;color:#fff;font-size:20px;padding:2px 8px}
|
||||
dialog .dcuerpo{padding:6px 18px 14px;max-height:calc(100vh - 160px);overflow:auto}
|
||||
dialog .dpie{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;padding:12px 18px;border-top:1px solid var(--linea)}
|
||||
dialog .dpie .izq{margin-right:auto}
|
||||
.dos-col{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}
|
||||
@media(max-width:560px){.dos-col{grid-template-columns:1fr}}
|
||||
dialog input,dialog select,dialog textarea{width:100%}
|
||||
.enlaces-ed .fila{display:grid;grid-template-columns:1fr 1.4fr auto;gap:6px;margin-bottom:6px}
|
||||
.enlaces-ed .fila button{padding:6px 9px;font-size:13px}
|
||||
.lista-chk{max-height:140px;overflow:auto;border:1px solid var(--linea);border-radius:9px;padding:6px 10px;font-size:13.5px}
|
||||
.lista-chk label{display:flex;gap:8px;align-items:flex-start;font-weight:500;margin:4px 0;color:var(--tinta)}
|
||||
.lista-chk input{width:auto;margin-top:3px}
|
||||
.hist{font-size:13px;color:var(--tinta2);max-height:180px;overflow:auto;border-top:1px solid var(--linea);margin-top:12px;padding-top:8px}
|
||||
.hist div{padding:3px 0;border-bottom:1px dashed var(--linea)}
|
||||
.hist b{color:var(--tinta)}
|
||||
.msg{display:none;padding:10px 12px;border-radius:9px;margin-top:10px;font-weight:600;font-size:14px}
|
||||
.msg.ok{display:block;background:var(--ok-bg);color:var(--ok);border:1px solid var(--ok)}
|
||||
.msg.err{display:block;background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla)}
|
||||
|
||||
/* ---- tablero de ingeniería: tira de versiones ---- */
|
||||
.versiones{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px;margin-bottom:14px}
|
||||
.ver{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px;border-top:4px solid var(--pend)}
|
||||
.ver.v55{border-top-color:#17A08C} .ver.v56{border-top-color:#2C3C5B} .ver.v57{border-top-color:#1c5c2e} .ver.v6{border-top-color:#c99a1a}
|
||||
.ver h3{margin:0 0 4px;font-size:17px;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
||||
.ver .est{font-weight:700;font-size:13.5px}
|
||||
.ver .sig{font-size:13.5px;margin:6px 0} .ver .sig b{color:var(--naranja)}
|
||||
.ver .quien{font-size:12.5px;color:var(--tinta2)}
|
||||
.ver .docs{margin-top:8px;display:flex;flex-wrap:wrap;gap:4px 10px;font-size:12.5px}
|
||||
.ver .cnt{font-size:12.5px;color:var(--tinta2);margin-top:6px}
|
||||
.decisiones li{margin:5px 0;font-size:14px} .decisiones b{color:var(--tinta)}
|
||||
.filosofia li{margin:6px 0;font-size:14px}
|
||||
|
||||
/* ---- tablero de producción ---- */
|
||||
.prog{height:14px;border-radius:7px;background:var(--pend-bg);overflow:hidden;margin:6px 0 4px;display:flex}
|
||||
.prog span{display:block;height:100%;background:var(--ok)}
|
||||
.prog span.pl{background:var(--info)}
|
||||
.obj{display:flex;justify-content:space-between;font-size:13px;color:var(--tinta2)}
|
||||
.embudo{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:8px}
|
||||
.etapa{background:var(--superf);border:1px solid var(--linea);border-radius:10px;padding:10px 10px;text-align:center}
|
||||
.etapa .n{font-size:26px;font-weight:700;line-height:1}
|
||||
.etapa .l{font-size:12px;color:var(--tinta2);margin-top:3px;line-height:1.25}
|
||||
.etapa .f{font-size:11px;color:var(--tinta2);margin-top:3px;opacity:.8}
|
||||
.etapa.sin .n{color:var(--pend)}
|
||||
.bloqueos .card{cursor:default}
|
||||
@ -4,16 +4,31 @@ title: "Taller"
|
||||
|
||||
# Taller
|
||||
|
||||
Unidad o tarjeta en la mesa. Antes de tocarla, abre su [expediente](../unidad.html). Reglas de banco: [diagnóstico en banco](../procedimientos/diagnostico-banco.md#reglas-de-banco).
|
||||
Unidad o tarjeta **en la mesa**: llegó del campo, falló en producción o hay que validar un firmware. Antes de tocarla, abre su [expediente](../unidad.html) y lee las actas anteriores. Reglas de banco (límite de corriente, 5 V ≥ 5.2 V o paquete conectado, baterías hinchadas): [diagnóstico en banco](../procedimientos/diagnostico-banco.md#reglas-de-banco).
|
||||
|
||||
| Trabajo | Cuándo | Procedimiento | Acta |
|
||||
|---|---|---|---|
|
||||
| Diagnóstico en banco | Llegó del campo o falló en producción | [Diagnóstico en banco](../procedimientos/diagnostico-banco.md) | [Banco](../formularios/banco.html) |
|
||||
| Reflasheo | Después de subir la captura del diagnóstico | [Reflasheo de unidades](../procedimientos/reflasheo-unidades.md) | [Reflasheo y configuración](../formularios/reflasheo.html) |
|
||||
| Módem 4G en batería | Validar firmware o unidad que no reporta en batería | [Módem 4G en batería](../procedimientos/prueba-bateria-modem.md) | [Módem en batería](../formularios/prueba-bateria.html) |
|
||||
| Ethernet y puertos serie | El cliente usa Ethernet, RS-485 o RS-232 | [Ethernet y puertos serie](../procedimientos/v5-prueba-ethernet-serie.md) | [Ethernet y serie](../formularios/ethernet-serie.html) |
|
||||
| Tarjeta V4 | Preparación y prueba con 0.6.0 | [Preparación y prueba V4](../procedimientos/v4-prueba-fw-0.5.6.md) | [Prueba V4](../formularios/v4.html) |
|
||||
| Tarjeta V4 sin NMEA | El módem registra (`$GPTXT`) pero no hay posición; `nmea=0`; NO RTK tenue | [Riel SW_5V y arranque del F9P](../procedimientos/v4-prueba-riel-sw5v-f9p.md) | [Prueba V4](../formularios/v4.html) |
|
||||
## Qué trabajo, qué procedimiento, qué acta
|
||||
|
||||
- [Manual V5](../manuales/manual-tarjeta-V5.md) · [Manual V4](../manuales/manual-tarjeta-V4.md) · [Pinout V5.6](../hardware/v56-pinout.md)
|
||||
- [Tablero de producción](../tablero.html)
|
||||
| Trabajo | Cuándo | Tarjetas | Procedimiento | Acta |
|
||||
|---|---|---|---|---|
|
||||
| Diagnóstico en banco | Llegó del campo (después del [diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md)) o falló en producción | V3 · V4 · V5.5 | [Diagnóstico en banco](../procedimientos/diagnostico-banco.md) | [Banco](../formularios/banco.html) |
|
||||
| Reflasheo | Solo después de subir la captura del diagnóstico. Es una prueba, no una reparación | V3 · V4 · V5.5 | [Reflasheo de unidades](../procedimientos/reflasheo-unidades.md) | [Reflasheo y configuración](../formularios/reflasheo.html) |
|
||||
| Módem 4G en batería | Validar un firmware nuevo en una unidad completa, o unidad que no reporta en batería | V5.5 (V4: ver abajo) | [Módem 4G en batería](../procedimientos/prueba-bateria-modem.md) | [Módem en batería](../formularios/prueba-bateria.html) |
|
||||
| Ethernet y puertos serie | El cliente usa Ethernet, RS-485 o RS-232; obligatoria en al menos una V5.5 antes de pedir la V5.6 | V5.5 · V5.6 | [Ethernet y puertos serie](../procedimientos/v5-prueba-ethernet-serie.md) | [Ethernet y serie](../formularios/ethernet-serie.html) |
|
||||
| Tarjeta V4 — preparación y prueba | Una vez por tarjeta V4 antes de darla por buena con 0.6.0 (puente de reset del módem, cable sin 5 V, 24 V, batería) | V4 | [Preparación y prueba V4](../procedimientos/v4-prueba-fw-0.5.6.md) | [Prueba V4](../formularios/v4.html) |
|
||||
| Tarjeta V4 sin NMEA | El módem registra (`$GPTXT`) pero no hay posición; `nmea=0`; LED **NO RTK** tenue | V4 | [Riel SW_5V y arranque del F9P](../procedimientos/v4-prueba-riel-sw5v-f9p.md) | [Prueba V4](../formularios/v4.html) |
|
||||
|
||||
## Qué firmware cargar
|
||||
|
||||
Por tarjeta, nunca por número de versión. **0.6.0** es el de campo para V4 y V5; **0.6.3** está en prueba de banco desde el 1-oct y no se instala en campo; **4.0.0** para V3. Binarios y SHA-256 en [Descargas](../descargas/index.md); qué cambió en cada versión, en las [notas de versión](../descargas/notas-de-version.md).
|
||||
|
||||
## Qué revisar en toda tarjeta que llega
|
||||
|
||||
- **U6 (LTC4040) con lupa** antes de energizar: bolas de soldadura y puentes bajo el QFN son el defecto recurrente de V5.5 y V4 (regla DFM del 8-oct). Foto al acta.
|
||||
- Batería hinchada, caliente o con olor: desconéctala primero y trabaja sin ella.
|
||||
- Cruce en el arnés GX16: así se destruyó la V5-006 (18-sep). Mide antes de conectar.
|
||||
|
||||
## Referencias de la mesa
|
||||
|
||||
- [Manual V5](../manuales/manual-tarjeta-V5.md) · [Manual V4](../manuales/manual-tarjeta-V4.md) · [Manual V3](../manuales/manual-tarjeta-V3.md) · [Pinout V5.6](../hardware/v56-pinout.md) · [Hoja de datos V5.6](../hardware/v56-datasheet.md)
|
||||
- [Estado de actas](../tablero.html) · [Expediente de una unidad](../unidad.html) · [Kanban](../tableros/kanban.html) (pista Producción)
|
||||
- Procedimientos anteriores (V3, protoboard, fan v3.0.3): [Referencia e histórico](../historia/index.md)
|
||||
|
||||
@ -61,6 +61,18 @@ table.det td{padding:5px 8px 5px 0;border-bottom:1px solid var(--linea);vertical
|
||||
table.det td.k{color:var(--tinta2);width:46%}
|
||||
.vacio{color:var(--tinta2);font-style:italic;font-size:14px}
|
||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px}
|
||||
/* cadenas de trazabilidad: producción → campo */
|
||||
.cadena{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 4px}
|
||||
.cadena .paso{flex:1 1 118px;min-width:118px;border:1px solid var(--linea);border-radius:9px;padding:7px 9px;background:var(--superf);font-size:12.5px;position:relative}
|
||||
.cadena .paso .n{font-weight:700;font-size:13px}
|
||||
.cadena .paso .e{margin-top:2px}
|
||||
.cadena .paso .d{color:var(--tinta2);font-size:11.5px;margin-top:2px;line-height:1.3}
|
||||
.cadena .paso.ok{border-color:var(--ok);background:var(--ok-bg)} .cadena .paso.ok .e{color:var(--ok)}
|
||||
.cadena .paso.falla{border-color:var(--falla);background:var(--falla-bg)} .cadena .paso.falla .e{color:var(--falla)}
|
||||
.cadena .paso.info{border-color:var(--info);background:var(--info-bg)} .cadena .paso.info .e{color:var(--info)}
|
||||
.cadena .paso.pend .e{color:var(--tinta2)}
|
||||
.cadena .paso a{color:inherit}
|
||||
.cad-t{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--tinta2);margin:8px 0 0}
|
||||
@media print{header.top{background:#fff;color:#000} .buscar,button{display:none}}
|
||||
</style>
|
||||
</head>
|
||||
@ -164,6 +176,9 @@ function render(j){
|
||||
.forEach(([n,l])=>kp.append(el('div',{class:'kpi'},el('div',{class:'n',text:String(n)}),el('div',{class:'l',text:l}))));
|
||||
cont.append(kp);
|
||||
|
||||
// ---- trazabilidad: cadena de producción y cadena de campo ----
|
||||
cont.append(trazabilidad(j));
|
||||
|
||||
// ---- línea de tiempo ----
|
||||
const b=el('section',{class:'bloque'});
|
||||
b.append(el('h2',{text:'Historial'}), el('p',{class:'sub',text:'Todo lo registrado para esta serie, de lo más reciente a lo más viejo.'}));
|
||||
@ -232,6 +247,84 @@ function render(j){
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- trazabilidad ----------
|
||||
// Cadena de producción (tarjeta → módulos → integración → ensamble → configuración) y cadena de
|
||||
// campo (envío → recepción → instalación → diagnósticos → daños), con fecha y sitio. Para una unidad
|
||||
// HBOX, las actas de sus módulos viven en OTRAS series (componentes del ensamble): se consultan aparte.
|
||||
let SITIOS = null;
|
||||
async function nombreSitio(id){
|
||||
if(!id) return '';
|
||||
if(!SITIOS){ try{ SITIOS = ((await (await fetch('/api/sitios?todos=1')).json()).sitios)||[]; }catch(e){ SITIOS=[]; } }
|
||||
const s = SITIOS.find(x=>x.id===id); return s ? s.nombre : id;
|
||||
}
|
||||
function paso(nombre, cls, estado, detalle){
|
||||
const p=el('div',{class:'paso '+cls}); p.append(el('div',{class:'n',text:nombre}), el('div',{class:'e',text:estado}));
|
||||
if(detalle) p.append(el('div',{class:'d',html:detalle}));
|
||||
return p;
|
||||
}
|
||||
function ultima(actas, tipos){ return actas.filter(a=>tipos.includes(a.tipo))[0]||null; } // ya vienen de reciente a vieja
|
||||
function pasoActa(nombre, a, textoOK, textoPend){
|
||||
if(!a) return paso(nombre,'pend',textoPend||'sin acta');
|
||||
const falla = a.resultado==='FALLA';
|
||||
return paso(nombre, falla?'falla':'ok', falla?'✕ FALLA':(textoOK||'✓ '+(a.resultado||'OK')), (a.fecha||fecha(a.recibido).slice(0,10))+(a.tecnico?' · '+a.tecnico:''));
|
||||
}
|
||||
function trazabilidad(j){
|
||||
const sec=el('section',{class:'bloque'});
|
||||
sec.append(el('h2',{text:'Trazabilidad'}), el('p',{class:'sub',text:'De la tarjeta al puerto: cada paso con su acta, fecha y sitio. Gris = todavía no hay acta.'}));
|
||||
const A=j.actas, esHbox=/^HBOX/i.test(j.serie);
|
||||
// --- producción
|
||||
sec.append(el('div',{class:'cad-t',text:'Producción'}));
|
||||
const prod=el('div',{class:'cadena'}); sec.append(prod);
|
||||
const ens=ultima(A,['ensamble']);
|
||||
let comp=(ens&&ens.componentes)||{};
|
||||
// sin acta de ensamble, los módulos se deducen del número de unidad: HBOX5-E02-A002-R0002 → V5-02 · ARD-002 · RTK-0002
|
||||
const mSer=!comp.einar && j.serie.match(/^HBOX5-E(\d+)-A(\d+)-R(\d+)$/i);
|
||||
if(mSer) comp={einar:'V5-'+mSer[1].padStart(2,'0'), ard:'ARD-'+mSer[2].padStart(3,'0'), modem:'RTK-'+mSer[3].padStart(4,'0')};
|
||||
if(esHbox){
|
||||
// módulos: se consultan por su serie
|
||||
const m=[['Tarjeta EinarBoard','prueba-v5',comp.einar],['ArduSimple','modulo-ard',comp.ard],['Módem 4G','modulo-4g',comp.modem]];
|
||||
const ph={}; m.forEach(([n,,s])=>{ const p=paso(n,'pend',s?'consultando…':'sin ensamble', s?'<a href="?serie='+encodeURIComponent(s)+'">'+s+'</a>':''); ph[n]=p; prod.append(p); });
|
||||
const integ=paso('Integración','pend',comp.ard&&comp.modem?'consultando…':'—'); prod.append(integ);
|
||||
(async()=>{
|
||||
for(const [n,tipo,s] of m){
|
||||
if(!s) continue;
|
||||
let txt='sin acta', cls='pend';
|
||||
try{ const r=await (await fetch(`/api/acta?tipo=${tipo}&serie=${encodeURIComponent(s)}`)).json();
|
||||
if(r.exists){ cls=r.acta.resultado==='FALLA'?'falla':'ok'; txt=cls==='ok'?'✓ aprobada':'✕ FALLA'; ph[n].querySelector('.d').innerHTML+=' · '+(r.acta.fecha||fecha(r.acta._recibido).slice(0,10)); } }catch(e){}
|
||||
ph[n].className='paso '+cls; ph[n].querySelector('.e').textContent=txt;
|
||||
}
|
||||
if(comp.ard&&comp.modem){
|
||||
const si='INT-A'+(String(comp.ard).match(/(\d+)\s*$/)||[,''])[1]+'-R'+(String(comp.modem).match(/(\d+)\s*$/)||[,''])[1];
|
||||
let txt='sin acta', cls='pend';
|
||||
try{ const r=await (await fetch(`/api/acta?tipo=integracion&serie=${encodeURIComponent(si)}`)).json();
|
||||
if(r.exists){ cls=r.acta.resultado==='FALLA'?'falla':'ok'; txt=cls==='ok'?'✓ aprobada':'✕ FALLA'; integ.append(el('div',{class:'d',html:'<a href="?serie='+encodeURIComponent(si)+'">'+si+'</a> · '+(r.acta.fecha||fecha(r.acta._recibido).slice(0,10))})); } }catch(e){}
|
||||
integ.className='paso '+cls; integ.querySelector('.e').textContent=txt;
|
||||
}
|
||||
})();
|
||||
prod.append(pasoActa('Ensamble',ens,'✓ unidad lista'));
|
||||
prod.append(pasoActa('Configuración / pruebas',ultima(A,['reflasheo','prueba-ethernet-serie','prueba-bateria-modem']),null,'sin acta'));
|
||||
} else {
|
||||
// pieza suelta: su propia acta de prueba, y en qué unidad quedó
|
||||
prod.append(pasoActa('Acta de la pieza',ultima(A,['prueba-v5','prueba-v4','modulo-ard','modulo-4g'])));
|
||||
const integ=j.relacionadas.find(r=>r.tipo==='integracion'), ensR=j.relacionadas.find(r=>r.tipo==='ensamble');
|
||||
prod.append(integ?paso('Integración',integ.resultado==='FALLA'?'falla':'ok',integ.resultado==='FALLA'?'✕ FALLA':'✓ en pareja','<a href="?serie='+encodeURIComponent(integ.serie)+'">'+integ.serie+'</a>'):paso('Integración','pend','sin acta'));
|
||||
prod.append(ensR?paso('Ensamble',ensR.resultado==='FALLA'?'falla':'info',ensR.resultado==='FALLA'?'✕ FALLA':'▣ montada en unidad','<a href="?serie='+encodeURIComponent(ensR.serie)+'">'+ensR.serie+'</a>'):paso('Ensamble','pend','libre (no está en una unidad)'));
|
||||
prod.append(pasoActa('Banco / reflasheo',ultima(A,['diagnostico-banco','reflasheo']),null,'—'));
|
||||
}
|
||||
// --- campo
|
||||
sec.append(el('div',{class:'cad-t',text:'Campo'}));
|
||||
const campo=el('div',{class:'cadena'}); sec.append(campo);
|
||||
const env=ultima(A,['envio']), rec=ultima(A,['recepcion']), ins=A.filter(a=>a.tipo==='instalacion-campo'&&a.resultado!=='FALLA')[0]||ultima(A,['instalacion-campo']);
|
||||
const diags=A.filter(a=>a.tipo==='diagnostico-campo'), danos=A.filter(a=>a.tipo==='dano');
|
||||
const conSitio=async(p,a)=>{ if(a&&a.sitio){ const n=await nombreSitio(a.sitio); p.querySelector('.d').innerHTML+=' · <a href="sitios/sitio.html?id='+encodeURIComponent(a.sitio)+'">'+n+'</a>'; } };
|
||||
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||'✓',(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 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;
|
||||
}
|
||||
|
||||
async function cargar(serie){
|
||||
cont.innerHTML='<p class="vacio">Cargando…</p>';
|
||||
try{
|
||||
|
||||
139
mkdocs.yml
139
mkdocs.yml
@ -56,10 +56,24 @@ markdown_extensions:
|
||||
permalink: true
|
||||
|
||||
nav:
|
||||
- "🏠 Inicio": index.md
|
||||
- "🧭 Guía del ingeniero": guia-ingeniero.md
|
||||
- "🏠 Inicio":
|
||||
- index.md
|
||||
- "Guía del ingeniero": guia-ingeniero.md
|
||||
- "⚙ Cuenta":
|
||||
- "Mi cuenta (cambiar contraseña)": cuenta.html
|
||||
- "Administración (Leo)": admin.html
|
||||
- "Salir": salir.html
|
||||
|
||||
- "🏭 Producción — lote V5.6":
|
||||
- "📋 Tableros":
|
||||
- tableros/index.md
|
||||
- "Kanban — tareas del equipo": tableros/kanban.html
|
||||
- "Ingeniería — estado por versión de tarjeta": tableros/ingenieria.html
|
||||
- "Producción — plan 570 vs. avance real": tableros/produccion.html
|
||||
- "Estado de actas (tarjetas, módulos, unidades)": tablero.html
|
||||
- "Tablero de campo (visitas de diagnóstico)": diagnostico/tablero-campo.html
|
||||
- "Expediente de una unidad": unidad.html
|
||||
|
||||
- "🏭 Producción":
|
||||
- produccion/index.md
|
||||
- "1 · Prueba y flasheo de tarjetas":
|
||||
- procedimientos/v5-prueba-y-flasheo.md
|
||||
@ -74,8 +88,9 @@ nav:
|
||||
- "Acta de ensamble": formularios/ensamble.html
|
||||
- "4 · Prueba final antes de embarcar": procedimientos/v5-prueba-final-unidad.md
|
||||
- "5 · Configuración de la unidad": procedimientos/v5-configuracion-unidad.md
|
||||
- "6 · Checklist de puesta en marcha": procedimientos/v5-checklist-puesta-en-marcha.md
|
||||
- "Tablero de producción": tablero.html
|
||||
- "📦 Inventario":
|
||||
- inventario/index.md
|
||||
- "Inventario — existencias y movimientos": inventario/inventario.html
|
||||
|
||||
- "🔬 Taller":
|
||||
- taller/index.md
|
||||
@ -94,86 +109,86 @@ nav:
|
||||
- "V4 — preparación y prueba con 0.6.0":
|
||||
- procedimientos/v4-prueba-fw-0.5.6.md
|
||||
- "Acta de prueba V4": formularios/v4.html
|
||||
- "V4 — riel SW_5V y arranque del F9P (sin NMEA)":
|
||||
- procedimientos/v4-prueba-riel-sw5v-f9p.md
|
||||
- "Acta de prueba V4": formularios/v4.html
|
||||
- "V4 — riel SW_5V y arranque del F9P (sin NMEA)": procedimientos/v4-prueba-riel-sw5v-f9p.md
|
||||
|
||||
- "🚜 Campo":
|
||||
- campo/index.md
|
||||
- "Diagnóstico en campo":
|
||||
- diagnostico/procedimiento-diagnostico-campo.md
|
||||
- "Acta de diagnóstico en campo": formularios/diagnostico.html
|
||||
- "Tablero de campo": diagnostico/tablero-campo.html
|
||||
- "Puesta en marcha en el puerto": procedimientos/v5-checklist-puesta-en-marcha.md
|
||||
- "Instalación en el puerto":
|
||||
- "Checklist de puesta en marcha (imprimir)": procedimientos/v5-checklist-puesta-en-marcha.md
|
||||
- "Acta de instalación en campo": formularios/instalacion.html
|
||||
- "Envío, recepción y daños":
|
||||
- procedimientos/ciclo-de-vida.md
|
||||
- "Acta de envío": formularios/envio.html
|
||||
- "Acta de recepción e inspección": formularios/recepcion.html
|
||||
- "Acta de daño o incidente": formularios/dano.html
|
||||
- "Expediente de una unidad": unidad.html
|
||||
- "📍 Sitios":
|
||||
- sitios/index.md
|
||||
- "Sitio — unidades instaladas y actas": sitios/sitio.html
|
||||
- "Actas — índice y cómo se usan": actas/index.md
|
||||
|
||||
- "🔩 Tarjetas y hardware":
|
||||
- hardware/index.md
|
||||
- "Manuales por tarjeta": manuales/index.md
|
||||
- "V5.6 — lote en pedido":
|
||||
- "V5.5 en campo · V5.6 en layout → pedido 113":
|
||||
- "Manual de la tarjeta V5 (V5.5 y V5.6)": manuales/manual-tarjeta-V5.md
|
||||
- "Hoja de datos V5.6": hardware/v56-datasheet.md
|
||||
- "Pinout de conectores V5.6": hardware/v56-pinout.md
|
||||
- "Instrucciones de layout (esquemático → PCB)": hardware/v56-layout-instrucciones.md
|
||||
- "ECO V5.6": hardware/v56-eco.md
|
||||
- "Cambios al esquemático V5.5 → V5.6": hardware/v56-cambios-esquematico.md
|
||||
- "Layout final — pasada 1 (Michelle, 12-oct)": hardware/v56-layout-pasada-1.md
|
||||
- "▶ Layout — pasada 2: correcciones antes del pedido (Michelle, 13-oct)": hardware/v56-layout-pasada-2.md
|
||||
- "Hoja de datos V5.6 (1-oct)": hardware/v56-datasheet.md
|
||||
- "Pinout de conectores V5.6 (29-sep)": hardware/v56-pinout.md
|
||||
- "ECO V5.6 — requisitos R1–R5 (27-sep)": hardware/v56-eco.md
|
||||
- "Cambios al esquemático V5.5 → V5.6 (28-sep)": hardware/v56-cambios-esquematico.md
|
||||
- "Reglas de layout: esquemático → PCB (29-sep)": hardware/v56-layout-instrucciones.md
|
||||
- "▶ Layout pasada 2 — correcciones antes del pedido (Michelle, 13-oct)": hardware/v56-layout-pasada-2.md
|
||||
- "Auditoría del layout 13-oct — NO LISTA (inglés)": hardware/v56-auditoria-layout-13oct.md
|
||||
- "V5.7 — fuente 24 V → 5 V a bordo (en diseño)":
|
||||
- "ECO V5.7 — fuente a bordo (v0.2, esquemático aplicado)": hardware/v57-eco-fuente.md
|
||||
- "▶ Guía de layout V5.7 (Leo → Michelle)": hardware/v57-layout-guia.md
|
||||
- "V4":
|
||||
- "Manual de la tarjeta V4": manuales/manual-tarjeta-V4.md
|
||||
- "V3 — producción antigua":
|
||||
- "Manual de la tarjeta V3": manuales/manual-tarjeta-V3.md
|
||||
- "Pinout V3 (arneses y conectores)": montaje/pinout-arneses.md
|
||||
- "BOM V3 (dividido para ensamble)": montaje/bom-split.md
|
||||
- "Guía de ensamble y control de calidad": montaje/guia-ensamble-qc.md
|
||||
- "Guía de ensamble (PDF)": montaje/assembly-guide.md
|
||||
- "v6 — en diseño, no pedir":
|
||||
- "Manual de la tarjeta v6": manuales/manual-tarjeta-v6.md
|
||||
- "V5.7 fuente a bordo → pedido 327 (en diseño)":
|
||||
- "ECO V5.7 — fuente 24 V → 5 V a bordo (v0.2, 13-oct)": hardware/v57-eco-fuente.md
|
||||
- "▶ Guía de layout V5.7 — colocar la fuente (Leo, 13-oct)": hardware/v57-layout-guia.md
|
||||
- "v6 GNSS + 4G a bordo → pedido 130 (en diseño)":
|
||||
- "Manual de la tarjeta v6 (marcador)": manuales/manual-tarjeta-v6.md
|
||||
- "▶ Próximos pasos para ordenar (29-sep)": hardware/v6-proximos-pasos.md
|
||||
- "✅ Plan de corrección y estado (28-sep)": hardware/v6-plan-correccion.md
|
||||
- "Cambios de esquemático — resumen de 3 pasadas": hardware/v6-cambios-resumen.md
|
||||
- "Puertos y sin nueva versión (inglés)": hardware/v6-puertos-sin-nueva-version.md
|
||||
- "Revisión final (inglés)": hardware/v6-revision-final.md
|
||||
- "Auditoría completa v6 (inglés)": hardware/v6-full-audit.md
|
||||
- "Auditoría completa v6": hardware/v6-auditoria-completa.md
|
||||
- "Cambios al esquemático v6": hardware/v6-cambios-esquematico.md
|
||||
- "Revisión v6 contra lecciones V5.5": hardware/v6-revision-lecciones-v55.md
|
||||
- "Mapa de pines MCU v6": hardware/v6-pinmap.md
|
||||
- "Auditoría pre-fabricación v6": hardware/v6-auditoria-prefab.md
|
||||
- "Auditoría pre-fabricación v6 (inglés)": hardware/v6-prefab-audit.md
|
||||
- "Plan de cobre y colocación EMC v6": hardware/EinarBoard-v6_Plan-de-Cobre_ES.html
|
||||
- "Plan de cobre y colocación EMC v6 (inglés)": hardware/EinarBoard-v6_Copper-Plan_EN.html
|
||||
- "Plan de corrección y lista de liberación (28-sep)": hardware/v6-plan-correccion.md
|
||||
- "Esquemático final — resumen de las 3 pasadas (28-sep)": hardware/v6-cambios-resumen.md
|
||||
- "Mapa de pines MCU para board_v6.h (28-sep)": hardware/v6-pinmap.md
|
||||
- "Plan de cobre y colocación EMC (5-sep)": hardware/EinarBoard-v6_Plan-de-Cobre_ES.html
|
||||
- "Plan de cobre y colocación EMC (inglés)": hardware/EinarBoard-v6_Copper-Plan_EN.html
|
||||
- "v6 — auditoría del 28-sep y sus pasadas (detalle)":
|
||||
- "Auditoría completa v6 (28-sep)": hardware/v6-auditoria-completa.md
|
||||
- "Auditoría completa v6 (inglés, original)": hardware/v6-full-audit.md
|
||||
- "Pasada 1 — cambios al esquemático (28-sep)": hardware/v6-cambios-esquematico.md
|
||||
- "Pasada 2 — puertos y sin nueva versión (inglés)": hardware/v6-puertos-sin-nueva-version.md
|
||||
- "Pasada 3 — revisión final independiente (inglés)": hardware/v6-revision-final.md
|
||||
- "Revisión v6 contra lecciones V5.5 (28-sep, superada)": hardware/v6-revision-lecciones-v55.md
|
||||
- "V4 y V3 — tarjetas anteriores aún en campo":
|
||||
- "Manual de la tarjeta V4": manuales/manual-tarjeta-V4.md
|
||||
- "Manual de la tarjeta V3": manuales/manual-tarjeta-V3.md
|
||||
- "Pinout V3 (arneses y conectores, jul-2026)": montaje/pinout-arneses.md
|
||||
|
||||
- "📕 Manuales y descargas":
|
||||
- "Manual del cliente (instalación y operación)": manuales/manual-cliente.md
|
||||
- "Manual de instalación y operación (PDF)": manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf
|
||||
- "Ficha técnica HBOX (PDF)": manuales/Drogheda-HBOX-Datasheet_v2.0.pdf
|
||||
- "Descargas":
|
||||
- "Manual de instalación y operación v2.0 (PDF)": manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf
|
||||
- "Ficha técnica HBOX v2.0 (PDF)": manuales/Drogheda-HBOX-Datasheet_v2.0.pdf
|
||||
- "Descargas (firmware por tarjeta)":
|
||||
- descargas/index.md
|
||||
- "Notas de versión del firmware": descargas/notas-de-version.md
|
||||
- "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md
|
||||
- "Git y archivos de diseño":
|
||||
- "Guía Git": git/guia-git-es.md
|
||||
- "Notas de versión del firmware V4 / V5": descargas/notas-de-version.md
|
||||
- "README firmware hbox_v5_bringup (inglés)": descargas/hbox-v5-bringup-README.md
|
||||
- "Git y archivos de diseño (KiCad)":
|
||||
- "Guía Git para colaboradores": git/guia-git-es.md
|
||||
- "Guía Git (inglés)": git/git-guide-en.md
|
||||
|
||||
- "📚 Referencia e histórico":
|
||||
- "Protocolo de diagnóstico remoto": diagnostico/protocolo-diagnostico-remoto.md
|
||||
- "EinarBoard 1.0 — puesta en marcha (histórico)": procedimientos/einarboard-1.0-bringup.md
|
||||
- "FW v3.0.3 — prueba de banco del ventilador": procedimientos/fan-policy-bench-test.md
|
||||
- "Histórico — prototipo v3 en protoboard": procedimientos/historico-proto-bench-v3.md
|
||||
- "Histórico — lista de compras del protoboard": procedimientos/historico-protoboard.md
|
||||
|
||||
- "⚙ Cuenta":
|
||||
- "Mi cuenta (cambiar contraseña)": cuenta.html
|
||||
- "Administración (Leo)": admin.html
|
||||
- "Salir": salir.html
|
||||
- historia/index.md
|
||||
- "Procedimientos anteriores (superados)":
|
||||
- "Protocolo de diagnóstico remoto — unidades v1 (jun-2026)": diagnostico/protocolo-diagnostico-remoto.md
|
||||
- "EinarBoard 1.0 — puesta en marcha (jul-2026)": procedimientos/einarboard-1.0-bringup.md
|
||||
- "FW v3.0.3 — prueba de banco del ventilador (ago-2026)": procedimientos/fan-policy-bench-test.md
|
||||
- "Prototipo v3 en protoboard — bring-up (jun-2026)": procedimientos/historico-proto-bench-v3.md
|
||||
- "Prototipo v3 en protoboard — lista de compras (jun-2026)": procedimientos/historico-protoboard.md
|
||||
- "V3 — producción de jul-2026 (superado)":
|
||||
- "BOM V3 dividido para ensamble (jul-2026)": montaje/bom-split.md
|
||||
- "Guía de ensamble y QC — borrador V3 (sep-2026)": montaje/guia-ensamble-qc.md
|
||||
- "Guía de ensamble del gabinete 0007-1000 (PDF)": montaje/assembly-guide.md
|
||||
- "Tarjetas — documentos superados":
|
||||
- "V5.6 layout pasada 1 (Michelle, 12-oct) — superada por la pasada 2": hardware/v56-layout-pasada-1.md
|
||||
- "v6 auditoría pre-fabricación (25-sep) — superada por la del 28-sep": hardware/v6-auditoria-prefab.md
|
||||
- "v6 auditoría pre-fabricación (25-sep, inglés)": hardware/v6-prefab-audit.md
|
||||
|
||||
@ -16,7 +16,7 @@ echo "Sitio publicado en $REMOTE:$REMOTE_PATH (puerto $PORT)"
|
||||
# El backend NO vive en git en el servidor: se copia igual que el sitio.
|
||||
# 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 \
|
||||
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 "$REMOTE:$BACKEND_PATH/"
|
||||
echo "Backend copiado en $REMOTE:$BACKEND_PATH"
|
||||
echo
|
||||
|
||||
Loading…
Reference in New Issue
Block a user