Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 26 additions & 22 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# PLAN - davcloud: de esqueleto a reemplazo de Radicale

Objetivo: sustituir Radicale (binario + systemd en el CT) por davcloud sin
que DAVx⁵, Thunderbird, iOS ni las extensiones web (opencloud-agenda,
Objetivo: sustituir Radicale (binario + systemd en un contenedor) por davcloud
sin que DAVx⁵, Thunderbird, iOS ni las extensiones web (opencloud-agenda,
opencloud-contacts) noten la diferencia.

Estado al 17-Oct-2026: **fases 0-3 completadas** (issues #1-#9 cerrados) y
**nada de Docker**: el despliegue es un binario estático + systemd (ver
`deploy/README.md`).
Estado al 18-Oct-2026: **fases 0-4 completadas** (issues #1-#9 y #21 cerrados;
davcloud es el servidor DAV de producción desde el 17-Oct) y **fase 5 en
curso** (soak + drill de rollback, issue #22). **Nada de Docker**: el
despliegue es un binario estático + systemd (ver `deploy/README.md`). Repo
público desde el 18-Oct-2026, licencia AGPL-3.0.

## Hecho

Expand All @@ -27,31 +29,33 @@ Estado al 17-Oct-2026: **fases 0-3 completadas** (issues #1-#9 cerrados) y
- **Recurrencias** (PR #13): caracterización: go-webdav YA expande
RRULE/RDATE/EXDATE en los time-ranges; tolerancia a RRULE rotas (fallback a
evento simple) y TZID sin VTIMEZONE verificado.
- **Contrato real de producción** (PR #15): verificado read-only contra el CT:
el proxy elimina el prefijo y manda `X-Script-Name` (→ modo `-dav-root`);
el `.Radicale.props` real guarda el component-set como string (→ formato
compatible en ambos sentidos).
- **Contrato real de producción** (PR #15): verificado read-only contra el
despliegue real: el proxy elimina el prefijo y manda `X-Script-Name`
(→ modo `-dav-root`); el `.Radicale.props` real guarda el component-set
como string (→ formato compatible en ambos sentidos).
- **Extras**: CI de 4 jobs (PR #25), binario versionado (PR #26), README
bilingüe (PR #17), licencia AGPL-3.0 (PR #14), sin Docker (PR #27).
- **Ensayo de migración en seco**: copia real del volumen del CT montada en
davcloud: lectura, escritura byte a byte y sync verificados con dos
usuarios reales. El CT quedó intacto.
- **Fase 4 - migración de producción** (issue #21, 17-Oct-2026): swap de
systemd en el contenedor según `deploy/README.md`, con backup previo,
verificado con la UI y con DAV por curl (PROPFIND con hrefs prefijados,
sync-collection, ciclo PUT/GET/DELETE). Radicale queda parado y deshabilitado.
- **Web UI**: vCard 3.0 al escribir y tests de los helpers (PR #30), vista de
tarjetas y arreglos de la libreta (PRs #21, #33).
- **Ensayo de migración en seco**: copia real del volumen de producción
montada en davcloud: lectura, escritura byte a byte y sync verificados con
dos usuarios reales. El original quedó intacto.

## Pendiente (issues)

- **#21 - Fase 4: migración de producción** (swap systemd en el CT 4125
según `deploy/README.md`; backup primero; verificación con la UI y DAVx⁵).
- **#22 - Fase 5: soak + drill de rollback** (1-2 semanas con davcloud como
único servidor DAV; después arrancar Radicale sobre el mismo volumen y
verificar que lee todo lo escrito por davcloud; solo entonces retirar
Radicale).
- **#23, #24 - Web UI menores**: vCard 3.0 en vez de 4.0 y tests de los
helpers (la edición de recurrencias en la UI sigue siendo un TODO
documentado en `web/src/lib/ics.ts`).
- **#22 - Fase 5: soak + drill de rollback** (en curso desde el 17-Oct:
davcloud es el único servidor DAV; 1-2 semanas de uso diario, después
arrancar Radicale sobre el mismo volumen y verificar que lee todo lo escrito
por davcloud; solo entonces retirar Radicale).
- **Edición de recurrencias en la web UI**: sigue siendo un TODO documentado
en `web/src/lib/ics.ts`.
- **Fase 6 - paridad extendida (opcional, por necesidad)**: iMIP
(invitaciones por email) y compartición de calendarios (requiere ACLs que
el proxy no proporciona). Radicale tampoco cubre bien ninguno de los dos.
- Publicar o no el repo en GitHub: decisión aparte (hoy privado).

## Regla de oro

Expand Down
5 changes: 4 additions & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,10 @@ el mismo volumen (layout y metadatos son byte-compatibles en ambos sentidos).

## Estado

Sirviendo producción. Cubierto por 37 tests de Go y 10 de la web, CI en cada
En uso diario como servidor DAV detrás del despliegue de OpenCloud del autor;
el periodo de soak está en marcha y el drill de rollback sigue pendiente, así
que es software en desarrollo activo. La lista de fases y lo que queda está en
[PLAN.md](PLAN.md). Cubierto por 40 tests de Go y 10 de la web, CI en cada
push (test/lint/security/web) y un ensayo de migración contra una copia de un
volumen real de producción. Las limitaciones conocidas están seguidas como
issues: iMIP, compartición de calendarios, edición de recurrencias en la web.
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,13 @@ volume (the layout and metadata are byte-compatible both ways).

## Status

Serving production. Covered by 37 Go tests and 10 web tests, CI on every
push (test/lint/security/web), and a migration dry run against a copy of a
real production volume. Known limitations are tracked as issues: iMIP,
calendar sharing, recurrence editing in the web UI.
In daily use as the DAV server behind the author's OpenCloud deployment; the
soak period is running and the rollback drill is still pending, so treat it as
software in active development. The phase list and what is left are in
[PLAN.md](PLAN.md). Covered by 40 Go tests and 10 web tests, CI on every push
(test/lint/security/web), and a migration dry run against a copy of a real
production volume. Known limitations are tracked as issues: iMIP, calendar
sharing, recurrence editing in the web UI.

## License

Expand Down
30 changes: 17 additions & 13 deletions deploy/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Despliegue de davcloud en sustitución de Radicale

davcloud es un binario estático de Go: no hay contenedores por medio. El
despliegue real (CT 4125, nodo4) sustituye el servicio systemd de Radicale por
uno propio, sobre el MISMO puerto y el MISMO volumen, así que el proxy de
OpenCloud no se toca.
despliegue de referencia (un contenedor LXC sobre Proxmox, en nuestro caso)
sustituye el servicio systemd de Radicale por uno propio, sobre el MISMO
puerto y el MISMO volumen, así que el proxy de OpenCloud no se toca.

Contrato que el proxy ya cumple y que NO hay que cambiar (verificado en
producción, `proxy.yaml`): reenvía `/caldav/` y `/carddav/` a
Expand All @@ -13,10 +13,11 @@ usuario. Por eso davcloud arranca con `-dav-root`.

## 0. Backup (obligatorio)

El CT está fuera de PBS: copia fría del volumen antes de nada.
Haz copia fría del volumen antes de nada (si el contenedor no está cubierto
por backups automáticos, es la única red de seguridad):

```
pct exec 4125 -- sh -c 'systemctl stop radicale && cp -a /var/lib/radicale /var/lib/radicale.bak-$(date +%Y%m%d) && systemctl start radicale'
pct exec <ctid> -- sh -c 'systemctl stop radicale && cp -a /var/lib/radicale /var/lib/radicale.bak-$(date +%Y%m%d) && systemctl start radicale'
```

## 1. Binario estático
Expand All @@ -35,8 +36,8 @@ Subida al CT (el `scp` del binario en marcha da "Text file busy": parar el
servicio antes del `mv`, como en ocapps):

```
scp -P 2222 davcloud root@nodo4.domatix.com:/root/davcloud-new
pct push 4125 /root/davcloud-new /usr/local/bin/davcloud --perms 755
scp davcloud root@<your-host>:/root/davcloud-new # -P <port> si tu SSH no es el 22
pct push <ctid> /root/davcloud-new /usr/local/bin/davcloud --perms 755
```

## 2. Web UI compilada (opcional)
Expand All @@ -46,9 +47,11 @@ este paso es solo si quieres conservar la UI para uso directo:

```
cd web && npm ci && npm run build
pct push 4125 web/dist root@nodo4.domatix.com... # o copiala fuera si no la vas a usar
pct push <ctid> web/dist /srv/davcloud-web --recursive
```

Si la sirves, apunta el binario a ese directorio con `-ui-dir /srv/davcloud-web`.

## 3. Unit systemd (mismo puerto, mismo volumen, mismo usuario)

`/etc/systemd/system/davcloud.service`. Usa el usuario `radicale` porque es el
Expand Down Expand Up @@ -86,7 +89,7 @@ Sin `-dev-user` en producción: la identidad SIEMPRE la inyecta el proxy.
## 4. Swap (transparente para el proxy)

```
pct exec 4125 -- sh -c 'systemctl stop radicale && systemctl disable radicale && systemctl daemon-reload && systemctl enable --now davcloud'
pct exec <ctid> -- sh -c 'systemctl stop radicale && systemctl disable radicale && systemctl daemon-reload && systemctl enable --now davcloud'
```

## 5. Verificación (dentro del CT, sin tocar el proxy)
Expand All @@ -108,17 +111,18 @@ curl -s -X REPORT -H 'X-Script-Name: /caldav' -H 'X-Remote-User: <uuid>' \
```

Y desde fuera (navegador): abrir la app agenda de OpenCloud y comprobar que
los calendarios y eventos reales aparecen. **No hacer `curl` sin auth contra
`drive.domatix.cloud`**: el fail2ban del NPM banea la IP de casa por 401
repetidos (lección del 13-Sep-2026).
los calendarios y eventos reales aparecen. **No hagas `curl` sin autenticar
contra el hostname público para "ver si responde"**: si delante hay un
fail2ban, una racha de 401 te banea la IP y te deja fuera del servicio (nos
pasó: el corte alcanza a toda la red de la que salgas).

## 6. Rollback drill (fase 5 del PLAN)

El volumen es byte-compatible en ambos sentidos (davcloud guarda los items
tal cual y las props de colección en el mismo formato de Radicale):

```
pct exec 4125 -- sh -c 'systemctl stop davcloud && systemctl start radicale'
pct exec <ctid> -- sh -c 'systemctl stop davcloud && systemctl start radicale'
```

Después de un periodo con davcloud, arrancar Radicale y comprobar que ve los
Expand Down
3 changes: 2 additions & 1 deletion internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ func TestEnsureUserCreatesDefaultsAndIsIdempotent(t *testing.T) {
}

// TestReadsRealRadicaleProps is a compatibility regression against the exact
// .Radicale.props read from a live Radicale deployment (CT 4125): the
// .Radicale.props read from a live Radicale deployment (in a container,
// behind an OpenCloud proxy): the
// supported-calendar-component-set is a comma separated STRING, not a JSON
// array, and it must be written back in that same shape so the volume stays
// interchangeable in both directions.
Expand Down
Loading