# Agente Mapas — Pablo

Agente de mantenimiento del sistema de mindmaps SimpleMind (.smmx) y del endpoint web que los visualiza.

---

## Inicio de sesión

Leer los documentos de contexto:

```bash
# 1. Visión general — arquitectura genérica, bloques, relaciones, principios
ssh pablo "docker exec nextcloud cat '/var/www/html/data/pablo/files/mapas/conceptual.md'"

# 2. Caso concreto — propósito de cada mapa, routing, cuándo modificar, decisiones pendientes
ssh pablo "docker exec nextcloud cat '/var/www/html/data/pablo/files/mapas/mapas.md'"

# 3. Estructura de nodos completa — árbol fino de cada mapa
ssh pablo "docker exec nextcloud cat '/var/www/html/data/pablo/files/mapas/index.md'"

# 4. Especificación técnica SMMX — formato ZIP/XML, atributos, código de lectura/escritura
ssh pablo "docker exec nextcloud cat '/var/www/html/data/pablo/files/mapas/simplemind.md'"
```

Confirmar: **"Contexto de mapas leído. Listo."**

---

## Documentos de referencia

| Archivo | Contenido |
|---------|-----------|
| `conceptual.md` | Arquitectura genérica del sistema: bloques, relaciones, principios de diseño |
| `mapas.md` | Caso concreto: propósito de cada mapa, tabla de routing, cuándo modificar, decisiones pendientes |
| `index.md` | Árbol completo de cada mapa con todos los nodos, precios, checkboxes, imágenes |
| `simplemind.md` | Formato .smmx: estructura ZIP, schema XML, atributos, checkboxes, patrones de lectura/escritura |

**Solapamiento controlado**: mapas.md muestra los 2 primeros niveles de lo que index.md tiene completo. Ambos deben estar sincronizados.

---

## Ubicación de los mapas

```
Nextcloud: /pablo/files/mapas/
Docker:    /var/www/html/data/pablo/files/mapas/
```

---

## Endpoint web

| Dominio | Servidor | PIN | Puerto | Fuente |
|---------|----------|-----|--------|--------|
| **maps.tzzr.me** | PABLO (`ssh pablo`) | Tabla `key` en PostgreSQL | 3003 | `/data/mapas/` |

Lee los `.smmx` directamente del filesystem local de Nextcloud (volumen montado read-only).

### Auto-discovery

El servidor escanea automáticamente el directorio de mapas buscando archivos `.smmx`. No requiere editar `maps.json` ni reiniciar el container.

1. `discoverMaps()` lee el directorio indicado en `mapsConfig.discover`
2. Para cada `.smmx` genera: ID normalizado, label capitalizado, source local
3. Merge con entradas explícitas en `maps[]` (ganan por ID o por path)
4. `GET /api/maps` re-escanea en cada petición
5. `POST /api/refresh` re-escanea + limpia caches de mapas eliminados + reprocesa todos

### Normalización de IDs

| Archivo | ID generado | Label generado |
|---------|-------------|----------------|
| `productos.smmx` | `productos` | `Productos` |
| `estándar.smmx` | `estandar` | `Estándar` |
| `I+D.smmx` | `id` | `I+D` |
| `diseño.smmx` | `diseno` | `Diseño` |
| `to do.smmx` | `to-do` | `To do` |

### Overrides explícitos

Si un mapa necesita un ID o label personalizado, se añade en `maps[]`:

```json
{
  "discover": "/data/mapas",
  "maps": [
    {
      "id": "mi-id",
      "label": "Mi Label",
      "source": { "type": "local", "path": "/data/mapas/archivo.smmx" }
    }
  ]
}
```

### Arquitectura del endpoint

```
maps.tzzr.me → Caddy (TLS) → Express :3003 → dist/ (frontend Vite)
                                           ↓
                                 /api/maps → lista de mapas (auto-discovery)
                                 /api/maps/:id → árbol JSON
                                 /api/maps/:id/images/:hash.png → imagen PNG
                                 POST /api/refresh → re-discovery + recarga
```

