Files
osint_web/app.md
T
egutierrez 43889bfc07 feat(contacts): CRUD de contactos (vault .md fuente de verdad + reflejo vCard)
Añade alta, edición y borrado de contactos (personas y organizaciones) a la
app osint_web. La fuente de verdad es la ficha .md del vault Obsidian
(CONVENTIONS.md §3b/§6); Xandikos es el retransmisor al móvil.

Backend (server/main.py):
- POST /api/contact: genera slug, escribe la ficha .md con el frontmatter
  canónico + PUT del vCard a Xandikos. 409 si el slug ya existe.
- PUT /api/contact/{slug}: merge del frontmatter (preserva campos heredados)
  + re-PUT del vCard. 404 si no existe.
- DELETE /api/contact/{slug}: borra la ficha .md + DELETE del vCard. 404 si
  no existe.
Cada escritura invalida la caché DAV para que el cambio se vea ya en la app.
Registry-first: orquesta create/update/delete_obsidian_note del grupo obsidian
y carddav_put_vcard/dav_delete_resource del grupo dav (sin reimplementar
parseo ni HTTP). Mapea los campos OSINT a propiedades X-OSINT-* del vCard.

Frontend (ContactsView.tsx + api.ts + format.ts):
- Botón "Nuevo contacto" → modal con formulario Mantine (TextInput,
  TagsInput aliases, Select contexto, Textarea notas).
- Detalle: botones "Editar" (formulario precargado) y "Borrar" (con
  confirmación). Tras guardar refresca la lista.
- Helper slugify (replica slugify_obsidian_name) para resolver la ficha.

Tests: 6 nuevos casos (ciclo crear→editar→borrar con .md real + reflejo vCard
mockeado, organización, 404s, tipo inválido, preserva campos heredados). Suite
27 passed. Ciclo e2e real verificado contra Xandikos + vault (vCard creado,
editado y borrado; slug zz-test-crud limpiado). pnpm build verde (React 19 +
Mantine v9).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 00:18:55 +02:00

146 lines
6.3 KiB
Markdown

