--- name: osint_web lang: py domain: osint version: 0.2.0 description: "App web local OSINT: explora el vault de Obsidian osint (grafo sigma.js, tablas por tipo, fichas con galería de attachments), la agenda del servidor Xandikos (CardDAV) y un calendario completo (CalDAV) con vista mes/semana/día, zonas horarias, selector de calendario, colores y CRUD de eventos. 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 - dav_list_calendars_py_infra - carddav_put_vcard_py_infra - caldav_put_event_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. Calendario (CRUD de eventos): la app lista las colecciones de calendario, muestra los eventos en vista mes/semana/día (con zona horaria y color seleccionables) y permite crear, editar y borrar eventos. Aquí la **fuente de verdad es Xandikos directamente** (el calendario NO existe en el vault): cada operación escribe el VCALENDAR/VEVENT en la colección CalDAV (`caldav_put_event` / `dav_delete_resource`) e invalida la caché de esa colección. 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`, `dav_list_calendars`, `carddav_put_vcard`, `caldav_put_event`, `dav_delete_resource`, `split_vcards`) más `pass_get_secret` para la credencial, todas declaradas en `uses_functions`. La única lógica propia de la app sobre el calendario es el parseo robusto de VEVENT (TZID / UTC / todo el día → ISO con offset, vía `zoneinfo`) y la construcción del VCALENDAR de salida. ## 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/` | ficha: frontmatter + body Markdown + attachments + wikilinks | | GET | `/api/attachment?path=` | 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/` | un vCard concreto a JSON | | POST/PUT/DELETE | `/api/contact[/]` | CRUD de contactos (ficha `.md` del vault + reflejo vCard) | | GET | `/api/calendars` | colecciones de calendario bajo `/enmanuel/calendars/` (nombre + color) | | GET | `/api/calendar?cal=&from=&to=` | eventos de una colección del calendario (CalDAV) en el rango | | POST | `/api/event` | crea un VEVENT (`{cal, summary, dtstart, dtend, tz, all_day, location, description, color}`) | | PUT | `/api/event/` | edita un VEVENT existente | | DELETE | `/api/event/?cal=` | borra un VEVENT | | 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) + frontend React/Vite/Mantine v9 (GraphView, TablesView, ContactsView, CalendarView). Suite backend verde (40 tests + 1 smoke gateado). - **Calendario (v0.2.0)**: vista mes/semana/día, selector de calendario (con color), selector de zona horaria, colores por evento y CRUD completo de eventos (crear en un hueco, editar/borrar). Backend con parseo robusto de TZID/UTC/todo-el-día y construcción de VCALENDAR (con VTIMEZONE para zonas con DST). Verificado con un ciclo real crear→editar→borrar contra Xandikos (cero residuo). Onboarding: backend en 8470 + `pnpm dev` en `frontend/` → abrir `http://127.0.0.1:5173` → pestaña "Calendario". - **Gaps conocidos**: MKCALENDAR (crear colección de calendario nueva) NO implementado — Xandikos tiene hoy una sola colección y la UI no lo necesita; documentado como pendiente. El frontend usa una rejilla Mantine propia (sin `react-big-calendar`) para evitar fricción de peer-deps con React 19. - Cuando exista el manifest de sub-repos del project (issue 0171), añadir esta app a `projects/osint/subrepos.yaml`.