Files
fn_registry/dev/issues/0172-osint-web-graph-explorer.md
T
egutierrez eb8dbf66a1 feat(infra): auto-commit con 88 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 00:16:46 +02:00

185 lines
10 KiB
Markdown

---
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/<tipo>/<slug>/`,
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/<persona con fotos>` + abrir NodeCard | frontmatter + cuerpo + galería con las imágenes de `attachments/personas/<slug>/` |
| 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.