---
name: osint_web
lang: py
domain: osint
version: 0.1.0
description: "App web local OSINT: explora el vault de Obsidian osint (grafo sigma.js, tablas por tipo, fichas con galería de attachments) y la agenda/calendario del servidor Xandikos (CardDAV/CalDAV). Backend FastAPI que orquesta los grupos obsidian y dav del registry; escucha solo en 127.0.0.1 (datos sensibles)."
tags: [osint, web, sigma, graph, mantine, dav, obsidian, vault, dashboard]
uses_functions:
- build_obsidian_graph_py_obsidian
- list_obsidian_notes_py_obsidian
- read_obsidian_note_py_obsidian
- create_obsidian_note_py_obsidian
- update_obsidian_note_py_obsidian
- delete_obsidian_note_py_obsidian
- extract_obsidian_embeds_py_obsidian
- resolve_obsidian_embed_py_obsidian
- slugify_obsidian_name_py_obsidian
- search_obsidian_notes_py_obsidian
- dav_get_collection_py_infra
- dav_collection_ctag_py_infra
- carddav_put_vcard_py_infra
- dav_delete_resource_py_infra
- split_vcards_py_infra
- pass_get_secret_py_infra
uses_types: []
framework: "react-vite-mantine"
entry_point: "server/main.py"
dir_path: "projects/osint/apps/osint_web"
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_web"
e2e_checks:
- id: tests
cmd: ".venv/bin/python -m pytest tests -q"
timeout_s: 120
- id: vault_missing
cmd: ".venv/bin/python server/main.py --vault /no/existe --port 0"
expect_exit: 2
timeout_s: 30
---
## Qué es
App del issue 0172 (project `osint`). Combina dos fuentes de datos en un frontend
web local:
1. **El vault de Obsidian `~/Obsidian/osint`** (sin BD intermedia — decisión
KISS): grafo explorable (sigma.js), tablas filtradas por tipo y fichas con la
galería de attachments de cada nodo.
2. **El servidor Xandikos** (CardDAV/CalDAV): agenda de contactos y calendario de
eventos.
Edición de contactos (CRUD): la app permite crear, editar y borrar contactos
(personas y organizaciones). La **fuente de verdad es la ficha `.md` del vault**
(esquema canónico de `CONVENTIONS.md` §3b/§6); Xandikos es el retransmisor al
móvil. Cada alta/edición/borrado escribe primero la ficha del vault (acción
primaria con `create_obsidian_note` / `update_obsidian_note` /
`delete_obsidian_note`) y a continuación refleja el cambio en Xandikos de
inmediato (`carddav_put_vcard` / `dav_delete_resource`) para que se vea ya en la
app y en el móvil sin esperar al sync periódico del dag_engine.
Registry-first: el backend NO parsea el vault ni habla DAV a mano — orquesta las
funciones del grupo `obsidian` (`build_obsidian_graph`, `read_obsidian_note`,
`create_obsidian_note`, `update_obsidian_note`, `delete_obsidian_note`,
`resolve_obsidian_embed`, ...) y del grupo `dav` (`dav_get_collection`,
`dav_collection_ctag`, `carddav_put_vcard`, `dav_delete_resource`,
`split_vcards`) más `pass_get_secret` para la credencial, todas declaradas en
`uses_functions`.
## Stack
- **Backend**: FastAPI + uvicorn (venv propio en `.venv/`, deps en
`pyproject.toml`). Escucha solo en `127.0.0.1`.
- **Frontend** (pendiente — otro agente): React + Vite + Mantine v9 +
`@fn_library` + sigma.js / graphology. Ver `frontend/README.md`.
## Arrancar el backend
```bash
cd projects/osint/apps/osint_web
.venv/bin/python server/main.py --vault ~/Obsidian/osint --port 8470
```
El servidor cachea el grafo en memoria al arrancar; `POST /api/refresh`
re-escanea el vault bajo demanda (botón "refrescar" del frontend). Los datos DAV
no se cachean (se piden a Xandikos en cada llamada).
## Endpoints
| Método | Ruta | Devuelve |
|---|---|---|
| GET | `/api/health` | estado + nº de nodos/aristas cacheados |
| GET | `/api/graph` | grafo completo `{nodes, edges, counts}` para sigma.js |
| GET | `/api/nodes?tipo=persona` | filas de la tabla de ese tipo (id, label, tipo, frontmatter) |
| GET | `/api/node/<slug>` | ficha: frontmatter + body Markdown + attachments + wikilinks |
| GET | `/api/attachment?path=<rel>` | binario del attachment (path relativo al vault, allowlist) |
| GET | `/api/search?q=...` | nodos cuyo contenido matchea la query |
| GET | `/api/contacts` | contactos del addressbook Xandikos (CardDAV) a JSON |
| GET | `/api/contact/<uid>` | un vCard concreto a JSON |
| GET | `/api/calendar?from=&to=` | eventos del calendario Xandikos (CalDAV) en el rango |
| POST | `/api/refresh` | re-escanea el vault y reconstruye la caché |
## Configuración Xandikos
- Base URL: `https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com`
- Usuario: `enmanuel`; password en `pass dav/xandikos-enmanuel` (vía
`pass_get_secret`, nunca hardcodeada).
- Colecciones: `/enmanuel/contacts/addressbook/` (CardDAV),
`/enmanuel/calendars/calendar/` (CalDAV).
## Seguridad
- El vault contiene datos personales sensibles (DNIs, fotos): el server escucha
**solo en `127.0.0.1`**`--host` distinto avisa en stderr y NO es un service
desplegable a VPS (sin tag `service`).
- `/api/attachment` bloquea path traversal: `realpath` del candidato debe quedar
estrictamente bajo el `realpath` del vault; cualquier otro caso → 403 (404 si
el path es legítimo dentro del vault pero el archivo no existe).
- Vault inexistente al arrancar → error claro en stderr + exit 2 (nunca 500
silencioso).
- Sin red / Xandikos caído → los endpoints DAV devuelven `{"status":"error"}`
con código 502/503, nunca un crash.
## Tests
```bash
cd projects/osint/apps/osint_web
.venv/bin/python -m pytest tests -q
```
Cubren el DoD backend del issue 0172 + las extensiones DAV: grafo golden, tabla
por tipo, ficha con attachments (embed por path), wikilink dangling (nodo
fantasma), slug con acentos (`María del Mar``maria-del-mar`), path traversal
bloqueado, attachment legítimo servido, vault inexistente con error claro,
parseo vCard/iCalendar a JSON y degradación de los endpoints DAV sin red.
## Estado / pendiente
- **Hecho**: scaffold del sub-repo + backend FastAPI completo (vault + DAV) con
13 tests verdes.
- **Pendiente (siguiente agente)**: `frontend/` React + Vite + Mantine v9 +
`@fn_library` con sigma.js + graphology (GraphView, TablesView, NodeCard,
ContactsView, CalendarView). Onboarding previsto: backend en 8470 +
`pnpm dev` en `frontend/` → abrir `http://127.0.0.1:5173`. Ver
`frontend/README.md`.
- Cuando exista el manifest de sub-repos del project (issue 0171), añadir esta
app a `projects/osint/subrepos.yaml`.