---
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.