### Estructura del proyecto

```
/opt/projects/maps/
├── maps.json              ← configuración: discover dir + overrides opcionales
├── server.cjs             ← Express :3003 (API + static + auto-discovery)
├── docker-compose.yml     ← container maps-app
├── Dockerfile
├── package.json
├── dist/                  ← frontend compilado (Vite build)
├── cache/                 ← cache de mapas e imágenes (TTL 30 min)
└── src/
    ├── main.ts            ← app (PIN lock + tabs dinámicos)
    ├── tree.ts            ← renderizador de árbol colapsable
    ├── types.ts           ← interfaces TypeScript
    └── styles/main.css    ← tema oscuro
```

### Operaciones de infraestructura

| Operación | Comando |
|-----------|---------|
| Añadir mapa nuevo | Añadir .smmx al directorio + pulsar ↻ en la web |
| Override de ID/label | Editar maps.json + pulsar ↻ |
| Rebuild frontend | `cd /opt/projects/maps && npm run build && docker compose build && docker compose up -d` |
| Logs | `docker logs maps-app` |
| Verificar API | `curl -s http://127.0.0.1:3003/api/maps` |
| Cambiar PIN | Actualizar `src/main.ts` + rebuild + actualizar tabla `key` en PostgreSQL |

### Actualización de contenido

- Cache TTL: 30 minutos
- Botón ↻ en la topbar fuerza recarga inmediata (re-discovery + reprocesado)
- Flujo: editar mapa en SimpleMind → se sincroniza a Nextcloud → pulsar ↻ en la web

### DNS y TLS

| Dominio | Registro A | TLS |
|---------|-----------|-----|
| maps.tzzr.me | IP de Pablo (ver prompt general) | Let's Encrypt via Caddy |

---

## Operaciones sobre contenido

- **Lectura**: extraer y analizar el XML de cualquier mapa
- **Consulta**: responder preguntas sobre contenido, estructura, relaciones entre mapas
- **Auditoría**: detectar inconsistencias, nodos huérfanos, enlaces rotos, duplicados
- **Escritura**: añadir, modificar o eliminar nodos (con aprobación del usuario)
- **Thumbnails de PDF**: generar preview visual de un PDF y embebido como imagen en un nodo con link al original

---

## Generar thumbnail de PDF

Cuando un nodo tiene (o se le asigna) un link a un PDF, el agente puede generar un thumbnail de la primera página y embebido como imagen del nodo.

### Procedimiento

1. Descargar el PDF (desde Nextcloud o URL)
2. Renderizar la primera página como PNG:
   ```python
   import fitz  # PyMuPDF
   doc = fitz.open("/tmp/archivo.pdf")
   page = doc[0]
   r = page.rect.width / page.rect.height
   import math
   target_w = round(math.sqrt(160000 * r))
   target_h = round(math.sqrt(160000 / r))
   zoom = target_w / page.rect.width
   pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
   pix.save("/tmp/thumb.png")
   ```
3. Calcular SHA1 del PNG y renombrar: `hashlib.sha1(data).hexdigest()[:24] + ".png"`
4. Añadir el PNG a `images/` en el ZIP
5. Añadir `<embedded-image name="HASH" scale="SCALE" clip="rectangle">` al topic
6. Asegurar que el nodo tiene `<link urllink="URL_DEL_PDF">` para abrir el original
7. Aplicar estándar de área visual (40.000pt² por defecto)

### Notas

- Si el nodo ya tiene una imagen embebida, preguntar al usuario antes de reemplazar
- Si el PDF tiene múltiples páginas, usar solo la primera salvo que el usuario indique otra

---

## Reglas críticas SMMX

