# H2A Studio — pendientes, con dueño y bloqueo

Estado al 27 de agosto de 2026. `main` de `DCEO-CO/h2a-studio` en **`3c92734`**.
Producción **no** tiene todavía nada de lo de abajo.

Todo lo de este documento está verificado contra el código o contra el sitio en
vivo; donde no lo pude comprobar, lo dice.

---

## A. Listo y verificado — solo falta desplegar

| # | Qué | Dónde está | Bloqueo | Dueño |
|---|---|---|---|---|
| A1 | **Favicon del Studio** — 2 PNG + 3 líneas en `base.html` | `main` @ `3c92734` | correr `./deploy.sh` en el VPS | JP o Sergio |
| A2 | **Login: el error de credenciales no se veía** | rama `andres/login-error-htmx` @ `d3558b8` | revisión de Sergio → merge a `main` → deploy | Sergio |

**A1.** El Studio no tenía favicon. Van dos versiones porque la silueta es
monocroma: negro para pestaña clara (21,0:1) y blanco para oscura (12,1:1); con
uno solo, la mitad de los casos queda invisible. Verificado en navegador: los dos
archivos dan 200 y el navegador elige el correcto por tema. **No** arranqué la
app completa (esta máquina no tiene `.venv`, `.env`, Docker ni Python).

**A2.** Con clave equivocada, el botón no hacía nada. Causa: el servidor mandaba
el error con status **401**, y htmx 2.0.4 descarta cualquier 4xx sin pintarlo
(`responseHandling:[…{code:"[45]..",swap:false}]`). Debajo había un segundo
defecto tapado por el primero: el error devolvía la **página completa** sobre un
`hx-swap="outerHTML"`, así que al arreglar solo el status quedaba el login
anidado dentro del login. Los dos arreglados juntos. Reproduje el bug y verifiqué
el arreglo con el parcial real del repo.

Va en rama y **no** en `main` a propósito: `pages.py` y `login.html` son del lado
de Sergio y es su login el que se toca.

---

## B. Decisiones tuyas — no bloquean el despliegue

| # | Qué | Por qué importa |
|---|---|---|
| B1 | **Google Drive escribiendo dentro de `.git`** | Rompió `git fetch` con `bad object refs/desktop.ini`. Limpié los 70 archivos, pero Drive los vuelve a sembrar. Recomendación: sacar el repo de `C:\Users\schmo\H2A` (su respaldo real es GitHub) y dejar la sincronización para los documentos, que no tienen otra copia. |
| B2 | `desktop.ini` al `.gitignore` | El repo ya ignora `.DS_Store` y `Thumbs.db`; le falta este por coherencia. **No** arregla B1: el ignore no aplica dentro de `.git`. |
| B3 | **El isotipo a 16px no se reconoce** | 243 de 256 píxeles son opacos: la muesca desaparece y en la pestaña se lee como un cuadrado. El contraste quedó resuelto; la legibilidad de la marca a ese tamaño, no. Pide un asset pensado para 16px. Dueño: quien lleva identidad. |
| B4 | Favicon en las páginas públicas | Informes (`/r/…`), propuestas, reportes de cursos y share de redes siguen sin favicon. Llevan el color del cliente, no el de H2A: es decisión de marca, no técnica. |

---

## C. Trabajo de Juan Pablo o Sergio

No son autorizaciones abstractas: son dos desarrollos que caen en su lado del
código —`models.py`, el servicio de publicación y el módulo de clientes— más el
acceso al servidor.

### C1 · Cierre de informes — lo urgente

**Hoy un informe finalizado se puede pisar en silencio.** `InformeRedes.estado`
(`models.py:3271`) solo conoce `borrador | publicado | retirado`: ninguno
significa "cerrado". Cualquier llave de escritura republica sobre el mismo slug y
el cliente ve otra cosa en el mismo enlace, sin rastro de que alguien tocó un
corte cerrado.

Los cinco puntos de la especificación (`proceso-de-cierre-de-informes`, en el
cerebro de Natura · Consultoría de Belleza), con lo que ya existe en el código:

| | Qué | Dónde | Qué hay ya |
|---|---|---|---|
| 1 | Estado por informe, con fecha y responsable | `models.py` | la columna `estado` existe; faltan el valor `finalizado` y las columnas `finalizado_at` / `finalizado_por_user_id` |
| 2 | Botón «Finalizar informe» en la ficha | `informes.py:371` + `templates/informes/ficha.html` | `POST /app/social/{cliente}/{informe}/estado` ya existe y ya recibe `estado`: es un tercer valor en el mismo sitio |
| 3 | **Que no se pueda republicar sobre un finalizado** | `services/informes_redes.py::publicar` | nada — es el punto que falta de verdad |
| 4 | Botón «Desbloquear para editar» que registre quién autorizó | mismo endpoint del 2 | `restaurar` (`informes.py:388`) ya publica como versión nueva sin retroceder el contador; falta el registro de la autorización |
| 5 | Que el historial muestre qué versiones están congeladas | `templates/informes/ficha.html` | la ficha ya lista todas las versiones; falta marcarlas |

