hbox-docs/backend/README.md
Leopoldo Rodríguez 4cc18b517f Cuentas en bloque, contraseña inicial y sitio/correo en Equipo
backend/cuentas.py: restablece en bloque (todos menos leo y claude) y da de alta
a varios desde backend/cuentas/2026-10-23_alta-sitios.json (jesush, miguel, joel,
antonio, danielg); la contraseña la pide en la terminal. Miembros con sitio,
correo y correo_alt; marca contrasena_inicial con aviso en el sitio hasta que la
persona la cambie. Los ids de responsable se recalculan con el equipo de hoy.
Lote 2026-10-23_usuarios-campo. Pruebas: tests-cuentas.sh (47).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
2026-10-23 14:14:50 -06:00

251 lines
16 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.
**Por sitio (23-oct).** Cada movimiento lleva `sitio`: `icave`, `timsa`, `eit`, `lzc` o `qro` (acepta `LCT`, `Ensenada`,
`Manzanillo`…; vacío = `qro`). Los de antes no tienen sitio y cuentan como Querétaro. Dos cuentas por movimiento: `delta`
mueve el **total** del ítem (`stock`, incluye lo que va en camino) y `delta_sitio` lo que hay **en ese sitio**; `ajuste` fija
el conteo de ese sitio. `envio` (con `destino`) sale del sitio y queda `"estado": "en-camino"` sin cambiar el total; con
`"sitio": "fuera"` no descuenta de nadie y sube el total (una compra en camino, o material que salió antes de contarlo).
`recepcion` (con `envio_id`; `cantidad` por omisión = lo enviado, nunca más) lo pone en el destino y cierra el envío; si
llegó menos, la diferencia baja del total. Salida o envío sin existencia en ese sitio → 409 «No hay stock suficiente en …».
`GET /api/inventario` trae en cada ítem `por_sitio` (`{"qro": 7, "eit": 2}`), `en_camino` (`{"eit": 3}`) y
`por_instalacion` (cuánto lleva una unidad instalada; 0 = no es del kit), y además `sitios` y `envios` (los abiertos).
Pruebas: `bash backend/tests-1023.sh .`
```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"}'
# por sitio (23-oct): entrada en TIMSA, envío TIMSA → EIT (queda en camino) y su recepción en EIT
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario/movimiento \
-d '{"item_id":"GAB-IP67","tipo":"entrada","cantidad":4,"sitio":"timsa","motivo":"llegó pedido","referencia":"guía 1234"}'
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario/movimiento \
-d '{"item_id":"GAB-IP67","tipo":"envio","cantidad":2,"sitio":"timsa","destino":"eit","motivo":"kit EIT","referencia":"guía 5678"}'
curl -s -u $U -H 'Content-Type: application/json' -X POST $D/api/inventario/movimiento \
-d '{"tipo":"recepcion","envio_id":"M261023-111500-ab12","cantidad":2}' # el id sale de "envios" en GET /api/inventario
curl -s -u $U "$D/api/inventario/movimientos?sitio=eit" # lo que pasó en EIT y lo que va hacia EIT
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, `sitio` (icave·timsa·eit·lzc·qro o vacío; 23-oct),
`correo`, `correo_alt` (el del otro dominio; 23-oct), `telefono`, `activo` y `contrasena_inicial` (23-oct: la contraseña
la puso alguien más; esta marca solo la ve un administrador).
```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`.
**Desde el 23-oct** los ids se recalculan del texto con el equipo de ese momento cada vez que se leen (`GET /api/tareas`,
`/api/tareas/resumen`, Equipo): si alguien estaba como texto libre («Miguel») y se da de alta como `miguel`, sus
pendientes ya cuentan como suyos sin volver a guardarlos. Nunca quita a nadie: los miembros no se borran.
### Cuentas en bloque: `cuentas.py` (23-oct)
Para restablecer contraseñas de muchos o dar de alta a varios de una vez, en el servidor y como `leo`:
```bash
cd /srv/projects/drogheda/hbox-docs/backend
venv/bin/python3 cuentas.py lista # cuentas, nombre, sitio, correo y si sigue con la inicial
venv/bin/python3 cuentas.py todo cuentas/2026-10-23_alta-sitios.json # altas + datos + todos a la misma contraseña (menos leo y claude)
venv/bin/python3 cuentas.py alta cuentas/OTRO.json # solo altas y datos
venv/bin/python3 cuentas.py restablecer --solo ana,beto # solo a esos
```
La contraseña la pide el script (dos veces, sin eco); nunca va en un archivo ni como argumento. Muestra el plan y pide
«si»; deja un respaldo del htpasswd en `equipo/_respaldos/` (modo 600). Las cuentas sin miembro activo no se tocan (se
listan para decidir si se quitan). A quien se le restablece o se le da de alta le queda `contrasena_inicial: true` en su
miembro: `/api/whoami` la devuelve, `docs/js/sitio.js` muestra una franja con la liga a Mi cuenta y Equipo la marca
(solo admin). Se quita sola cuando la persona cambia su contraseña (`POST /api/password` propio); un restablecimiento
desde Mi cuenta (admin) o desde `/api/usuarios` la vuelve a poner. En el archivo de altas, los datos de quien ya existe
solo se llenan si están vacíos. Pruebas: `bash backend/tests-cuentas.sh .` (47).