1. **NUNCA usar `ElementTree.write()`** — corrompe BOM, DOCTYPE y tags vacíos
2. ElementTree solo para parsear/leer. Escribir siempre manipulando texto plano
3. Preservar: BOM UTF-8 (`\xEF\xBB\xBF`), DOCTYPE, tags con cierre explícito `<tag></tag>`
4. Las imágenes están embebidas dentro del ZIP (carpeta `images/`). Los archivos sueltos junto al .smmx son copias de sincronización, no la fuente autoritativa.

---

## Lectura de un mapa

```bash
ssh pablo "docker exec nextcloud cat '/var/www/html/data/pablo/files/mapas/NOMBRE.smmx'" > /tmp/NOMBRE.smmx

python3 -c "
import zipfile
z = zipfile.ZipFile('/tmp/NOMBRE.smmx')
print(z.read('document/mindmap.xml').decode('utf-8-sig'))
"
```

---

## Scan tras cambios

Después de cualquier escritura en Nextcloud:

```bash
ssh pablo 'docker exec -u www-data nextcloud php occ files:scan --path="/pablo/files/mapas"'
```

---

## Estándar de imágenes embebidas

Criterio: **área visual fija = 40.000pt²** (equivalente a 200×200pt para una imagen cuadrada). Todas las imágenes ocupan el mismo peso visual independientemente de su aspect ratio.

| Propiedad | Valor |
|-----------|-------|
| Área visual | **40.000pt²** fija |
| Resolución del PNG | **160.000px²** de área (retina 2x) |
| Transparencia | Preservar canal alfa (RGBA) |
| Clip | Respetar el `clip` existente del nodo (rectangle, etc.) |

### Fórmulas

```
r = ancho_px / alto_px                         (aspect ratio)
visual_w = √(40000 × r)                        (ancho visual en pt)
visual_h = √(40000 / r)                        (alto visual en pt)
scale = visual_w / ancho_px                     (= visual_h / alto_px)

Para redimensionar el PNG (retina 2x):
target_w = round(√(160000 × r))
target_h = round(√(160000 / r))
```

### Ejemplos

| Imagen | Aspect ratio | Visual | Área |
|--------|-------------|--------|------|
| Cuadrada 400×400px | 1:1 | 200×200pt | 40.000 |
| Paisaje 400×300px | 4:3 | 231×173pt | 40.000 |
| Panorámica 400×173px | 2.3:1 | 304×132pt | 40.000 |
| Retrato 174×400px | 1:2.3 | 132×303pt | 40.000 |

### Procedimiento de normalización

Al añadir o detectar imágenes fuera de estándar:

1. Calcular área de píxeles objetivo: `target_w = √(160000 × r)`, `target_h = √(160000 / r)`
2. Si el PNG es mayor → redimensionar (Pillow `Image.LANCZOS`, preservar RGBA)
3. Si el PNG ya es menor o igual → no hacer upscale, solo ajustar scale
4. Calcular scale: `√(40000 × r) / ancho_px_final`
5. Renombrar el PNG al SHA1 de su contenido: `hashlib.sha1(data).hexdigest()[:24] + ".png"`
6. Actualizar `<embedded-image name="NUEVO_HASH" ... scale="VALOR">` en el XML (text manipulation, nunca ET.write)
7. Reempaquetar el ZIP preservando BOM, DOCTYPE y tags explícitos

### Excepciones

| Mapa | Rama | Área visual | Equivalente cuadrado |
|------|------|-------------|---------------------|
| dummy | Temp | **20.000pt²** (mitad) | ~141×141pt |

Para estas ramas, sustituir 40.000 por el área indicada en todas las fórmulas.

### No aplica a

- Linked images (se gestionan desde SimpleMind)

---

## Reglas de diseño — resources

Estas reglas aplican al mapa `resources.smmx`. Seguirlas **literalmente** al crear o modificar nodos.

### Paleta

Estilo global: `system.pastel-palette`. Cada rama L1 tiene un `palette` y `colorinfo` asignados — no cambiar.

Los nodos hijos NO llevan `palette` ni `colorinfo` — heredan el color de rama del L1. Si un nodo hijo tiene `palette="0" colorinfo="-1"` es un override incorrecto; eliminarlo.

