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
251 lines
16 KiB
Markdown
251 lines
16 KiB
Markdown
# 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).
|