fix(events): envolver VEVENT en VCALENDAR al push (Xandikos 500) + INSERT explicito en contacts (columna import_key)

El raw de un evento guardaba solo BEGIN:VEVENT...END:VEVENT; subirlo a CalDAV
genera un .ics invalido que rompe Xandikos (assert isinstance(cal, Calendar) ->
500 en todo el calendario). _ensure_vcalendar lo envuelve en el push. Ademas, la
columna import_key (migracion 004) rompia los INSERT posicionales de contacts:
ahora son explicitos por columna y el ingest puebla import_key con la funcion del
registry. Tests actualizados (4 derivadas, INSERT explicito).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:15:27 +02:00
parent 36c4e06779
commit d53d7a9a7e
4 changed files with 49 additions and 9 deletions
+7 -1
View File
@@ -30,6 +30,7 @@ from .config import Config
from .db import write_conn
from .derived import rebuild_derived
from .registry_bridge import (
contact_import_key,
dav_get_collection,
dav_list_addressbooks,
dav_list_calendars,
@@ -450,6 +451,9 @@ def ingest_dav(cfg: Config) -> dict:
res.get("data", ""),
None, # note_path se rellena en el enlace posterior
now,
# Clave de importación determinística: nace con el contacto
# para que los re-imports lo localicen sin match frágil.
contact_import_key(parsed["fn"] or "", parsed["tels"], parsed["emails"]),
]
)
@@ -503,7 +507,9 @@ def ingest_dav(cfg: Config) -> dict:
conn.execute("DELETE FROM contacts")
if contact_rows:
conn.executemany(
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO contacts (uid, collection, etag, fn, tels, "
"emails, raw, note_path, updated_at, import_key) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
contact_rows,
)
conn.execute("DELETE FROM events")
+7
View File
@@ -123,6 +123,12 @@ duckdb_upsert = _load_registry_fn("infra", "duckdb_upsert", "duckdb_upsert")
# Composición del vCard multi-valor (DB -> Xandikos), puro.
build_vcard = _load_registry_fn("core", "build_vcard", "build_vcard")
# Clave de importación determinística (tel > email > nombre) para imports
# idempotentes de contactos. Pura.
contact_import_key = _load_registry_fn(
"core", "contact_import_key", "contact_import_key"
)
# Render de tablas Markdown + bloques sentinel idempotentes para las notas.
render_markdown_table = _load_registry_fn(
"core", "render_markdown_table", "render_markdown_table"
@@ -153,6 +159,7 @@ __all__ = [
"duckdb_execute",
"duckdb_upsert",
"build_vcard",
"contact_import_key",
"render_markdown_table",
"upsert_sentinel_block",
]
+26 -1
View File
@@ -848,7 +848,7 @@ def push_all_dav(cfg: Config) -> dict:
uid = row["uid"]
calendar = row.get("calendar") or "default"
collection = cfg.dav_calendar_home.rstrip("/") + "/" + calendar + "/"
raw = row.get("raw") or _build_vcalendar(uid, {})
raw = _ensure_vcalendar(row.get("raw")) or _build_vcalendar(uid, {})
push = caldav_put_event(
cfg.dav_base, cfg.dav_user, pwd, collection, uid, raw
)
@@ -866,6 +866,31 @@ def push_all_dav(cfg: Config) -> dict:
}
def _ensure_vcalendar(raw) -> str:
"""Garantiza que un recurso de evento tenga el envoltorio VCALENDAR.
El ``raw`` de un evento a veces guarda SOLO el bloque ``BEGIN:VEVENT ...
END:VEVENT`` (así lo extrae el parser del ingest DAV). Subir eso a CalDAV
produce un recurso ``.ics`` inválido: Xandikos falla al pedir la propiedad
``schedule-tag`` (``assert isinstance(cal, Calendar)``) y devuelve 500 para
todo el calendario. Esta función envuelve el VEVENT en un VCALENDAR mínimo
cuando falta, normalizando a CRLF; si el raw ya es un VCALENDAR lo deja igual.
Devuelve cadena vacía si no hay contenido (el llamador cae a _build_vcalendar).
"""
text = (raw or "").strip()
if not text:
return ""
if "BEGIN:VCALENDAR" in text.upper():
return raw if raw.endswith("\r\n") else raw + "\r\n"
text = text.replace("\r\n", "\n").replace("\r", "\n")
body = "\r\n".join(text.split("\n"))
return (
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//osint_db//events//EN\r\n"
+ body
+ "\r\nEND:VCALENDAR\r\n"
)
# ---------------------------------------------------------------------------
# push masivo POR DISCO (vía rápida: 1 rsync + 1 commit + 1 PROPFIND)
# ---------------------------------------------------------------------------