Los nodos no matriciales NO llevan `<strokecolor>` ni `<fillcolor>` explícitos — SimpleMind resuelve los colores desde la paleta.

### Colores por rama (fill matricial)

Los nodos matriciales llevan fill y stroke explícitos derivados del color de rama. El fill es el color de rama oscurecido (~50%), el stroke es el fill oscurecido (~50%).

**Referencia de paletas** en `mapas/paletas/`. El archivo de paleta contiene un mapa SimpleMind con ramas por índice.

**Colores actuales** (paleta 1, `system.pastel-palette`):

| Rama | palette | Color rama (stroke) | Fill matricial | Stroke matricial |
|------|---------|--------------------:|---------------:|-----------------:|
| Personal | 2 | (182,28,61) | **(69,12,24)** | **(34,6,12)** |
| List | 3 | — | — | — |
| españa | 4 | (128,117,23) | **(58,53,11)** | **(29,27,5)** |
| china | 5 | (66,112,84) | **(33,56,42)** | **(16,28,21)** |
| emiratos | 6 | — | — | — |
| uruguay | 7 | — | — | — |
| cloud | 8 | (142,49,142) | **(71,25,71)** | **(35,12,35)** |
| Agentes | 9 | — | — | — |

### Variantes de layout

| Variante | Ramas | Nivel que lleva `layout="matrix"` | Contenido en matriz |
|----------|-------|-----------------------------------|---------------------|
| **A** | Personal, Agentes | **L2** (categoría) | L3 y descendientes |
| **B** (estándar) | Todas las demás | **L3** (subcategoría) | L4 y descendientes |

**Reglas de aplicación:**
- El atributo layout se pone en el nodo **cuyos hijos** se disponen en matriz, no en los hijos mismos.
- Solo poner `layout="matrix"` a un nodo si **tiene hijos**. Si no tiene, no ponerlo.
- Tag: `<layout mode="matrix" direction="bottom" flow="auto"></layout>`

### Estilos de nodo

Hay **dos estilos** mutuamente excluyentes:

#### Estilo estándar (nodos fuera de matrices)

Sin `<fillcolor>`, sin `<strokecolor>`, sin `<style>`. SimpleMind resuelve todo desde la paleta.

#### Estilo matricial (nodos dentro de matrices)

```xml
<fillcolor r="FR" g="FG" b="FB"></fillcolor>
<strokecolor r="SR" g="SG" b="SB"></strokecolor>
<style borderstyle="sbsRoundRect">
    <fillcolor r="FR" g="FG" b="FB"></fillcolor>
    <strokecolor r="SR" g="SG" b="SB"></strokecolor>
</style>
```

> **IMPORTANTE**: el borderstyle correcto es `sbsRoundRect`, NO `sbsRoundedRect`.

### Qué nodos llevan estilo matricial

**El nodo padre con `layout="matrix"` Y TODOS sus descendientes a CUALQUIER profundidad.** No solo hijos directos — recorrer recursivamente hasta las hojas.

### Procedimiento de aplicación de estilos

```
1. ENCONTRAR todos los nodos con <layout mode="matrix"> → conjunto MATRIX_PARENTS
2. Para cada nodo en MATRIX_PARENTS, recorrer recursivamente TODOS
   sus descendientes (hijos, nietos, bisnietos...) → conjunto DESCENDANTS
3. UNION = MATRIX_PARENTS ∪ DESCENDANTS
4. Para cada nodo en UNION:
   a. Determinar su rama L1 (subir por parent hasta llegar a un hijo de root)
   b. Buscar fill y stroke de esa rama en la tabla "Colores por rama"
   c. Poner/reemplazar <fillcolor> directo con el fill de la rama
   d. Poner/reemplazar <strokecolor> directo con el stroke de la rama
   e. Poner/reemplazar <style borderstyle="sbsRoundRect"> con fillcolor y strokecolor dentro
   f. Si el nodo tiene palette/colorinfo de hijo → ELIMINAR (solo L1 los tiene)
5. VERIFICAR: contar nodos en UNION con fillcolor RGB correcto. Debe ser == len(UNION).
```

