hbox-docs/backend/README.md

206 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Backend de actas — configuración en el servidor (una sola vez)
Servicio Flask que recibe los formularios firmados de los técnicos y los guarda como
JSON por número de serie en `/srv/projects/drogheda/actas/`. Corre detrás de nginx en
`127.0.0.1:8081`; nginx expone `/api/` en el mismo dominio, con la misma contraseña.
## 1. Instalar (en el servidor, como leo)
```bash
cd /srv/projects/drogheda/hbox-docs/backend
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
# carpeta donde se guardan las actas
mkdir -p /srv/projects/drogheda/actas
```
## 2. Servicio systemd (arranque automático)
```bash
sudo cp actas-hbox.service /etc/systemd/system/actas-hbox.service
sudo systemctl daemon-reload
sudo systemctl enable --now actas-hbox
systemctl status actas-hbox # debe decir "active (running)"
curl -s http://127.0.0.1:8081/api/salud # {"ok":true,...}
```
## 3. nginx — exponer /api/ en el mismo dominio
Dentro del bloque `server { ... }` de `drogheda.helgen.online` (el mismo archivo
`/etc/nginx/sites-enabled/drogheda.helgen.online.conf`, en la parte de `listen 443`),
agrega **antes** del `location / {`:
```nginx
location /api/ {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# hereda el auth_basic del server; si quieres, puedes repetirlo aquí.
}
```
Luego:
```bash
sudo nginx -t && sudo systemctl reload nginx
curl -u leo:TU_PASS https://drogheda.helgen.online/api/salud # {"ok":true,...}
```
## 4. Ver las actas recibidas
- Índice rápido por tipo: `/srv/projects/drogheda/actas/<tipo>/_indice.csv` (ábrelo en Excel).
- Listado JSON: `https://drogheda.helgen.online/api/actas`
- Un acta concreta: `https://drogheda.helgen.online/api/actas/prueba-v5/V5-07_....json`
- Respaldo: basta con copiar la carpeta `/srv/projects/drogheda/actas/`.
## Actualizar el backend
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) |
| `EQUIPO_DIR` | `/srv/projects/drogheda/equipo` | miembros del equipo (14-oct) + `_accesos.csv` |
| `HTPASSWD_LOCK` | `/srv/projects/drogheda/.htpasswd.lock` | candado para escribir el htpasswd desde Equipo |
| `TAREAS_BORRAN` | `adriana,gibranlp` | quién borra pendientes además de `ACTAS_ADMINS` (22-oct); lo borrado va a `tareas/_borrados/` |
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`), salvo los pendientes, que también
borran los de `TAREAS_BORRAN` y pasan a `tareas/_borrados/` en vez de desaparecer (22-oct). 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.
## 6. Equipo y accesos (14-oct)
Dos cosas distintas: un **miembro del equipo** (`EQUIPO_DIR`, omisión `/srv/projects/drogheda/equipo`,
un JSON por persona) es quien puede ser **responsable** de un pendiente; un **acceso** es una línea en
el htpasswd de nginx. `tiene_login` se deduce del htpasswd al leer; `es_admin` de `ACTAS_ADMINS`.
Los accesos se crean y quitan desde la página **Equipo** (admin), con la misma rutina `$apr1$` y la misma
escritura atómica que `/api/password`, bajo un `flock` (`HTPASSWD_LOCK`, omisión
`/srv/projects/drogheda/.htpasswd.lock`). Bitácora de altas/bajas en `equipo/_accesos.csv`.
Requisito en el servidor: el servicio debe poder **escribir** el htpasswd (`chown leo:www-data` +
`chmod 640`, §7 de DESPLIEGUE.md).
Campos del miembro: `id` (= usuario del sitio si lo tiene), `nombre`, `rol`, `area` ∈ direccion·operaciones·
ingenieria·layout·produccion·pruebas·fabricacion·software·campo·asistente, `correo`, `telefono`, `activo`.
```bash
curl -s -u $U "$D/api/equipo" # activos, con tiene_login, es_admin y pendientes {abiertos, vencidas, por estado}
curl -s -u $U "$D/api/equipo?todos=1" # incluye inactivos; para admin trae logins_sin_miembro
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/equipo \
-d '{"id":"adriana","nombre":"Adriana","rol":"Operaciones; lleva el PM","area":"operaciones"}' # admin; sin acceso al sitio
curl -s -u $U -H 'Content-Type: application/json' -X PATCH "$D/api/equipo/miembro?id=aldair" -d '{"rol":"Fabricación y ensamble"}' # admin
curl -s -u $U -H 'Content-Type: application/json' -X PATCH "$D/api/equipo/miembro?id=aldair" -d '{"activo":false}' # desactivar (nunca se borra)
# acceso al sitio: crea la línea en el htpasswd (409 si existe; 400 si la contraseña es más corta que PASS_MIN)
# y crea/actualiza al miembro. La contraseña inicial se pasa en persona; la cambia en /cuenta.html.
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/usuarios \
-d '{"usuario":"adriana","password_inicial":"operaciones-2026","nombre":"Adriana","rol":"Operaciones","area":"operaciones","correo":"adriana@helgen.tech"}'
# quitar el acceso: borra la línea y deja al miembro activo=false. Se niega a quitarte a ti mismo (400)
# y a quitar al último administrador con acceso (409).
curl -s -u $U -X DELETE "$D/api/usuario?u=adriana"
curl -s -u $U "$D/api/usuarios" # sigue igual: lista de cuentas del htpasswd (admin)
```
**Pendientes (cambio del 14-oct):** `responsable` es obligatorio al crear (400 si falta). Acepta ids de
miembros separados por coma, espacio o `/` (`"leo, Jesús"` → `responsable_ids: ["leo","jesus"]`; los acentos
no importan) y también texto libre si la persona aún no está dada de alta: se guarda y la respuesta trae
`aviso`. `GET /api/tareas?responsable=jesus` filtra por id (o por texto, para los libres) y la respuesta
incluye `equipo` (id → nombre) para pintar nombres. `GET /api/tareas/resumen` agrega `por_responsable`
`{id: {por-hacer, en-curso, bloqueado, hecho, abiertos, vencidas, nombre}}`; los pendientes sin miembro
registrado cuentan bajo `_sin_equipo`.