Files
agent e792bc6e17 feat(calendar): vista mes/semana/día, TZ, selector de calendario, colores y CRUD de eventos
Backend (server/main.py):
- GET /api/calendars: lista las colecciones de calendario bajo el calendar-home
  con nombre y color (compone dav_list_calendars del registry).
- GET /api/calendar?cal=&from=&to=: eventos de una colección concreta (caché por
  colección validada por ctag). dtstart/dtend ahora en ISO con offset + tz
  original + all_day; parseo robusto de TZID/UTC/todo-el-día con zoneinfo.
- POST/PUT/DELETE /api/event[/<uid>]: CRUD de VEVENT contra Xandikos (fuente de
  verdad). Construye el VCALENDAR (con VTIMEZONE para zonas con DST), reutiliza el
  UID al editar (idempotente), trata 404 del DELETE como idempotente, invalida la
  caché de la colección tras escribir.

Frontend:
- CalendarView reescrita: conmutador Mes/Semana/Día con rejilla horaria propia
  (Mantine + dayjs, sin react-big-calendar para evitar fricción con React 19),
  mini-calendario de navegación, selector de calendario (con color), selector de
  zona horaria que recoloca los eventos, colores por evento (del VEVENT o del
  calendario).
- EventModal: alta/edición/borrado con summary, inicio/fin, todo-el-día, TZ,
  calendario, color, ubicación y descripción. Fechas en formato local 24h.
- calendar.ts: helpers de TZ (dayjs utc+timezone), posicionado por hora, semana
  empezando en lunes, locale es. api.ts: tipos y funciones de eventos/calendarios.

Verificado: ciclo real crear→editar→borrar contra Xandikos (cero residuo),
render del calendario en navegador (React 19 + Mantine v9 montan), pnpm build
verde, 40 tests verdes (+ smoke gateado). MKCALENDAR queda fuera (documentado).

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

169 lines
8.2 KiB
Markdown

---
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/<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 |
| POST/PUT/DELETE | `/api/contact[/<slug>]` | 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/<uid>` | edita un VEVENT existente |
| DELETE | `/api/event/<uid>?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`.