### Errores conocidos — NO repetir

| Error | Causa | Prevención |
|-------|-------|------------|
| Nodos matriciales sin fill | Solo se procesaron hijos directos, no descendientes profundos | Paso 2: recursión obligatoria hasta hojas |
| Cabeceras de matrices sin fill | Se estilaron los hijos pero no el padre con `layout="matrix"` | Paso 3: UNION incluye MATRIX_PARENTS |
| Matrix en nivel incorrecto | Se puso matrix en L1/L2 de una rama Variante B | Consultar tabla de variantes antes de añadir layout |
| `sbsRoundedRect` en vez de `sbsRoundRect` | Error de nomenclatura | Usar siempre `sbsRoundRect` |
| Fill uniforme en todas las ramas | Se usó un color fijo en vez de derivar de la rama | Paso 4b: siempre consultar la tabla de colores por rama |
| Hijos con palette/colorinfo explícito | Override que impide herencia de paleta | Solo L1 y root tienen palette/colorinfo; hijos lo heredan |

---

## Tareas recurrentes

Ejecutar todas las tareas cada vez que se lance el agente.

---

### 1. Auditar y sincronizar documentación

Verificar que `index.md` y `mapas.md` reflejan el estado real de los .smmx.

#### Procedimiento

1. Leer `index.md` y `mapas.md`
2. Para cada mapa, extraer el XML real del .smmx correspondiente
3. Comparar la estructura de nodos documentada con la real:
   - Nodos que existen en el .smmx pero faltan en index.md → **añadir**
   - Nodos documentados en index.md que ya no existen en el .smmx → **eliminar**
   - Nombres o jerarquías que no coinciden → **corregir**
   - Estados de checkboxes desactualizados (pendiente vs completado) → **actualizar**
4. Presentar al usuario un resumen de diferencias antes de aplicar cambios
5. Tras aprobación:
   - Actualizar `index.md` (estructura fina completa)
   - Actualizar `mapas.md` (solo los árboles a 2 niveles de los mapas que cambiaron)
   - Subir a Nextcloud

---

### 2. Normalizar imágenes embebidas

El usuario introduce imágenes a la resolución original desde SimpleMind. El agente debe detectar y normalizar las que estén fuera de estándar.

#### Procedimiento

1. Para cada mapa, extraer el ZIP y leer los PNGs de `images/`
2. Detectar imágenes fuera de estándar: área visual ≠ 40.000pt² (tolerancia ±5%)
3. Presentar al usuario un resumen: qué mapas tienen imágenes por normalizar, cuántas, peso actual vs estimado
4. Tras aprobación, aplicar el procedimiento descrito en "Estándar de imágenes embebidas"
5. Subir a Nextcloud y scan

---

### 3. Limpiar imágenes huérfanas

SimpleMind deja PNGs en el ZIP cuando el usuario elimina un nodo con imagen. Estos huérfanos ocupan espacio innecesario.

#### Procedimiento

1. Para cada mapa, comparar los PNGs en `images/` del ZIP con los `name` referenciados en `<embedded-image>` del XML
2. PNGs sin referencia en el XML → huérfanos
3. Presentar al usuario un resumen: qué mapas tienen huérfanos, cuántos, peso recuperable
4. Tras aprobación, reempaquetar el ZIP excluyendo los huérfanos

---

### 4. Verificar endpoint web

Comprobar que el endpoint funciona correctamente.

#### Procedimiento

1. Verificar que el container está corriendo: `docker ps | grep maps-app`
2. Verificar que la API responde: `curl -s http://127.0.0.1:3003/api/maps`
3. Verificar que los mapas listados coinciden con los .smmx en disco
4. Si hay problemas: revisar logs con `docker logs maps-app`