--- id: "0172" title: "App web OSINT: grafo sigma.js + tablas por tipo + fichas con imágenes sobre el vault osint" status: pendiente type: app domain: - osint - frontend scope: app-scoped priority: media depends: [] blocks: [] related: ["0171"] created: 2026-06-10 updated: 2026-06-10 tags: [osint, web, sigma, graph, mantine, obsidian, vault, dashboard] --- # 0172 — App web OSINT: grafo sigma.js + tablas por tipo + fichas con imágenes ## APP Metadata | Campo | Valor | |-------|-------| | **ID** | 0172 | | **Estado** | pendiente (solo plan — se construye cuando el vault tenga más datos) | | **Prioridad** | media | | **Tipo** | app — nueva app web en `projects/osint/apps/osint_web` | | **Project** | osint (`projects/osint/`) | ## Contexto El project `osint` guarda sus investigaciones en el vault de Obsidian `/home/enmanuel/Obsidian/osint` (sub-repo `dataforge/osint`). Hoy ese vault tiene: - **~82 nodos** repartidos en carpetas tipadas: `personas/` (45), `organizaciones/` (25), `lugares/` (10), `dominios/` (1), `casos/` (1). - **Datos tabulares** en el frontmatter YAML de cada ficha: `tipo`, `nombre`, `sexo`, `fecha_nacimiento`, `dni`, `direccion`, `pais`, `aliases`, `tags`, etc. - **Aristas implícitas**: los wikilinks `[[...]]` en las secciones `Relaciones`, `Lugares` y `Documentos` conectan unas fichas con otras (y con sus attachments). - **~240 attachments**: fotos, DNIs, certificados y PDFs en `attachments///`, embebidos en las notas con `![[...]]`. Obsidian es bueno para *escribir* la investigación, pero malo para *explorarla* de un vistazo: no da un grafo navegable de todos los objetivos, ni una tabla filtrable, ni una ficha-resumen con la galería de imágenes de cada persona. Metabase/Grafana no encajan: leen BD SQL (no `.md`), y no muestran ni grafo de nodos ni imágenes inline. Decisión del usuario (10/06/2026): construir una **app web propia** que lea el vault y ofrezca tres vistas — **grafo explorable con sigma.js**, **tablas filtradas por tipo**, y **fichas con imágenes**. Este issue es **solo el plan**: la recopilación de datos en Obsidian continúa primero; la app se implementa cuando haya suficiente material que justifique la inversión. ## Objetivo Una app web local que, leyendo directamente los `.md` del vault `osint` (sin BD intermedia obligatoria en v1), permita: 1. **Explorar el grafo** de nodos (personas, organizaciones, lugares, dominios, casos) y sus conexiones por wikilinks, con sigma.js: zoom, pan, click en nodo → ficha, colores por tipo, filtro de tipos visibles, búsqueda de nodo. 2. **Ver tablas filtradas por tipo**: una tabla por categoría (personas, organizaciones, ...) con las columnas del frontmatter, ordenable y filtrable (por dni, lugar, fecha, tag). 3. **Abrir la ficha** de cualquier nodo: frontmatter renderizado + cuerpo Markdown + galería de sus attachments (fotos, DNIs, PDFs) servidos por el backend. ## Arquitectura propuesta ``` projects/osint/apps/osint_web/ (sub-repo Gitea dataforge/osint_web) app.md frontmatter de registro (framework: react-vite-mantine) server/ backend Python (lee el vault, sirve JSON + attachments) main.py FastAPI o stdlib http frontend/ React + Vite + Mantine + sigma.js src/ views/GraphView.tsx sigma.js + graphology views/TablesView.tsx Mantine DataTable filtrable por tipo views/NodeCard.tsx ficha + galería de attachments ``` ### Backend (Python — máximo reuso del grupo `obsidian`) Python porque el grupo de capacidad `obsidian` (11 funciones, dominio `obsidian`) ya cubre casi todo el parseo del vault. **Registry-first**: el backend orquesta estas funciones, no reimplementa el parseo. Funciones del registry a reutilizar: | Función | Uso en la app | |---|---| | `list_obsidian_notes_py_obsidian` | enumerar nodos por carpeta/tipo | | `read_obsidian_note_py_obsidian` | leer ficha: `{frontmatter, body, wikilinks, tags}` | | `parse_obsidian_frontmatter_py_obsidian` | datos tabulares de cada nodo | | `extract_obsidian_wikilinks_py_obsidian` | aristas del grafo | | `extract_obsidian_embeds_py_obsidian` | attachments embebidos en cada nota | | `resolve_obsidian_embed_py_obsidian` | resolver `![[foto.jpg]]` → path real en disco para servir la imagen | | `slugify_obsidian_name_py_obsidian` | normalizar nombre de wikilink → id de nodo | | `search_obsidian_notes_py_obsidian` | búsqueda global en el grafo | Funciones **nuevas** a delegar a `fn-constructor` (no escribir inline en la app): - `build_obsidian_graph_py_obsidian` (impure) — dado `vault_dir`, devuelve `{"nodes": [{id, tipo, label, frontmatter}], "edges": [{source, target, kind}]}`. Resuelve cada wikilink a un nodo existente (vía slug / nombre de archivo); los wikilinks que no resuelven a un `.md` del vault se marcan como aristas "dangling" o se descartan según flag. Tag de grupo: `obsidian`. Es la pieza que el grupo declara como frontera no cubierta ("No indexa el grafo agregado") — esta función la cierra. Endpoints HTTP (JSON salvo el de attachments): | Método | Ruta | Devuelve | |---|---|---| | GET | `/api/graph` | grafo completo `{nodes, edges}` para sigma.js | | GET | `/api/nodes?tipo=persona` | filas de la tabla de ese tipo (frontmatter aplanado) | | GET | `/api/node/{slug}` | ficha: frontmatter + body (HTML/markdown) + lista de attachments | | GET | `/api/attachment?path=...` | sirve el binario del attachment (image/pdf), con allowlist al vault | | GET | `/api/search?q=...` | nodos que matchean | Seguridad: el backend solo sirve archivos **dentro** del vault osint (path traversal bloqueado). El vault contiene datos personales sensibles (DNIs) → la app escucha **solo en `127.0.0.1`**, sin exponer a red. No es un service desplegable a VPS. ### Frontend (React + Vite + Mantine + sigma.js) - Sistema del registry: React + Vite + Mantine v9 + `@fn_library` (grupo `mantine`, 63 funciones). Componentes propios de `@fn_library` antes que HTML nativo (regla `frontend_theming.md`). - **Grafo**: `sigma.js` + `graphology`. Color por `tipo`, tamaño por grado, layout force-directed (graphology-layout-forceatlas2). Click en nodo → abre `NodeCard`. Panel lateral con toggles de tipos visibles y caja de búsqueda. - **Tablas**: una pestaña por tipo, Mantine `Table`/DataTable con columnas del frontmatter, orden y filtro por columna (dni, lugar, fecha_nacimiento, tags). - **Fichas**: `NodeCard` con frontmatter en formato clave-valor (fechas en formato europeo DD/MM/AAAA — memoria `formato-fecha-europeo`), cuerpo Markdown, y galería de attachments (imágenes con lightbox; PDFs como enlace/embed). `sigma.js` y `graphology` son dependencias nuevas del frontend (no en `@fn_library`). KISS: añadir solo esas dos; el resto (tabla, layout, modales) sale de Mantine/`@fn_library`. ## Decisiones abiertas 1. **¿BD intermedia o lectura directa del vault?** v1 lee el vault en cada arranque (cachea el grafo en memoria). Si el vault crece mucho o se quiere histórico/diff, evaluar un `operations.db` con `entities`/`relations` (encaja con el bucle reactivo). Recomendado: empezar sin BD (KISS), añadirla solo si el rendimiento o un caso de uso lo exige. 2. **Backend FastAPI vs stdlib http**: FastAPI da validación y OpenAPI gratis; stdlib evita una dependencia. Como el backend es fino (orquesta funciones del registry), decidir al construir. 3. **Live-reload del vault**: ¿re-escanear bajo demanda (botón "refrescar") o watcher de filesystem? v1: botón refrescar (simple). Watcher si molesta. 4. **Aristas dangling**: wikilinks a notas que aún no existen — ¿mostrarlos como nodos fantasma (útil para ver "objetivos pendientes de fichar") o esconderlos? Propuesta: nodo fantasma con estilo atenuado, toggle para ocultar. ## Definition of Done | Escenario | Tipo | Comando / evidencia | Resultado esperado | |---|---|---|---| | Golden: grafo carga el vault | e2e | `GET /api/graph` con el vault osint real | `nodes` ≥ nº de `.md`, `edges` con los wikilinks resueltos; sigma.js los pinta | | Golden: ficha con imágenes | e2e | `GET /api/node/` + abrir NodeCard | frontmatter + cuerpo + galería con las imágenes de `attachments/personas//` | | Edge: tabla filtrada por tipo | e2e | `GET /api/nodes?tipo=organizacion` | solo nodos de ese tipo, columnas del frontmatter | | Edge: wikilink dangling | unit | nota con `[[Persona-Inexistente]]` | arista marcada dangling / nodo fantasma, sin crash | | Edge: nombre con mayúsculas/acentos | unit | wikilink `[[María del Mar]]` → slug | resuelve a `maria-del-mar-...md` vía `slugify_obsidian_name` | | Error: path traversal en attachment | e2e | `GET /api/attachment?path=../../etc/passwd` | 403/404, jamás sirve fuera del vault | | Error: vault inexistente | e2e | arrancar con `--vault /no/existe` | error claro al arrancar, no 500 silencioso | | Cobertura | audit | `uses_functions` del `app.md` | declara todas las funciones del grupo `obsidian` consumidas | Vida útil (cuando se construya): usar la app de verdad sobre el vault osint durante ≥7 días en investigaciones reales; medir que el grafo sigue cargando sin romperse al crecer el vault. ## Notas **Estado actual: solo plan.** No construir todavía — la recopilación de datos en Obsidian continúa; cuando el vault tenga masa crítica de objetivos/relaciones, se arranca con `/new-cpp-app` no aplica (es web): se hace `git init` del sub-repo `dataforge/osint_web` dentro de `projects/osint/apps/osint_web/` antes de limpiar cualquier worktree (regla `apps_subrepo.md`), scaffolding de frontend con el stack Mantine del registry, y backend Python orquestando el grupo `obsidian`. Onboarding (para cuando exista): arrancar backend `python server/main.py --vault /home/enmanuel/Obsidian/osint --port 8470` y `pnpm dev` en `frontend/`; abrir `http://127.0.0.1:5173`. Pestañas: Grafo / Tablas / (ficha al click). Solo localhost por los datos sensibles del vault. Relación con #0171 (manifest de sub-repos): cuando esta app exista será un hijo del project `osint` y debe entrar en su `subrepos.yaml` para re-clonarse en otros PCs.