**Sin el 3 los otros cuatro son decorativos.**

**Corrección al plan.** Yo tenía escrito que el candado iba en `preparar_informe`.
Lo revisé en el código y ahí está mal: `preparar_informe` (`mcp_clientes.py:1132`)
no publica nada, entrega un permiso de subida de un solo uso que dura 24 horas.
Cerrar solo esa puerta deja abiertas las demás y deja una ventana de un día entre
el permiso y la subida.

Todas las publicaciones pasan por **una sola función**: `publicar()` en
`services/informes_redes.py:161`, con cinco llamadores —

```
informes.py:320          subir un informe desde la app (persona)
informes.py:403          restaurar una versión anterior
informes_publico.py:332  la subida por /u/{token}
mcp_clientes.py:1028     publicar_informe (herramienta MCP)
mcp_clientes.py:1086     publicar_propuesta
```

— así que el candado va ahí, donde ya se resuelve el slug y ya se decide si nace
un informe o se le agrega versión. Dos avisos: `publicar_propuesta` usa la misma
función (el chequeo debe mirar el estado del informe, no bloquear por tipo), y
conviene además un rechazo temprano en `preparar_informe` para que el agente no
arme el informe entero y se estrelle al final.

### C2 · Clientes → proyectos

**Lo que hay hoy.** Todo cuelga del cliente en paralelo y al mismo nivel: el
cerebro (`KnowledgeCollection` → `KnowledgeDoc` → `KnowledgeAsset`), los NITs,
las métricas (`SocialPeriod`), los informes (`InformeRedes` + versiones), las
llaves MCP (`ClientAgentToken`) y las solicitudes (`WorkRequest`). **No existe
ninguna entidad de proyecto.** Lo confirmé en `models.py`: "proyecto" solo
aparece como `Contract.kind='project'`, como texto libre en
`EquipmentCheckout.project_name`, y en el docstring de `Course`.

**El mecanismo ya existe y está probado, pero solo para cursos.**
`KnowledgeCollection` tiene cinco ámbitos, y uno es literalmente un eje de
proyecto. Del docstring del modelo:

> `course_id=X` → el cerebro de ESE curso (insumos, guion, decisiones)
> *"TX-123 añade dos ámbitos más, para que el conocimiento de un ÁREA y de un
> PROYECTO no se derrame al cerebro general."*

Y `_ambito()` en `services/knowledge_base.py` es **el único punto** que decide
qué ve cada cerebro. O sea: replicar algo que ya funciona, no arquitectura nueva.

**Lo caro no es el código**, es migrar lo que ya está guardado sin ámbito de
proyecto y decidir qué pasa con el contrato de las herramientas del MCP de
clientes. Y toca `models.py` —hotspot de merge— y todo el módulo de JP.

**Decisión pendiente de ellos:** lo construyen ustedes, o lo escribo yo con JP
revisando el diseño antes de tocar `models.py`.

### C3 · Acceso de despliegue para Andrés

Hoy no alcanzo el VPS: no hay ninguna llave en `~/.ssh` y el intento devuelve
`Permission denied (publickey,password)`. Tampoco hay `gh`. Cada despliegue
depende de que JP o Sergio corran el script. Es el mismo pedido que ya está en
`MENSAJE-JUAN-PABLO.md` para el sitio.

---

## D. Abierto en el trabajo de clientes — no bloquea el despliegue

Lo anoto porque lo verifiqué de paso y si no queda escrito se pierde.

| # | Qué |
|---|---|
| D1 | **El informe publicado de Néstor Ochoa va de febrero a junio, no incluye julio**, aunque el título diga "Julio 2026". El objeto de datos solo tiene `2026-02` a `2026-06`. |
| D2 | **La v2 nunca se publicó.** El cerebro describe un HTML de 9 pestañas con corte julio y la pestaña IG ↔ FB; el cliente está viendo la v1, de 6 pestañas. |
| D3 | **Hallazgos y Plan del informe publicado están escritos a mano y contradicen el análisis posterior**: afirman que junio no tiene datos de Facebook (sí los tiene), atribuyen todo a la constancia editorial cuando la variable fue la pauta, y miden contra metas de ER 5% / 200 seguidores que no son las del brief (10% / 20.000 al año). |
| D4 | **El §11 del documento del cerebro está obsoleto**: dice que el informe está pendiente de publicar por un 403. El informe está publicado (v1, 5 vistas), y CLAUDE.md explica que ese 403 venía del proxy del propio agente, no del servidor — y que ya existe la puerta `GET /u/{token}` para hacer clic. Mientras no se corrija, el próximo agente va a creer que hay trabajo sin publicar. |
