# Baikal MCP pour la gestion des agendas perso et NEXTE et de l'observatoire des tâches

# 1-l'infrastructure Baikal et Infcloud sur le vps personnel - juxjux.ovh

<div id="bkmrk-" style="clear: left;">  
</div>Cas à part de la gestion des contacts et du calendrier -   
Non pas sur alteris.ovh mais sur juxjux.ovh car primauté de l'infrastructure personnelle de JUlien pour la gestion des agendas et des contacts.

# la solution contacts et calendrier

<div id="bkmrk--1">  
</div>Solution d'unification des contacts et du calendrier de Julien.

**État au 3 août 2026 — déployé et opérationnel de bout en bout.** Seul vdirsyncer (agrégation Google/Infomaniak) reste à faire.

---

# 1. Objectif

Unifier la base de contacts et de calendriers, dispersée entre plusieurs sources (Google, Infomaniak, téléphone), autour d'un point central auto-hébergé sur le VPS juxjux.ovh, synchronisé vers tous les clients existants.

Baïkal est la **source de vérité unique**. Roundcube, InfCloud et DAVx5 en sont des clients ; vdirsyncer sera le seul composant qui écrit depuis l'extérieur vers Baïkal.

# 2. Architecture réellement déployée

<table id="bkmrk-composant-r%C3%B4le-empla"><thead><tr><th>Composant</th><th>Rôle</th><th>Emplacement</th><th>État</th></tr></thead><tbody><tr><td>Baïkal</td><td>Serveur CardDAV + CalDAV, stockage central</td><td>Docker, VPS juxjux</td><td>En service</td></tr><tr><td>InfCloud</td><td>Agenda **et** contacts web</td><td>Statique, servi par nginx</td><td>En service</td></tr><tr><td>Roundcube</td><td>Interface web des contacts</td><td>Docker, VPS juxjux</td><td>En service</td></tr><tr><td>Nginx</td><td>Reverse proxy HTTPS</td><td>Hôte VPS</td><td>En service</td></tr><tr><td>DAVx5</td><td>Pont de synchronisation vers Android</td><td>Téléphone</td><td>**Opérationnel**</td></tr><tr><td>Fossify Agenda</td><td>Client calendrier</td><td>Téléphone (via DAVx5)</td><td>Opérationnel</td></tr><tr><td>vdirsyncer</td><td>Agrège Google et Infomaniak vers Baïkal</td><td>VPS, cron</td><td>**Non déployé**</td></tr><tr><td>Thunderbird</td><td>Client mail, indépendant de ce circuit</td><td>Téléphone</td><td>Inchangé</td></tr></tbody></table>

# 3. Accès

<table id="bkmrk-service-url-identifi"><thead><tr><th>Service</th><th>URL</th><th>Identifiant</th></tr></thead><tbody><tr><td>**Agenda + contacts web**</td><td>https://baikal.juxjux.ovh/infcloud/</td><td>`Julien` (compte Baïkal)</td></tr><tr><td>Contacts web (Roundcube)</td><td>https://secretariat.juxjux.ovh</td><td>`julien.bertrand@ik.me` (compte mail Infomaniak)</td></tr><tr><td>Administration Baïkal</td><td>https://baikal.juxjux.ovh/admin/</td><td>compte admin Baïkal</td></tr><tr><td>Découverte DAV (DAVx5)</td><td>`https://baikal.juxjux.ovh/dav.php/`</td><td>`Julien`</td></tr></tbody></table>

**Utilisateur Baïkal : `Julien` — avec un J majuscule.** La casse compte dans les URLs DAV.

- Principal : `https://baikal.juxjux.ovh/dav.php/principals/Julien/`
- Contacts : `https://baikal.juxjux.ovh/dav.php/addressbooks/Julien/default/`
- Calendrier : `https://baikal.juxjux.ovh/dav.php/calendars/Julien/default/`

Certificats Let's Encrypt valides jusqu'au **1er novembre 2026** pour les deux domaines. Celui de `baikal.juxjux.ovh` existait déjà mais avait **expiré le 8 février 2026** (vestige d'une tentative antérieure) ; il a été renouvelé le 3 août.

# 4. Déploiement

Stack sur le VPS : `/home/debian/baikal/docker-compose.yml`, projet compose `baikal`. Sources versionnées : `Syncthing/Jux_univers/Jux-scripts/Baikal-Contacts/`.

<table id="bkmrk-fichier-r%C3%B4le-docker-"><thead><tr><th>Fichier</th><th>Rôle</th></tr></thead><tbody><tr><td>`docker-compose.yml`</td><td>Stack Baïkal + Roundcube</td></tr><tr><td>`baikal.juxjux.ovh.conf`</td><td>Vhost Baïkal (`.well-known` forcés en HTTPS + service d'InfCloud)</td></tr><tr><td>`secretariat.juxjux.ovh.conf`</td><td>Vhost Roundcube</td></tr><tr><td>`infcloud-config.js`</td><td>Configuration InfCloud</td></tr><tr><td>`finaliser_tls.sh`</td><td>Vérifie le DNS puis lance certbot</td></tr><tr><td>`vps_backup.sh`</td><td>Script de sauvegarde complet, Baïkal inclus</td></tr><tr><td>`push_bookstack.py`</td><td>Publication de cette page via l'API BookStack</td></tr></tbody></table>

<div id="bkmrk-1-cd-%2Fhome%2Fdebian%2Fba"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div></div></div><div class="cm-content"><div class="cm-line"><span class="ͼs">cd</span> /home/debian/baikal &amp;&amp; <span class="ͼs">sudo</span> docker compose <span class="ͼq">-p</span> baikal up <span class="ͼq">-d</span></div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div><table id="bkmrk-service-image-port-l"><thead><tr><th>Service</th><th>Image</th><th>Port local</th><th>Base</th><th>RAM</th></tr></thead><tbody><tr><td>baikal</td><td>`ckulka/baikal:nginx`</td><td>127.0.0.1:8088</td><td>SQLite</td><td>~38 Mo</td></tr><tr><td>roundcube</td><td>`roundcube/roundcubemail:latest` (1.7.2)</td><td>127.0.0.1:8083</td><td>SQLite</td><td>~32 Mo</td></tr><tr><td>InfCloud</td><td>fichiers statiques (13 Mo sur disque)</td><td>—</td><td>aucune</td><td>0</td></tr></tbody></table>

Les deux containers pèsent **environ 70 Mo au total**, contre environ 600 Mo avec l'architecture MariaDB initialement prévue. Ce choix était important : le VPS était en pression mémoire au moment du déploiement (swap à 3,8/4,0 Go).

# 5. Écarts avec le plan initial

Le plan préparé prévoyait une configuration qui ne pouvait pas fonctionner en l'état. Écarts constatés et corrigés :

<table id="bkmrk-point-pr%C3%A9vu-r%C3%A9el-rai"><thead><tr><th>Point</th><th>Prévu</th><th>Réel</th><th>Raison</th></tr></thead><tbody><tr><td>Port Baïkal</td><td>8081</td><td>**8088**</td><td>8081 est occupé par FreshRSS. 8088 correspond au vhost `baikal.juxjux.ovh` préexistant</td></tr><tr><td>Port Roundcube</td><td>8082</td><td>**8083**</td><td>8082 est ciblé par un vhost `nextcloud` mort</td></tr><tr><td>Domaine Roundcube</td><td>`mail.juxjux.ovh`</td><td>**`secretariat.juxjux.ovh`**</td><td>Aucun DNS n'existait ; `mail.` prêtait à confusion avec les MX OVH du domaine</td></tr><tr><td>Base Roundcube</td><td>MariaDB</td><td>**SQLite**</td><td>~400 Mo de RAM économisés, suffisant en mono-utilisateur</td></tr><tr><td>Vhost Baïkal</td><td>À créer</td><td>**Déjà présent**</td><td>Le fichier `baikal.conf` préparé n'a pas servi</td></tr><tr><td>Agenda web</td><td>Plugin calendar Roundcube</td><td>**InfCloud**</td><td>Plugin inutilisable, voir §6.2</td></tr></tbody></table>

# 6. Pièges rencontrés

### 6.1 Les plugins carddav et calendar ne sont pas dans l'image Roundcube

L'image officielle ne fournit ni `carddav` ni `calendar`. Les déclarer dans `ROUNDCUBEMAIL_PLUGINS` ne fait que les *activer* — s'ils sont absents, Roundcube part en erreur fatale.

Il faut les faire installer par composer au démarrage, via **`ROUNDCUBEMAIL_COMPOSER_PLUGINS`** (et non `ROUNDCUBEMAIL_INSTALL_PLUGINS`, qui n'existe pas dans cette image) :

<div id="bkmrk-1-2-roundcubemail_co"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div><div class="cm-gutterElement">2</div></div></div><div class="cm-content"><div class="cm-line"><span class="ͼ12">ROUNDCUBEMAIL\_COMPOSER\_PLUGINS</span><span class="ͼw">: </span><span class="ͼ13">"roundcube/carddav"</span></div><div class="cm-line"><span class="ͼ12">ROUNDCUBEMAIL\_PLUGINS</span><span class="ͼw">: </span><span class="ͼ13">"carddav"</span></div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>Le premier démarrage est alors plus long (téléchargement composer).

### 6.2 Le plugin calendar de Roundcube est une impasse

Trois obstacles successifs, testés le 3 août :

**a) Il casse le tout premier démarrage.** Le script d'initialisation de base de `kolab/calendar` s'exécute **avant** que l'entrypoint n'écrive la configuration Roundcube : il tombe sur le DSN MySQL par défaut et échoue en `SQLSTATE[HY000] [2002] No such file or directory`. Cette erreur fatale empêche la régénération de l'autoloader composer, ce qui casse **aussi** le plugin carddav :

<div id="bkmrk-1-2-3-php-fatal-erro"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div><div class="cm-gutterElement">2</div><div class="cm-gutterElement">3</div></div></div><div class="cm-content"><div class="cm-line">PHP Fatal error: Uncaught Error: Interface</div><div class="cm-line">"MStilkerich\RCMCardDAV\Frontend\RcmInterface" not found</div><div class="cm-line">in /var/www/html/plugins/carddav/carddav.php:43</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>Symptôme : HTTP 500 permanent, alors que les fichiers du plugin sont bien présents sur le disque — c'est `vendor/composer/autoload_psr4.php` qui ne contient aucune entrée carddav.

**b) Ce premier obstacle est contournable.** Réinstallé *après* que Roundcube ait été initialisé une première fois, la configuration existe déjà dans le volume et l'initialisation aboutit (`Creating database schema... [OK]`). Vérifié.

**c) Mais le plugin ne sait pas parler à Baïkal.** La seule version installable, `kolab/calendar` 3.2.9.1, ne livre que les pilotes `database`, `kolab` et `ldap` — **aucun pilote `caldav`**. Il ne créerait qu'un agenda **local** dans Roundcube, sans lien avec Baïkal ni le téléphone : pire qu'inutile, on pourrait y saisir des rendez-vous en croyant qu'ils se synchronisent.

Les versions qui embarquent le pilote CalDAV (3.5.7, 3.6.1) dépendent de `kolab/libkolab`, qui réclame `pear/http_request2` — paquet bloqué par composer pour **faille de sécurité** (`PKSA-jrt3-xndd-g4sz`). Ne pas contourner ce garde-fou sur un service exposé sur internet.

**Conclusion : agenda web assuré par InfCloud (§7), plugin calendar définitivement écarté.**

### 6.3 Baïkal ignore X-Forwarded-Proto — fuite HTTP sur la découverte

Baïkal ne tient pas compte de `X-Forwarded-Proto` et générait ses redirections de découverte en **HTTP nu** :

<div id="bkmrk-1-%2F.well-known%2Fcardd"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div></div></div><div class="cm-content"><div class="cm-line">/.well-known/carddav -&gt; http://baikal.juxjux.ovh/dav.php</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>C'est exactement le risque identifié dès la conception : CardDAV et CalDAV envoient les identifiants **à chaque synchronisation**. Un client suivant cette redirection part sur du HTTP.

Corrigé en court-circuitant Baïkal directement dans le vhost :

<div id="bkmrk-1-2-3-4-5-6-location"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div><div class="cm-gutterElement">2</div><div class="cm-gutterElement">3</div><div class="cm-gutterElement">4</div><div class="cm-gutterElement">5</div><div class="cm-gutterElement">6</div></div></div><div class="cm-content"><div class="cm-line"><span class="ͼp">location</span> = /.<span class="ͼu">well-known</span>/<span class="ͼu">carddav </span>{</div><div class="cm-line"><span class="ͼp">return</span> <span class="ͼu">301 https</span>://$host/dav.php/;</div><div class="cm-line">}</div><div class="cm-line"><span class="ͼp">location</span> = /.<span class="ͼu">well-known</span>/<span class="ͼu">caldav </span>{</div><div class="cm-line"><span class="ͼp">return</span> <span class="ͼu">301 https</span>://$host/dav.php/;</div><div class="cm-line">}</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>À vérifier après tout `reload nginx` : le rechargement n'est pas instantané, un test lancé dans la foulée peut encore montrer l'ancien comportement.

### 6.4 Roundcube exige un serveur IMAP pour authentifier

Roundcube **n'a pas de base d'utilisateurs propre** : son écran de login valide les identifiants contre le serveur IMAP configuré. Sans IMAP joignable, aucune connexion n'est possible — donc aucun accès aux contacts non plus.

L'idée d'un Roundcube « contacts et agenda seulement, sans IMAP » n'est pas réalisable. La configuration retenue pointe vers `ssl://mail.infomaniak.com:993`, ce qui couvre le compte `julien.bertrand@ik.me` (le domaine `ik.me` est servi par l'infrastructure Infomaniak).

Si un jour le compte passe en double authentification, il faudra générer un **mot de passe d'application** Infomaniak.

### 6.5 Baïkal doit être installé en SQLite

L'installeur Baïkal propose MySQL ou SQLite. **Choisir SQLite** : la stack ne contient aucun serveur MySQL, cocher « Enable MySQL » mène à une impasse. Base à `/var/www/baikal/Specific/db/db.sqlite`, dans le volume `baikal-data`.

# 7. InfCloud — l'agenda web

Client CalDAV/CardDAV **entièrement côté navigateur** : aucun backend, aucune base, du HTML/JS servi en statique par nginx. Affiche agenda et contacts.

- Source officielle : `https://www.inf-it.com/InfCloud_0.13.1.zip` (3,9 Mo, sha256 `9fa95edd2dcc2b864a10b503ab9220895ea28d4c5541ab02117de1511d5464d4`)
- Installé dans `/home/debian/baikal/infcloud`, propriétaire `www-data`
- **Servi sur le même domaine que Baïkal** (`/infcloud/`) et non sur un sous-domaine dédié : InfCloud interroge `/dav.php/` en direct depuis le navigateur ; depuis une autre origine il aurait fallu ouvrir du CORS sur les méthodes DAV de Baïkal, ce qui est fragile et l'affaiblirait. Même origine = aucun CORS.
- Dossier `auth/` **supprimé** : module PHP d'authentification par proxy inutilisé ici. nginx ne traite pas le PHP à cet emplacement — servis en statique, ces fichiers `.inc` auraient exposé leur contenu en clair.

**Réserve assumée** : projet figé depuis 2015, interface datée, embarque jQuery 2.1.4. Acceptable parce qu'il n'a aucun composant serveur : rien à maintenir, rien à patcher. La seule alternative maintenue serait SOGo, au prix d'environ 500 Mo de RAM et d'une base PostgreSQL.

## 7.1 Les trois pièges d'InfCloud

**a) Le bloc regex du vhost avale tous les JS.** Le vhost Baïkal contient déjà :

<div id="bkmrk-1-location-%7E%2A-%5C.%28js%7C"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div></div></div><div class="cm-content"><div class="cm-line"><span class="ͼp">location</span> <span class="ͼu">~</span>* <span class="ͼu">\\</span>.<span class="ͼu">(js|css|png|jpg|jpeg|gif|ico|svg)$ </span>{ <span class="ͼp">proxy\_pass</span> <span class="ͼp">http</span>://127.0.0.1:8088; }</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>En nginx, un bloc regex `~*` a priorité sur un bloc préfixe ordinaire. Tous les fichiers JavaScript d'InfCloud auraient donc été proxifiés vers Baïkal. Comme InfCloud est presque intégralement du JavaScript, la page serait restée blanche — symptôme difficile à diagnostiquer. **Il faut `^~`, qui passe devant les regex :**

<div id="bkmrk-1-2-3-4-5-location-%5E"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div><div class="cm-gutterElement">2</div><div class="cm-gutterElement">3</div><div class="cm-gutterElement">4</div><div class="cm-gutterElement">5</div></div></div><div class="cm-content"><div class="cm-line"><span class="ͼp">location</span> <span class="ͼu">^~</span> /infcloud/ {</div><div class="cm-line"><span class="ͼp">alias</span> /home/debian/baikal/infcloud/;</div><div class="cm-line"><span class="ͼp">index</span> <span class="ͼp">index</span>.html;</div><div class="cm-line"><span class="ͼp">location</span> <span class="ͼu">~</span> /<span class="ͼu">\\</span>. { <span class="ͼp">deny</span> all; }</div><div class="cm-line">}</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>**b) L'endpoint par défaut vise DAViCal.** Le `config.js` livré pointe `/caldav.php/` ; Baïkal expose `/dav.php/`.

**c) Le href doit viser les *principals*, pas la racine DAV.** InfCloud accole le nom d'utilisateur au `href`. Avec `/dav.php/` il demandait `/dav.php/Julien/` → **404**. La documentation du fichier le précise : *« globalNetworkCheckSettings : principal URL WITHOUT the USER/ part »*. Chez Baïkal, la bonne valeur est donc :

<div id="bkmrk-1-%27%2Fdav.php%2Fprincipa"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div></div></div><div class="cm-content"><div class="cm-line"><span class="ͼ13">'/dav.php/principals/'</span><span class="ͼt">,</span></div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>Après toute modification de `config.js`, **rechargement forcé du navigateur (Ctrl+Maj+R)** : le fichier est mis en cache.

## 7.2 Digest → Basic : le blocage d'authentification

Baïkal était configuré en `dav_auth_type: Digest` et annonçait `WWW-Authenticate: Digest realm="BaikalDAV"`.

**InfCloud ne sait faire que du Basic.** Ses requêtes partaient donc en 401 en boucle, alors que DAVx5 — qui gère Digest — fonctionnait parfaitement avec le même compte. Le symptôme prêtait à confusion : les identifiants étaient bons.

Diagnostic par les logs nginx, où le champ utilisateur est renseigné mais la réponse reste 401 :

<div id="bkmrk-1-83.195.45.93---jul"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement">  
</div><div class="cm-gutterElement">1</div></div></div><div class="cm-content"><div class="cm-line">83.195.45.93 - Julien "PROPFIND /dav.php/ HTTP/2.0" 401</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>**Correctif** : `dav_auth_type: Basic` dans `/var/www/baikal/config/baikal.yaml`, puis `docker restart baikal`.

Ce n'est pas un pis-aller. Digest repose sur MD5, est considéré comme obsolète, et impose au serveur de stocker une empreinte exploitable ; **Basic sur TLS est aujourd'hui la recommandation**, et tout est en HTTPS ici, y compris les redirections de découverte (§6.3).

**Les mots de passe restent valides** : Baïkal calcule la même empreinte `md5(username:auth_realm:password)` dans les deux modes (`Baikal\Core\PDOBasicAuth::validateUserPass`), avec `auth_realm: BaikalDAV` conservé en interne — même si le serveur annonce désormais `realm="sabre/dav"` côté client. Rien à redéfinir, DAVx5 bascule tout seul.

Sauvegarde : `/var/www/baikal/config/baikal.yaml.bak-260803-digest` (dans le container).

# 8. Sauvegarde

Baïkal a été ajouté à `/opt/backups/vps_backup.sh` le 3 août 2026 (cron quotidien à 2h UTC).

- Dossier kDrive de destination : **1467771** (`SYNC-pour_VPS/backups/baikal`)
- Le bilan de fin de log passe de 5 à **6 services** (le total est calculé dynamiquement, pas codé en dur)
- Sauvegarde de l'ancien script : `/opt/backups/vps_backup.sh.bak-260803`

La fonction archive **deux** volumes, via un `tar` intermédiaire puis 7z :

- `Specific/` — la base `db.sqlite` (contacts et calendriers)
- `config/` — `baikal.yaml` (paramètres et empreinte du mot de passe admin)

**Sans le second, la restauration est incomplète.** Testée le 3 août : upload HTTP 200, aucun résidu temporaire.

InfCloud n'a pas besoin d'être sauvegardé (fichiers statiques réinstallables), mais son `config.js` est versionné dans `Jux-scripts/Baikal-Contacts/`.

# 9. Vérifications effectuées le 3 août 2026

<table id="bkmrk-test-r%C3%A9sultat-get-%2Fd"><thead><tr><th>Test</th><th>Résultat</th></tr></thead><tbody><tr><td>`GET /dav.php/`</td><td>HTTP 401 — authentification bien exigée</td></tr><tr><td>`PROPFIND /dav.php/`</td><td>HTTP 401 et non 405 — nginx laisse passer les méthodes DAV étendues</td></tr><tr><td>`.well-known/carddav` et `caldav`</td><td>301 vers HTTPS</td></tr><tr><td>Installation Baïkal</td><td>Utilisateur `Julien`, carnet et calendrier `default` créés</td></tr><tr><td>Login Roundcube</td><td>IMAP Infomaniak accepté</td></tr><tr><td>Découverte CardDAV Roundcube</td><td>Carnet résolu à `.../dav.php/addressbooks/Julien/default/`</td></tr><tr><td>Écriture d'un contact</td><td>vCard 3.0 écrite par RCMCardDAV v5.1.3, relue dans Baïkal, accents UTF-8 préservés</td></tr><tr><td>**Synchronisation DAVx5**</td><td>5 rendez-vous d'août écrits dans Baïkal par `DAVx5/4.5.18-ose`, `synctoken` du calendrier à 6</td></tr><tr><td>**InfCloud**</td><td>32 ressources servies en 200 ; `PROPFIND` sur principals, addressbooks et calendars tous en **207**</td></tr><tr><td>Dotfiles InfCloud</td><td>`.htaccess` bloqué en 403</td></tr><tr><td>Sauvegarde Baïkal</td><td>Archive uploadée sur kDrive, HTTP 200</td></tr></tbody></table>

Note : dans la table `carddav_addressbooks` de Roundcube, une seconde ligne nommée `%N` avec une URL vide n'est **pas** une erreur — c'est le *template* de rcmcarddav v5, qui porte les réglages appliqués aux carnets découverts.

# 10. Reste à faire

### 10.1 vdirsyncer — agrégation Google et Infomaniak

Seule brique du plan initial encore absente. Objectif : faire remonter dans Baïkal les contacts existants sur Google et Infomaniak, pour que Baïkal devienne le point central réel et pas un silo de plus.

- Google Contacts : CardDAV via `https://www.google.com/carddav/v1/principals/EMAIL/` — nécessite un **mot de passe d'application** (compte en 2FA)
- Infomaniak : CardDAV natif, identifiants standards du compte mail
- vdirsyncer : outil CLI léger, une paire de synchronisation par source, exécuté par cron sur le VPS

**Décision à prendre avant déploiement :**

- **one-way** (Google/Infomaniak → Baïkal, lecture seule) : sans risque, mais les modifications faites dans Baïkal ne remontent pas vers Google
- **two-way** : tout reste aligné, mais un conflit ou une erreur de configuration peut propager des suppressions dans les deux sens

Recommandation : démarrer en one-way le temps de vérifier que l'import est fidèle, puis basculer si besoin.

### 10.2 Nettoyage

Quatre vhosts morts traînent dans `/etc/nginx/sites-enabled/`, vestiges des tentatives précédentes : `radicale`, `agendav`, `infcloud`, `nextcloud`. Le dernier cible le port 8082, ce qui a contraint le choix du port de Roundcube. **Attention** : le vhost nommé `radicale.juxjux.ovh` sert en réalité Readeck — ne pas le supprimer sans vérifier.

# 11. Checklist sécurité

- [x] Aucun mot de passe `CHANGE_ME_...` en production — la bascule vers SQLite a supprimé les identifiants MariaDB du compose
- [x] HTTPS forcé sur les deux vhosts (redirection 80 vers 443)
- [x] Certificats Let's Encrypt valides, renouvellement automatique certbot en place
- [x] Découverte DAV forcée en HTTPS (§6.3)
- [x] Authentification Basic **uniquement sur TLS** (§7.2)
- [x] Les deux containers n'écoutent que sur `127.0.0.1` — seul nginx les expose
- [x] Module PHP `auth/` d'InfCloud supprimé, dotfiles bloqués
- [x] Sauvegarde quotidienne des volumes Baïkal vers kDrive
- [ ] Mot de passe d'application dédié pour l'accès CardDAV Google — à faire au moment de vdirsyncer

# 12. Commandes utiles

<div id="bkmrk-1-2-3-4-5-6-7-8-9-10"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before"><div class="cm-gutter cm-lineNumbers">  
</div></div><div class="cm-content"><div class="cm-line"><span class="ͼw">\# État de la stack</span></div><div class="cm-line"><span class="ͼs">sudo</span> docker <span class="ͼs">ps</span> <span class="ͼq">--filter</span> <span class="ͼt">name</span><span class="ͼv">=</span>baikal <span class="ͼq">--filter</span> <span class="ͼt">name</span><span class="ͼv">=</span>roundcube</div><div class="cm-line">  
</div><div class="cm-line"><span class="ͼw">\# Logs</span></div><div class="cm-line"><span class="ͼs">sudo</span> docker logs roundcube <span class="ͼq">--tail</span> <span class="ͼu">50</span></div><div class="cm-line"><span class="ͼs">sudo</span> docker logs baikal <span class="ͼq">--tail</span> <span class="ͼu">50</span></div><div class="cm-line">  
</div><div class="cm-line"><span class="ͼw">\# Redéployer</span></div><div class="cm-line"><span class="ͼs">cd</span> /home/debian/baikal &amp;&amp; <span class="ͼs">sudo</span> docker compose <span class="ͼq">-p</span> baikal up <span class="ͼq">-d</span></div><div class="cm-line">  
</div><div class="cm-line"><span class="ͼw">\# Inspecter la base Baïkal</span></div><div class="cm-line"><span class="ͼs">sudo</span> docker <span class="ͼs">cp</span> baikal:/var/www/baikal/Specific/db/db.sqlite /tmp/bk.sqlite</div><div class="cm-line"><span class="ͼs">sudo</span> sqlite3 /tmp/bk.sqlite <span class="ͼ13">"SELECT COUNT(\*) FROM cards;"</span></div><div class="cm-line"><span class="ͼs">sudo</span> sqlite3 /tmp/bk.sqlite <span class="ͼ13">"SELECT COUNT(\*) FROM calendarobjects;"</span></div><div class="cm-line"><span class="ͼs">sudo</span> sqlite3 /tmp/bk.sqlite <span class="ͼ13">"SELECT id, username FROM users;"</span></div><div class="cm-line">  
</div><div class="cm-line"><span class="ͼw">\# Méthode d'authentification annoncée par Baïkal</span></div><div class="cm-line"><span class="ͼs">curl</span> <span class="ͼq">-s</span> <span class="ͼq">-D</span> <span class="ͼq">-</span> <span class="ͼq">-o</span> /dev/null <span class="ͼq">-X</span> PROPFIND <span class="ͼq">-H</span> <span class="ͼ13">'Depth: 0'</span> \</div><div class="cm-line">https://baikal.juxjux.ovh/dav.php/ | <span class="ͼs">grep</span> <span class="ͼq">-i</span> www-authenticate</div><div class="cm-line">  
</div><div class="cm-line"><span class="ͼw">\# Suivre les requêtes DAV des clients (DAVx5, InfCloud, Roundcube)</span></div><div class="cm-line"><span class="ͼs">sudo</span> tail <span class="ͼq">-f</span> /var/log/nginx/access.log | <span class="ͼs">grep</span> dav.php</div><div class="cm-line">  
</div><div class="cm-line"><span class="ͼw">\# Vérifier la découverte DAV</span></div><div class="cm-line"><span class="ͼs">curl</span> <span class="ͼq">-s</span> <span class="ͼq">-o</span> /dev/null <span class="ͼq">-w</span> <span class="ͼ13">"%{http\_code} -&gt; %{redirect\_url}\\n"</span> \</div><div class="cm-line">https://baikal.juxjux.ovh/.well-known/carddav</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div># 13. Choix d'architecture — pourquoi ces outils

**Radicale** écarté : stockage fichier brut, pas de vraie gestion. La stack Radicale/InfCloud initialement envisagée n'a pas été mise en place — seul InfCloud a finalement été retenu, en client de Baïkal.

**Nextcloud** écarté : trop lourd pour l'usage visé (PHP-FPM + base + Redis + écosystème complet), alors que le VPS fait déjà tourner PostGIS, Metabase, BookStack, FreshRSS.

**Baïkal** retenu : léger, CardDAV et CalDAV natifs, interface d'administration suffisante. Développé par des bénévoles sous l'organisation sabre-io (mainteneurs de SabreDAV, la librairie sous-jacente) — pas de société commerciale derrière, mais communauté active et outil robuste pour un usage personnel sur la durée.

**Roundcube** retenu comme interface de saisie des contacts, Baïkal n'ayant pas de vraie UI de consultation : léger, traduit en français, projet actif (version 1.7.2 en août 2026). A rejoint la famille Nextcloud en 2023 tout en restant indépendant, sous licence GPL. Réserves constatées à l'usage : il impose une authentification IMAP (§6.4) et ne couvre pas l'agenda (§6.2).

**InfCloud** retenu pour l'agenda web : sans backend, donc sans coût mémoire ni surface d'attaque serveur, et couvre agenda et contacts. Ancien mais fonctionnel.

**SOGo** envisagé et écarté : suite complète et maintenue, mais environ 500 Mo de RAM et une base PostgreSQL sur un VPS déjà en pression mémoire, et il aurait fait doublon avec Roundcube.

**Thunderbird containerisé** (VNC/noVNC) envisagé puis écarté : fonctionnel mais plus lourd et moins adapté au multi-accès qu'un vrai webmail.

# 2-MCP CalDAV pour Claude Code — un agenda Baïkal partagé par les deux PC

<div id="bkmrk--3" style="clear: left;">  
</div># MCP CalDAV pour Claude Code

**État au 16 septembre 2026 — opérationnel sur les deux PC.** Cycle complet validé le 15/09 sur NEXTE : création de calendrier, écriture d'un événement, relecture, visibilité sur le téléphone (DAVx5 → Fossify) **et dans InfCloud**, suppression. Installé sur le PC perso le 16/09 (§7.8), avec un écart de méthode : venv Python sur `D:` au lieu de `uv`.

Prérequis : l'infrastructure Baïkal décrite en page 1 de ce chapitre (Baïkal en Docker sur juxjux.ovh, Basic auth sur TLS, utilisateur DAV `Julien`).

---

## 1. Décision d'architecture

**Un seul Baïkal, sur juxjux.ovh, partagé par les deux Claude Code (PC perso et PC NEXTE).** C'est la seule exception assumée à l'étanchéité entre l'univers Jux et l'univers Alteris/NEXTE.

Pourquoi ne pas dupliquer Baïkal sur le VPS Alteris :

- Baïkal est posé comme *source de vérité unique* (page 1). Deux Baïkal = deux vérités, et le problème d'origine (agendas dispersés) se recrée.
- Deux flux dans DAVx5 fonctionnent, mais les deux calendriers ne se voient pas : les conflits d'horaire entre perso et pro deviennent invisibles.
- Doublement de la maintenance (certificat, sauvegarde, compose, InfCloud) sur un VPS déjà chargé.

Pourquoi l'exception est acceptable : l'étanchéité protège les **fichiers et données de mission**. Un agenda n'est pas une donnée de mission, c'est un service de la personne, comme le téléphone. Le PC NEXTE devient un client CalDAV de plus, au même titre que DAVx5 ; aucune donnée Jux n'entre sur NEXTE, aucune donnée Alteris ne sort vers juxjux. Seul l'identifiant Baïkal traverse — il est dans le Vault 2603.

## 2. Modèle : un utilisateur, deux calendriers

<table id="bkmrk-calendrier-url-usage"><thead><tr><th>Calendrier</th><th>URL</th><th>Usage</th><th>Couleur</th><th>PC qui y écrit par défaut</th></tr></thead><tbody><tr><td>`Default calendar`</td><td>`https://baikal.juxjux.ovh/dav.php/calendars/Julien/default/`</td><td>Personnel (Julien)</td><td>(installeur)</td><td>PC perso</td></tr><tr><td>`NEXTE`</td><td>`https://baikal.juxjux.ovh/dav.php/calendars/Julien/nexte/`</td><td>Professionnel NEXTE (pas Alteris)</td><td>`#1E88E5` bleu</td><td>PC NEXTE</td></tr></tbody></table>

Même compte `Julien` pour les deux, donc les deux calendriers apparaissent ensemble dans DAVx5, Fossify et InfCloud, chacun avec sa couleur.

**Règle de lecture/écriture du MCP :** chaque PC **écrit** dans son calendrier par défaut (`CALDAV_DEFAULT_CALENDAR`) mais **lit les deux** — sinon Claude placerait un rendez-vous pro sur un créneau perso. L'écriture dans l'autre calendrier reste possible en le nommant explicitement (`calendar="Default calendar"`).

Le calendrier `NEXTE` a été créé le 15/09/2026 par le MCP lui-même (`MKCALENDAR`, outil `create_calendar`). Dans DAVx5, il faut ensuite rafraîchir la liste des calendriers du compte et le cocher pour qu'il se synchronise. Dans InfCloud, il faut l'inscrire dans les réglages stockés sur le serveur — voir §5.7, c'est désormais fait automatiquement par `create_calendar`.

## 3. Le serveur MCP

Aucun serveur MCP CalDAV communautaire mûr : serveur maison, ~400 lignes de Python.

- Fichier : `C:\Users\nexte\Nexte Claude\MCP CalDAV\260915-mcp_caldav_server.py`
- Script autonome **PEP 723** : dépendances déclarées en tête de fichier, résolues par `uv run --script`. Aucune installation Python système nécessaire (il n'y en a pas sur NEXTE, seulement celui de QGIS) — `uv` est déjà là pour le MCP QGIS.
- Dépendances : `mcp>=1.2,<2` (FastMCP), `caldav` (3.3 au 15/09), `icalendar`, `python-dateutil`.
- Configuration par variables d'environnement, passées par `claude mcp add -e` :

<table id="bkmrk-variable-valeur-next"><thead><tr><th>Variable</th><th>Valeur NEXTE</th><th>Rôle</th></tr></thead><tbody><tr><td>`CALDAV_URL`</td><td>`https://baikal.juxjux.ovh/dav.php/`</td><td>racine DAV de Baïkal</td></tr><tr><td>`CALDAV_USER`</td><td>`Julien`</td><td>utilisateur DAV — **J majuscule**, la casse compte</td></tr><tr><td>`CALDAV_PASSWORD`</td><td>Vault 2603</td><td>mot de passe de l'utilisateur DAV (**pas** celui de l'admin Baïkal)</td></tr><tr><td>`CALDAV_DEFAULT_CALENDAR`</td><td>`NEXTE`</td><td>calendrier d'écriture par défaut (`default` sur le PC perso)</td></tr><tr><td>`CALDAV_TZ`</td><td>`Europe/Paris` (défaut)</td><td>fuseau des heures saisies sans fuseau, et fuseau posé sur les calendriers créés</td></tr></tbody></table>

Le script ne journalise jamais le mot de passe ; s'il manque, il le signale sur stderr sans en dire plus.

### Outils exposés

<table id="bkmrk-outil-r%C3%B4le-list_cale"><thead><tr><th>Outil</th><th>Rôle</th></tr></thead><tbody><tr><td>`list_calendars`</td><td>calendriers du compte, avec marquage du calendrier par défaut</td></tr><tr><td>`create_calendar(name, cal_id, color)`</td><td>`MKCALENDAR`, puis pose `calendar-color` / `calendar-order` / `calendar-timezone` (palette par défaut) et **inscrit le calendrier dans les réglages InfCloud** (§5.7) ; refuse si le nom existe déjà</td></tr><tr><td>`infcloud_sync_collections`</td><td>inscrit dans les réglages InfCloud tous les calendriers du compte qui n'y figurent pas — pour un calendrier créé ailleurs (DAVx5, admin Baïkal)</td></tr><tr><td>`list_events(start, end, calendar, all_calendars)`</td><td>événements sur une période (défaut : aujourd'hui → +14 j), **tous calendriers** sauf si `calendar` est nommé ; occurrences récurrentes développées</td></tr><tr><td>`search_events(text, calendar)`</td><td>recherche plein texte (résumé, lieu, description), fenêtre −6/+12 mois</td></tr><tr><td>`get_event(uid, calendar)`</td><td>détail + iCalendar brut</td></tr><tr><td>`create_event(summary, start, end, calendar, all_day, location, description, rrule)`</td><td>`end` absent → +1 h (ou +1 jour en journée entière) ; heures sans fuseau = Europe/Paris ; dates ISO ou `jj/mm/aaaa`, heures `16:00` ou `16h00`</td></tr><tr><td>`update_event(uid, …)`</td><td>ne modifie que les champs fournis ; incrémente `SEQUENCE` et `LAST-MODIFIED`</td></tr><tr><td>`delete_event(uid, calendar)`</td><td>suppression définitive — **uniquement après confirmation explicite de Julien**</td></tr><tr><td>`free_slots(start, end, duration_minutes, day_start, day_end, weekdays_only)`</td><td>créneaux libres tous calendriers confondus, dans la plage horaire quotidienne indiquée</td></tr></tbody></table>

## 4. Enregistrement dans Claude Code (PC NEXTE)

Portée utilisateur (`-s user`), comme le MCP QGIS. À lancer dans un terminal, en remplaçant `<MDP>` par le mot de passe de l'utilisateur DAV `Julien` (Vault 2603) :

<div id="bkmrk-1-claude-mcp-add--s-"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">claude mcp add -s user caldav -e CALDAV_URL=https://baikal.juxjux.ovh/dav.php/ -e CALDAV_USER=Julien -e CALDAV_DEFAULT_CALENDAR=NEXTE -e 'CALDAV_PASSWORD=&lt;MDP&gt;' -- "C:\Users\nexte\.local\bin\uv.exe" run --script "C:\Users\nexte\Nexte Claude\MCP CalDAV\260915-mcp_caldav_server.py"</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>Puis :

<div id="bkmrk-1-claude-mcp-list"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">claude mcp list</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>qui doit afficher `caldav: … - ✔ Connected`.

Pour refaire l'enregistrement (mot de passe changé, mauvais calendrier par défaut) : `claude mcp remove -s user caldav` puis la commande `add`.

**Où va le secret :** `claude mcp add` l'écrit **en clair** dans `C:\Users\nexte\.claude.json` — même niveau de protection que le MCP postgres. Le mot de passe reste archivé dans le Vault 2603 ; il n'a pas à figurer dans une conversation ni dans BookStack.

## 5. Pièges rencontrés le 15/09/2026

### 5.1 Les chevrons du gabarit ne font pas partie du mot de passe

Une commande donnée avec `CALDAV_PASSWORD=<MDP>` a été lancée telle quelle, puis avec `<vrai-mdp>` chevrons compris. Dans les deux cas Baïkal répond 401. Vérification sans afficher le secret : longueur de la valeur stockée, premier caractère, présence des caractères spéciaux attendus.

### 5.2 Mot de passe admin ≠ mot de passe utilisateur DAV

Baïkal a deux comptes distincts : l'admin (`/admin/`) et l'utilisateur DAV (`Julien`). Le premier essai a été fait avec le mot de passe admin → 401 systématique, avec `Julien` comme avec `julien`, alors que le serveur annonçait bien `Basic realm="sabre/dav"`. Le bon mot de passe est celui saisi dans DAVx5 et InfCloud. Test de référence qui distingue « serveur joignable » de « identifiants acceptés » :

<div id="bkmrk-1-curl--s--o-%2Fdev%2Fnu"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">curl -s -o /dev/null -w "%{http_code}\n" -X PROPFIND -H "Depth: 0" https://baikal.juxjux.ovh/dav.php/</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>401 sans identifiants = normal ; 401 avec identifiants = mauvais couple utilisateur/mot de passe.

### 5.3 Caractères spéciaux dans PowerShell

Le mot de passe contient `%` et `@` : passer la variable **entre apostrophes simples** (`'CALDAV_PASSWORD=…'`), pas entre guillemets. Vérifié : la valeur stockée est intacte.

### 5.4 SDK `mcp` 2.x

La 2.x du SDK Python a renommé `FastMCP` en `MCPServer` et cassé l'import `mcp.server.fastmcp`. Le script épingle `mcp<2`. À revoir si on migre un jour vers la 2.x.

### 5.5 Reprise de session ≠ nouvelle session

Un serveur MCP est chargé au démarrage de la session. Après `claude mcp add`, une session **reprise** dans l'app ne voit pas le nouveau serveur alors que `claude mcp list` le donne connecté. Il faut une **nouvelle** session. Entre-temps, le script peut être piloté directement (import du module + lecture de l'env dans `.claude.json`) — c'est ainsi que le calendrier `NEXTE` et l'événement test ont été créés.

### 5.6 Rappels de la page 1 qui s'appliquent au MCP

- Racine DAV `/dav.php/`, pas `/caldav.php/`.
- `Julien` en majuscule dans les URLs.
- Basic sur TLS : la bibliothèque `caldav` gère la négociation 401 → Basic toute seule.

### 5.7 Un calendrier créé hors InfCloud n'apparaît jamais dans InfCloud

**Symptôme :** `NEXTE` visible dans DAVx5 et Fossify, mais absent d'InfCloud, même après déconnexion, `Ctrl+Maj+R` et reconnexion.

**Fausse piste écartée en premier :** le calendrier créé par `MKCALENDAR` nu n'avait ni `calendar-color`, ni `calendar-order`, ni `calendar-timezone` (404 sur ces trois propriétés, alors que le calendrier de l'installeur les a). Elles ont été posées par PROPPATCH — nécessaire pour l'homogénéité, mais **insuffisant**.

**Cause réelle :** `config.js` d'InfCloud a `settingsAccount: true`. InfCloud stocke alors ses réglages **sur le serveur**, dans la propriété `{http://inf-it.com/ns/dav/}settings` du principal (`/dav.php/principals/Julien/`), sous forme de JSON. Ce JSON contient `loadedcalendarcollections` et `activecalendarcollections` (idem pour les tâches), figés à la première session — donc réduits à `default/`. InfCloud **ne charge que les collections de cette liste** ; un nouveau calendrier n'y entre jamais tout seul.

Diagnostic (avec identifiants) :

<div id="bkmrk-1-2-propfind-%2Fdav.ph"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 46.4px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div><div class="cm-gutterElement" style="height: 19.2px;">2</div></div></div><div class="cm-content"><div class="cm-line">PROPFIND /dav.php/principals/Julien/ Depth: 0</div><div class="cm-line">&lt;D:propfind xmlns:D="DAV:" xmlns:I="http://inf-it.com/ns/dav/"&gt;&lt;D:prop&gt;&lt;I:settings/&gt;&lt;/D:prop&gt;&lt;/D:propfind&gt;</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>puis lire `loadedcalendarcollections` dans le JSON retourné.

Correctif : relire le JSON, ajouter l'URL du calendrier aux quatre listes (`loadedcalendarcollections`, `activecalendarcollections`, `loadedtodocollections`, `activetodocollections`), réécrire la propriété par PROPPATCH, puis **déconnexion/reconnexion** InfCloud (les réglages sont lus au login). Sauvegarde du JSON d'origine : `MCP CalDAV\260915-infcloud_settings_backup.json`.

Depuis le 15/09, `create_calendar` fait tout cela d'emblée, et `infcloud_sync_collections` rattrape un calendrier créé ailleurs. Le `config.js` lui-même n'est pas en cause (`additionalResources: []`, pas de liste figée).

### 5.8 Le parseur de dates lisait 2026-10-02 comme le 10 février

`dateutil.parser.parse(..., dayfirst=True)` — voulu pour accepter `02/10/2026` — inverse aussi jour et mois d'une date **ISO** quand les deux valeurs sont ≤ 12 : `2026-10-02` devient le 10 février, `2026-10-09` le 10 septembre, `2026-10-12` le 10 décembre. `2026-10-13` passe, ce qui rend le bug intermittent et sournois.

Constaté lors du premier blocage de créneaux (6 dates, 3 fausses). Correctif dans `_parse_dt` : `datetime.fromisoformat` d'abord, dateutil en jour-premier seulement en repli ; `16h00` / `14h` sont normalisés en `16:00` / `14:00`. Les trois événements erronés ont été supprimés et recréés.

**Règle :** toujours relire les dates renvoyées par `create_event` (le résultat affiche `start`/`end` en ISO avec fuseau) avant d'annoncer qu'un créneau est bloqué.

## 6. Vérifications effectuées le 15/09/2026

<table id="bkmrk-test-r%C3%A9sultat-propfi"><thead><tr><th>Test</th><th>Résultat</th></tr></thead><tbody><tr><td>`PROPFIND /dav.php/` sans identifiants</td><td>401, `Basic realm="sabre/dav"`</td></tr><tr><td>`list_calendars`</td><td>`Default calendar` visible</td></tr><tr><td>`create_calendar("NEXTE", cal_id="nexte")`</td><td>créé, URL `…/calendars/Julien/nexte/`</td></tr><tr><td>`list_events` 14 jours, tous calendriers</td><td>1 événement perso lu</td></tr><tr><td>`create_event` dans NEXTE (16/09 10:00–10:30, Europe/Paris)</td><td>écrit, relu par `get_event` avec fuseau `+02:00`</td></tr><tr><td>Synchronisation DAVx5 → Fossify</td><td>événement visible sur le téléphone après avoir coché NEXTE</td></tr><tr><td>`delete_event` par UID</td><td>supprimé, calendrier NEXTE vide</td></tr><tr><td>PROPPATCH couleur/ordre/fuseau sur NEXTE</td><td>200, relu</td></tr><tr><td>Inscription de NEXTE dans les réglages InfCloud</td><td>200 ; NEXTE visible dans InfCloud après reconnexion</td></tr><tr><td>`infcloud_sync_collections` après correction</td><td>`added: []` — idempotent</td></tr><tr><td>`create_calendar("NEXTE")` une seconde fois</td><td>refusé, « existe déjà »</td></tr><tr><td>`claude mcp list`</td><td>`caldav … ✔ Connected`</td></tr><tr><td>6 créneaux « Tourrettes PLU (validation en attente) » dans NEXTE (02→16/10)</td><td>créés, relus aux bonnes dates après correctif §5.8 ; aucun conflit perso</td></tr></tbody></table>

## 7. Installation sur le PC perso

Objectif : que le Claude Code du PC perso lise les deux calendriers et écrive dans l'un ou l'autre, avec le **même** serveur Baïkal et le **même** compte `Julien`. Aucune configuration côté Baïkal : le compte voit déjà les deux calendriers. Tout se passe sur le PC perso.

### 7.1 Copier le script

Source (PC NEXTE) : `C:\Users\nexte\Nexte Claude\MCP CalDAV\260915-mcp_caldav_server.py` — prendre la version courante, elle contient les correctifs du 15/09 (§5.7 InfCloud, §5.8 dates).

Destination recommandée : `Syncthing/Jux_univers/Jux-scripts/Baikal-Contacts/260915-mcp_caldav_server.py`, à côté d'`infcloud-config.js` — c'est le dossier où sont déjà versionnés les fichiers Baïkal (page 1, §4). Le script est autonome : un seul fichier, pas de dépendance à copier.

Point d'attention : le fichier ne doit pas être dans un coffre Cryptomator — un MCP se lance au démarrage de Claude Code, coffre fermé ou non (règle « aucun script ne dépend d'un coffre déverrouillé »).

### 7.2 Vérifier `uv`

<div id="bkmrk-1-uv---version"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">uv --version</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>Si absent :

<div id="bkmrk-1-powershell--c-%22irm"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">powershell -c "irm https://astral.sh/uv/install.ps1 | iex"</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>puis rouvrir le terminal. `uv` télécharge lui-même un Python et les dépendances au premier lancement (~50 paquets, une dizaine de secondes) ; aucune installation Python système n'est nécessaire.

Repérer le chemin complet de `uv.exe` (`(Get-Command uv).Source` en PowerShell) — l'utiliser en absolu dans la commande d'enregistrement, comme sur NEXTE, évite les surprises de PATH au lancement par Claude Code.

**Fait autrement sur le PC perso (16/09)** : pas d'`uv`, un venv sur `D:` — voir §7.8 pour la raison.

### 7.3 Choisir le calendrier par défaut

<table id="bkmrk-choix-caldav_default"><thead><tr><th>Choix</th><th>`CALDAV_DEFAULT_CALENDAR`</th><th>Comportement</th></tr></thead><tbody><tr><td>**Recommandé**</td><td>`default`</td><td>le PC perso écrit dans le calendrier perso sauf mention contraire ; symétrique de NEXTE sur le PC pro</td></tr><tr><td>Variante</td><td>`NEXTE`</td><td>le PC perso se comporte exactement comme le PC NEXTE</td></tr></tbody></table>

Dans les deux cas, la **lecture** porte sur les deux calendriers, et l'écriture dans l'autre calendrier reste possible en le nommant (« ajoute ça dans NEXTE »).

### 7.4 Enregistrer le MCP

Dans un terminal (PowerShell), en remplaçant `<MDP>` par le mot de passe de l'utilisateur DAV `Julien` (Vault 2603) et `<chemin>` par le dossier de destination :

<div id="bkmrk-1-claude-mcp-add--s--1"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">claude mcp add -s user caldav -e CALDAV_URL=https://baikal.juxjux.ovh/dav.php/ -e CALDAV_USER=Julien -e CALDAV_DEFAULT_CALENDAR=default -e 'CALDAV_PASSWORD=&lt;MDP&gt;' -- "&lt;chemin complet de uv.exe&gt;" run --script "&lt;chemin&gt;\260915-mcp_caldav_server.py"</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>Rappels des pièges §5 :

- mot de passe de l'**utilisateur DAV** `Julien`, pas celui de l'admin Baïkal (§5.2) ;
- **sans chevrons** autour de la valeur (§5.1) ;
- **apostrophes simples** autour de `CALDAV_PASSWORD=…` à cause des `%` et `@` (§5.3) ;
- `Julien` avec un J majuscule (§5.6).

Vérifier :

<div id="bkmrk-1-claude-mcp-list-1"><div class="cm-editor ͼ1 ͼ3 ͼ4 ͼo"><div class="cm-announced">  
</div><div class="cm-scroller" tabindex="-1"><div class="cm-gutters cm-gutters-before" style="min-height: 27.2px;"><div class="cm-gutter cm-lineNumbers"><div class="cm-gutterElement" style="height: 0px; visibility: hidden;">  
</div><div class="cm-gutterElement" style="height: 19.2px; margin-top: 4px;">1</div></div></div><div class="cm-content"><div class="cm-line">claude mcp list</div></div><div class="cm-layer cm-layer-above cm-cursorLayer">  
</div><div class="cm-layer cm-selectionLayer">  
</div></div></div></div>→ `caldav: … - ✔ Connected`. Si `Failed to connect` : lancer à la main `uv run --script "<chemin>\260915-mcp_caldav_server.py"` dans le terminal pour voir l'erreur (dépendance, chemin, Python).

Pour corriger une valeur : `claude mcp remove -s user caldav` puis relancer la commande `add`.

### 7.5 Nouvelle session et test

1. Ouvrir une **nouvelle** session Claude Code — pas une reprise (§5.5).
2. Demander `list_calendars` : les deux calendriers doivent apparaître, `Default calendar` marqué par défaut.
3. Écrire un événement test dans NEXTE (« crée un test demain 10 h dans NEXTE »), le vérifier sur le téléphone ou dans InfCloud, puis le faire supprimer.

### 7.6 Ce qui n'est PAS à faire

- Rien dans l'admin Baïkal : pas de second utilisateur, pas de partage de calendrier.
- Rien dans DAVx5 ni InfCloud : ils voient déjà les deux calendriers.
- Ne pas dupliquer Baïkal sur un autre VPS (§1).
- Ne pas copier le mot de passe dans BookStack, dans un script ou dans une conversation : il vit dans le Vault 2603 et dans `.claude.json` du PC.

### 7.7 Entretien à deux PC

Le script existe en deux exemplaires (NEXTE et perso). Toute correction faite sur l'un doit être recopiée sur l'autre — c'est le prix d'un serveur maison hors dépôt. Convention : le fichier daté reste `260915-…` tant que l'API des outils ne change pas ; une refonte produit un nouveau fichier daté et une mise à jour des deux enregistrements MCP.

### 7.8 Installation effective sur le PC perso — 16/09/2026

Faite depuis une session Claude Code du poste `julie`, après dépôt du script par Julien. Vérifiée en session : `list_calendars` rend *Default calendar* (par défaut) et *NEXTE*, `list_tasks` lit les deux tâches ouvertes de NEXTE.

<table id="bkmrk-7.8-table"><thead><tr><th>Élément</th><th>Valeur sur le PC perso</th></tr></thead><tbody><tr><td>Script</td><td>`D:\Syncthing\Jux_univers\Jux-scripts\Baikal-Contacts\260915-mcp_caldav_server.py` — dans l'arbre Syncthing, hors coffre</td></tr><tr><td>Interpréteur</td><td>`D:\Python\venv-caldav-mcp\Scripts\python.exe` (venv créé par `py -3.12 -m venv`, puis `pip install "mcp>=1.2,<2" "caldav>=1.4" "icalendar>=6" python-dateutil` — mcp 1.30, caldav 3.3, icalendar 7.3)</td></tr><tr><td>Enregistrement</td><td>à la main dans `C:\Users\julie\.claude.json`, clé `mcpServers.caldav` (portée utilisateur, `type: stdio`, `command` = python du venv en absolu, `args` = chemin du script, `env` = les 4 variables). Sauvegarde : `.claude.json.bak-260916-caldav`</td></tr><tr><td>`CALDAV_DEFAULT_CALENDAR`</td><td>`default` — le PC perso écrit dans le perso, lit les deux</td></tr></tbody></table>

**Pourquoi pas `uv` ni `pip` dans le Python système.** Sur ce poste, Claude Code tourne dans un conteneur MSIX : tout ce qu'une session écrit sous `%LOCALAPPDATA%` ou `%APPDATA%` est redirigé vers `…\Packages\Claude_pzs8sxrjxfjjc\LocalCache\`, invisible du reste du système (page 282). Or le Python 3.12 du poste est installé sous `%LOCALAPPDATA%\Programs\Python`, et `uv` met son cache dans `%LOCALAPPDATA%\uv` : un `pip install` ou un premier `uv run` lancés depuis la session auraient produit une installation fantôme. Un venv sur `D:` est hors redirection, et cohérent avec les six autres MCP Python du poste (enregistrés avec un `python.exe` en chemin absolu, page 282). `uv` reste valable sur NEXTE, où Claude Code n'est pas dans un conteneur.

**Pas de `claude mcp add`** : le CLI `claude` n'est pas dans le PATH de ce poste (application de bureau). L'entrée est écrite directement dans `.claude.json`, comme pour Mealie le 13/09. Le mot de passe y est en clair, au même titre que sur NEXTE.

**Correctif apporté au script (à recopier sur NEXTE, §7.7).** `list_calendars` marquait le calendrier par défaut en comparant `CALDAV_DEFAULT_CALENDAR` au seul nom affiché. Avec `default`, le nom affiché est *Default calendar* : aucun calendrier n'était marqué (`default: false` partout), alors que l'écriture fonctionnait — `_find_calendar`, lui, se rabat sur le dernier segment d'URL. Invisible sur NEXTE parce que nom et segment coïncident (`NEXTE`/`nexte`). `list_calendars` applique désormais la même règle : nom affiché **ou** segment d'URL, insensible à la casse. Une ligne changée, même fichier daté `260915-…` (l'API des outils ne bouge pas).

**Chargement en session** : après l'écriture dans `.claude.json`, l'application de bureau a dû être fermée et rouverte — une session reprise ne voit pas le nouveau serveur (§5.5). Au retour, les 17 outils `mcp__caldav__*` étaient présents et `list_calendars` a répondu par le MCP.

## 8. Règles d'usage pour Claude

- Lire **les deux** calendriers avant de proposer un créneau (`list_events` sans `calendar`, ou `free_slots`).
- Écrire dans le calendrier par défaut du PC ; n'écrire dans l'autre que sur demande explicite.
- Ne jamais appeler `delete_event` sans confirmation explicite de Julien dans la conversation.
- Ne jamais afficher, journaliser ni répéter `CALDAV_PASSWORD` ; diagnostiquer sur les longueurs et les codes HTTP.
- Si un calendrier manque dans InfCloud : `infcloud_sync_collections`, puis déconnexion/reconnexion — avant toute autre hypothèse.
- Relire les dates renvoyées par `create_event` avant d'annoncer un créneau bloqué (§5.8).

# 3 - gestionnaires de tâches - Tasks.org sur Baikal - Infcloud et DAVx5

<div id="bkmrk--5" style="clear: left;">  
</div># Gestionnaire de tâches — Tasks.org sur Baïkal, InfCloud et DAVx5

**État au 15 septembre 2026 — opérationnel de bout en bout.** Cycle validé dans les deux sens : une tâche créée par le MCP est apparue dans Tasks.org, y a été cochée, et le MCP l'a relue COMPLETED ; une tâche saisie dans Tasks.org a été lue par le MCP ; les deux sont visibles dans InfCloud (§6).

Décision : **on valorise au maximum l'existant — Baïkal, InfCloud, DAVx5.** Les tâches vivent dans les mêmes collections que les événements, aucun composant serveur n'est ajouté. Seul ajout : Tasks.org sur le téléphone, comme écran des tâches que DAVx5 lui dépose.

---

## 1. Ce que l'existant sait déjà faire

<table id="bkmrk-composant-t%C3%A2ches-%28vt"><thead><tr><th>Composant</th><th>Tâches (`VTODO`)</th><th>Constat</th></tr></thead><tbody><tr><td>**Baïkal**</td><td>oui, nativement</td><td>les deux collections `default/` et `nexte/` annoncent `VEVENT` **et** `VTODO` (vérifié par PROPFIND `supported-calendar-component-set`)</td></tr><tr><td>**InfCloud**</td><td>oui, onglet *Tâches*</td><td>les réglages serveur contiennent déjà `loadedtodocollections` / `activetodocollections` avec les deux calendriers (page 2, §5.7)</td></tr><tr><td>**DAVx5**</td><td>oui, synchronise les `VTODO`</td><td>**mais Android n'a pas de fournisseur de tâches natif** : DAVx5 ne peut les déposer que dans une appli tierce — Tasks.org, jtx Board ou OpenTasks. Sans elle, l'option « Tâches » n'apparaît même pas dans le compte</td></tr><tr><td>**MCP CalDAV**</td><td>oui depuis le 15/09</td><td>7 outils `*_task` ajoutés au script (§3)</td></tr></tbody></table>

## 2. Choix du client Android

### Fossify Calendar — écarté

La fonction « Tâche » de Fossify est **locale** : c'est un événement d'un type particulier dans sa propre base, jamais écrit en `VTODO`, jamais synchronisé (héritage de Simple Calendar, dont la synchro CalDAV ne couvre que les événements). Une tâche saisie là n'atteint ni Baïkal, ni InfCloud, ni le MCP — et on croirait qu'elle est synchronisée. Même famille de piège que le plugin calendar de Roundcube (page 1, §6.2).

Fossify reste le client **événements** ; il n'affiche pas les tâches — chaque appli dans son rôle.

### Tasks.org (F-Droid, 15.x) — retenu

<table id="bkmrk-avantages-inconv%C3%A9nie"><thead><tr><th> </th><th>Avantages</th><th>Inconvénients</th></tr></thead><tbody><tr><td>Synchro</td><td>`VTODO` complet : échéance, priorité, récurrence, rappels, sous-tâches, étiquettes ; CalDAV direct **ou** via DAVx5</td><td>deux chemins possibles → en choisir un seul (§4)</td></tr><tr><td>Fonctionnel</td><td>vraie appli de tâches : listes = collections CalDAV, filtres, widgets, tri par échéance</td><td>plus riche que nécessaire pour un usage simple — d'où le §5</td></tr><tr><td>Projet</td><td>actif, GPL, version F-Droid sans services Google</td><td>pas de synchro Google Tasks sur F-Droid — sans importance ici</td></tr><tr><td>Cohérence</td><td>les tâches apparaissent dans InfCloud et sont lisibles/écrites par le MCP</td><td>—</td></tr></tbody></table>

Alternative connue : **jtx Board** (bitfire, même éditeur que DAVx5) — tâches + journaux (`VJOURNAL`), intégration DAVx5 très propre, mais moins de fonctions de gestion de tâches. À garder en tête si les notes datées deviennent un besoin ; Baïkal gère `VJOURNAL`.

### Branchement : via DAVx5, pas en CalDAV direct

Tasks.org sait parler CalDAV tout seul, mais on le branche **comme fournisseur de tâches de DAVx5** : un seul compte Baïkal sur le téléphone, une seule planification de synchro, un seul mot de passe stocké. DAVx5 détecte Tasks.org à l'installation et fait apparaître « Tâches » dans le compte. **Rien à saisir dans Tasks.org** : ni compte, ni URL, ni mot de passe.

### Emplacement des tâches : dans les calendriers existants

Pas de collection dédiée aux tâches. La séparation perso/pro est portée par la collection (`Default calendar` / `NEXTE`), InfCloud est déjà configuré ainsi, et Tasks.org montre deux listes portant ces noms. L'échéance d'une tâche reste visible à côté des rendez-vous dans InfCloud.

## 3. Outils tâches du MCP CalDAV

Ajoutés le 15/09/2026 au script `260915-mcp_caldav_server.py` (page 2, §3). Même mécanique que les événements : **écriture dans le calendrier par défaut du PC, lecture des deux**.

<table id="bkmrk-outil-r%C3%B4le-list_task"><thead><tr><th>Outil</th><th>Rôle</th></tr></thead><tbody><tr><td>`list_tasks(calendar, include_completed, due_before)`</td><td>tâches ouvertes de tous les calendriers (ou d'un seul) ; `include_completed` ajoute terminées/annulées ; `due_before` filtre sur l'échéance ; tri échéance puis priorité</td></tr><tr><td>`get_task(uid, calendar)`</td><td>détail + iCalendar brut</td></tr><tr><td>`create_task(summary, due, calendar, priority, description, start, categories, rrule)`</td><td>`due` = date seule (journée) ou date-heure (Europe/Paris) ; `priority` 0 aucune / 1 haute / 5 moyenne / 9 basse (convention Tasks.org) ; `rrule` ex. `FREQ=WEEKLY;BYDAY=MO`</td></tr><tr><td>`update_task(uid, …)`</td><td>ne modifie que les champs fournis ; `due=""` retire l'échéance ; `status` ∈ NEEDS-ACTION / IN-PROCESS / COMPLETED / CANCELLED ; `percent_complete`</td></tr><tr><td>`complete_task(uid, calendar)`</td><td>STATUS COMPLETED, COMPLETED = maintenant, 100 % ; une tâche récurrente est close pour l'occurrence courante et la suivante est générée</td></tr><tr><td>`reopen_task(uid, calendar)`</td><td>retour à NEEDS-ACTION, COMPLETED et pourcentage retirés</td></tr><tr><td>`delete_task(uid, calendar)`</td><td>suppression définitive — **uniquement après confirmation explicite de Julien**</td></tr></tbody></table>

Champs renvoyés : `calendar`, `uid`, `summary`, `status`, `due`, `start`, `priority`, `percent_complete`, `completed`, `description`, `categories`, `rrule`, `last_modified`.

## 4. Configuration du téléphone — faite le 15/09

1. Tasks.org installé depuis F-Droid.
2. DAVx5 → compte Baïkal : calendriers **et** tâches activés pour `NEXTE` et `Default calendar` (DAVx5 récent les montre dans le même onglet, avec une icône différente — il faut cocher les deux entrées de chaque collection).
3. Tasks.org → Paramètres → Synchronisation : le compte « DAVx5 » est actif. Aucun compte CalDAV ajouté dans Tasks.org lui-même — ce serait le second chemin de synchro qu'on a écarté.

## 5. Utiliser Tasks.org au quotidien

Tasks.org intimide parce qu'il **s'ouvre sur un filtre** (« Ma journée » ou « Boîte de réception »), pas sur les listes. Une tâche due demain n'y apparaît pas ; il faut aller la chercher dans sa liste ou dans « Toutes les tâches ». Le vocabulaire à connaître :

### 5.1 Listes et filtres

<table id="bkmrk-notion-ce-que-c%27est-"><thead><tr><th>Notion</th><th>Ce que c'est</th><th>Où</th></tr></thead><tbody><tr><td>**Liste**</td><td>une collection CalDAV — ici `NEXTE` et `Default calendar`</td><td>menu ≡ (icône en bas à gauche, ou balayage depuis le bord gauche), bloc au nom du compte DAVx5</td></tr><tr><td>**Filtre**</td><td>une vue transversale sur **toutes** les listes</td><td>menu ≡, en haut</td></tr><tr><td>« Toutes les tâches »</td><td>l'ensemble des tâches ouvertes, perso + pro, dans une seule vue — **la vue du quotidien**</td><td>filtre</td></tr><tr><td>« Ma journée »</td><td>échues aujourd'hui ou en retard, tous calendriers — l'équivalent de `list_tasks(due_before=aujourd'hui)`</td><td>filtre</td></tr><tr><td>« Récemment modifiées »</td><td>ce qui a bougé, utile après une synchro</td><td>filtre</td></tr><tr><td>**Filtre personnalisé**</td><td>composition de critères : listes, étiquettes, priorité, échéance (« cette semaine »), état</td><td>menu ≡ → Filtres → + ; devient une entrée du menu</td></tr><tr><td>**Widget**</td><td>un widget d'écran d'accueil par liste **ou** par filtre — « Toutes les tâches » en widget montre donc les deux calendriers</td><td>appui long sur l'écran d'accueil</td></tr></tbody></table>

Pour distinguer perso et pro dans la vue commune : Paramètres → Apparence → **regrouper par liste** (ou afficher le nom de la liste sous chaque tâche), et une **couleur par liste** (appui long sur la liste → couleur), affichée en pastille. La couleur est locale à Tasks.org ; celle d'InfCloud est la `calendar-color` de la collection (page 2, §5.7).

### 5.2 Le déclenchement — quand une tâche se rappelle à toi

C'est la partie à comprendre avant de s'en remettre à l'appli :

<table id="bkmrk-situation-notificati"><thead><tr><th>Situation</th><th>Notification ?</th></tr></thead><tbody><tr><td>Tâche **sans échéance** (ex. « reprendre des fruits »)</td><td>**jamais**. Elle n'existe que dans les listes et « Toutes les tâches ». C'est un pense-bête, pas un rappel</td></tr><tr><td>Échéance **date seule** (`DUE;VALUE=DATE`)</td><td>à l'**heure d'échéance par défaut** de Tasks.org (Paramètres → Échéances / Notifications → « heure par défaut », à régler — souvent 18 h d'origine)</td></tr><tr><td>Échéance **date + heure**</td><td>à cette heure, si le rappel « à l'échéance » est actif dans les réglages de la tâche (il l'est par défaut pour les tâches créées dans Tasks.org ; pour une tâche créée par le MCP, voir §7.3)</td></tr><tr><td>**Rappels supplémentaires** (dans la tâche → Rappels)</td><td>« au début », « à l'échéance », « aléatoire », « personnalisé » (x heures/jours avant ou après), et **« quand dépassée »** qui relance périodiquement tant que la tâche n'est pas cochée</td></tr><tr><td>Tâche **cochée**</td><td>plus aucune notification, elle passe dans les terminées (masquées ou non selon Paramètres → Apparence → « afficher les terminées »)</td></tr></tbody></table>

Une notification peut être **reportée** (snooze) depuis la notification elle-même. Les rappels sont écrits en `VALARM` dans le `VTODO` et donc synchronisés vers Baïkal ; InfCloud les affiche, le MCP les lit dans `get_task` (iCalendar brut) mais ne les crée pas encore.

### 5.3 Ce que porte une tâche, et sa correspondance CalDAV

<table id="bkmrk-dans-tasks.org-dans-"><thead><tr><th>Dans Tasks.org</th><th>Dans le `VTODO`</th><th>Vu par le MCP</th></tr></thead><tbody><tr><td>Titre</td><td>`SUMMARY`</td><td>`summary`</td></tr><tr><td>Échéance (date, ou date + heure)</td><td>`DUE`</td><td>`due`</td></tr><tr><td>Date de début (« masquer jusqu'à »)</td><td>`DTSTART`</td><td>`start`</td></tr><tr><td>Priorité (aucune / basse / moyenne / haute)</td><td>`PRIORITY` 0 / 9 / 5 / 1</td><td>`priority`</td></tr><tr><td>Notes</td><td>`DESCRIPTION`</td><td>`description`</td></tr><tr><td>Étiquettes (tags)</td><td>`CATEGORIES`</td><td>`categories`</td></tr><tr><td>Répétition</td><td>`RRULE`</td><td>`rrule`</td></tr><tr><td>Sous-tâches</td><td>`RELATED-TO` (tâche parente)</td><td>non exposé — à ajouter si l'usage vient</td></tr><tr><td>Rappels</td><td>`VALARM`</td><td>iCalendar brut seulement</td></tr><tr><td>Coché</td><td>`STATUS:COMPLETED` + `COMPLETED` + `PERCENT-COMPLETE:100`</td><td>`status`, `completed`, `percent_complete`</td></tr><tr><td>Pièces jointes, lieux, minuteur</td><td>**locaux**, jamais synchronisés</td><td>—</td></tr></tbody></table>

**Piège constaté :** une tâche saisie dans Tasks.org **sans toucher à la priorité** arrive avec `PRIORITY:9` (basse), pas 0. Ne pas lire « basse » comme un choix de Julien.

### 5.4 Trois gestes utiles

- **Changer une tâche de liste** (perso ↔ pro) : ouvrir la tâche → champ Liste. Tasks.org la déplace côté serveur (suppression dans une collection, création dans l'autre : l'UID change).
- **Saisie rapide** : le bouton + saisit dans la **liste ou le filtre courant** — depuis « Toutes les tâches », la tâche va dans la liste par défaut (Paramètres → Liste par défaut : à fixer sur `Default calendar` pour que le perso soit le réflexe et que NEXTE soit un choix).
- **Forcer la synchro** : balayage vers le bas dans une liste, ou DAVx5 → Synchroniser maintenant. Sinon, périodicité DAVx5 (120 s dans InfCloud, celle de DAVx5 pour le téléphone).

## 6. Vérifications effectuées le 15/09/2026

### 6.1 MCP → Baïkal (script seul)

<table id="bkmrk-test-r%C3%A9sultat-propfi-1"><thead><tr><th>Test</th><th>Résultat</th></tr></thead><tbody><tr><td>PROPFIND `supported-calendar-component-set` sur `default/` et `nexte/`</td><td>`VEVENT`, `VTODO` sur les deux</td></tr><tr><td>`create_task` échéance `18/09/2026`, priorité 1, catégorie</td><td>créée dans NEXTE, `DUE;VALUE=DATE`</td></tr><tr><td>`create_task` échéance `2026-09-20 17h00`, priorité 5</td><td>créée, `DUE` horodaté `+02:00`</td></tr><tr><td>`list_tasks(calendar="NEXTE")`</td><td>les deux, triées par échéance</td></tr><tr><td>`list_tasks(due_before="19/09/2026")`</td><td>une seule (la première)</td></tr><tr><td>`update_task` IN-PROCESS, 50 %, échéance décalée au 19/09</td><td>relu conforme</td></tr><tr><td>`complete_task`</td><td>COMPLETED, 100 %, horodatage `COMPLETED` posé</td></tr><tr><td>`list_tasks` sans / avec `include_completed`</td><td>0 / 1</td></tr><tr><td>`reopen_task`</td><td>NEEDS-ACTION, 0 %, `COMPLETED` retiré</td></tr><tr><td>`update_event` (régression §7.1)</td><td>description posée sur un créneau Tourrettes, date inchangée</td></tr><tr><td>`delete_task` ×2</td><td>NEXTE vide</td></tr><tr><td>`claude mcp list` après modification</td><td>`caldav … ✔ Connected`</td></tr></tbody></table>

### 6.2 Bout en bout, avec le téléphone

<table id="bkmrk-%C3%89tape-r%C3%A9sultat-creat"><thead><tr><th>Étape</th><th>Résultat</th></tr></thead><tbody><tr><td>`create_task` « TEST MCP → Tasks.org », échéance 16/09, priorité 5, dans NEXTE</td><td>créée</td></tr><tr><td>Synchro DAVx5 → Tasks.org, liste NEXTE</td><td>**visible**, avec échéance et priorité</td></tr><tr><td>Cochée dans Tasks.org, synchro</td><td>`list_tasks(include_completed=True)` : **COMPLETED 100 %**, `completed` 15/09 17:02</td></tr><tr><td>Tâche « reprendre des fruits » saisie dans Tasks.org (liste NEXTE)</td><td>lue par le MCP : NEEDS-ACTION, sans échéance, `PRIORITY:9`</td></tr><tr><td>InfCloud → onglet Tâches</td><td>les deux visibles</td></tr></tbody></table>

Les deux tâches sont **laissées en place** à la demande de Julien, pour observer le comportement des rappels (§5.2).

## 7. Pièges rencontrés le 15/09/2026

### 7.1 `icalendar.walk()` renvoie une liste

Avec icalendar 6, `Calendar.walk("VTODO")` renvoie une **liste**, pas un itérateur : `next(ic.walk(...))` échoue en `TypeError`. Le même code dormait dans `update_event` depuis la création du script — jamais exercé jusque-là. Corrigé aux deux endroits (`ic.walk(...)[0]`).

### 7.2 `Todo.complete()` ne pose pas le pourcentage

La bibliothèque `caldav` pose `STATUS:COMPLETED` et `COMPLETED:<horodatage>` mais laisse `PERCENT-COMPLETE` à sa valeur précédente (50 % dans le test). Certains clients affichent alors une tâche « terminée à 50 % ». `complete_task` force 100 %.

### 7.3 Une tâche créée par le MCP n'a pas de rappel

Le MCP écrit `DUE` mais aucun `VALARM`. Dans Tasks.org, la tâche a une échéance mais son rappel « à l'échéance » dépend du réglage par défaut de l'appli pour les tâches importées (Paramètres → Notifications). Si Julien veut être notifié des tâches créées depuis Claude, deux options : régler Tasks.org pour appliquer les rappels par défaut aux nouvelles tâches synchronisées, ou ajouter un `VALARM` côté MCP (paramètre `reminder` sur `create_task`). À trancher à l'usage.

### 7.4 Échéance : date seule vs date-heure

Une échéance `18/09/2026` doit produire `DUE;VALUE=DATE:20260918` (journée entière), et `2026-09-20 17h00` un `DUE` horodaté en Europe/Paris. Les deux formes sont gérées ; `due_before` compare une date seule à la fin de journée, pour ne pas exclure une tâche échue le jour même.

### 7.5 Priorité 9 par défaut côté Tasks.org

Voir §5.3 : « basse » n'est pas un choix, c'est le défaut de l'appli.

## 8. Règles d'usage pour Claude

- Une tâche se crée dans le calendrier par défaut du PC (NEXTE ici) ; les tâches perso ne sont lues que pour la vue d'ensemble, jamais modifiées sans demande explicite.
- `delete_task` uniquement après confirmation explicite de Julien. Terminer ≠ supprimer : une tâche faite se **termine** (`complete_task`), on ne l'efface pas — l'historique reste consultable avec `include_completed`.
- Priorités : n'en poser une que si Julien la donne ; par défaut 0. Ne pas interpréter un 9 venant de Tasks.org.
- Une tâche sans échéance ne notifiera jamais : si Julien veut « être rappelé », demander une date.
- Ne jamais annoncer qu'une tâche est créée sans relire `due` dans le résultat (même règle que pour les dates d'événements, page 2 §5.8).

# 4 - intégration des contacts personnels et professionnels de Julien

<div id="bkmrk--7" style="clear: left;">  
</div># Intégration des contacts personnels et professionnels de Julien

**Statut au 15 septembre 2026 : en discussion. Rien n'est construit.** Cette page contient le plan en trois actes posé par Julien, puis les retours de Claude. Les décisions prises seront consignées en §5 au fur et à mesure.

---

## 1. Le plan de Julien (15/09/2026)

Sources actuelles, dispersées :

- contacts **hérités** dans Synology Contacts (NAS SasNexte) ;
- contacts **pro** chez Infomaniak (compte `julien.bertrand@ik.me`) ;
- contacts **téléphone** qui se baladent dans le Xiaomi (compte local / Mi Cloud) ;
- contacts **Gmail** perso.

Trois actes :

1. **Une base unificatrice simple (SQLite)** pour tout rapatrier, trier, sérier, ancrer, archiver — lisible depuis Thunderbird sur le téléphone, depuis Synology Contacts, etc.
2. **Baïkal comme catalyseur** de cette base : il la distribue, mais reçoit aussi les mises à jour depuis les foyers de création que sont Gmail, Infomaniak et le Xiaomi.
3. **InfCloud comme répertoire web** (Roundcube : à quoi sert-il encore ?), pour rapprocher adresses et rendez-vous.

Procédure hébergée sur le VPS perso juxjux.ovh, comme le reste de la stack (page 1).

## 2. Retours de Claude

### 2.1 L'acte 1 existe déjà : Baïkal *est* la base SQLite

Baïkal stocke ses contacts dans **une base SQLite** (`/var/www/baikal/Specific/db/db.sqlite`, table `cards`, une vCard par ligne — page 1 §6.5 et §12). Construire une seconde SQLite « unificatrice » à côté, c'est créer **deux sources de vérité** pour le même objet, avec un synchroniseur maison entre les deux à écrire et à maintenir. C'est précisément le problème que Baïkal a été installé pour résoudre (page 1 §1 : « Baïkal est la source de vérité unique »).

Ce que le plan attend de la base « versatile dans sa lecture » — être vue par Thunderbird, par le téléphone, par un répertoire web — c'est **exactement ce qu'un serveur CardDAV fournit**. Une SQLite nue ne le fournit pas : il faudrait un connecteur par client. Baïkal est la SQLite *plus* la couche de lecture universelle.

**Proposition : fusionner les actes 1 et 2.** Baïkal est la base ET le catalyseur. Ce qui manque n'est pas une base, ce sont trois choses :

1. un **outil d'ingestion** des sources (vivantes et mortes) ;
2. un **espace de tri** dans Baïkal, avant que les contacts n'entrent dans le carnet de référence ;
3. un **outil de triage** : détecter les doublons, fusionner, classer, archiver.

Le tri, le sériage, l'ancrage et l'archivage se font **dans le vocabulaire vCard**, pas dans un schéma SQL à inventer : `CATEGORIES` pour les étiquettes (mairie, bureau d'études, famille, …), `NOTE` ou un champ `X-PROVENANCE` pour l'origine, `ORG`/`TITLE` pour l'ancrage professionnel, et **plusieurs carnets** pour les grands compartiments. Tous les clients relisent ces champs.

### 2.2 Les sources ne sont pas de même nature

Le plan les traite comme quatre « foyers de création » équivalents. Elles ne le sont pas :

<table id="bkmrk-source-nature-traite"><thead><tr><th>Source</th><th>Nature</th><th>Traitement proposé</th></tr></thead><tbody><tr><td>**Synology Contacts** (hérités)</td><td>source **morte** : un stock à vider, pas un flux</td><td>export `.vcf` **une fois**, import dans un carnet de transit, puis triage. Synology Contacts n'est ensuite plus alimenté</td></tr><tr><td>**Infomaniak** (pro)</td><td>source **vivante**, CardDAV natif</td><td>vdirsyncer, prévu depuis août (page 1 §10.1), **en sens unique vers Baïkal** pour commencer</td></tr><tr><td>**Gmail** (perso)</td><td>source **vivante**, CardDAV Google (mot de passe d'application, compte en 2FA)</td><td>idem, vdirsyncer sens unique vers Baïkal</td></tr><tr><td>**Xiaomi**</td><td>**pas une source distante** : ce sont des contacts dans le compte « Téléphone » ou « Mi Cloud » de l'appareil, invisibles de tout serveur</td><td>export `.vcf` **une fois** depuis l'appli Contacts, import en transit, triage ; puis **réglage du compte par défaut** pour les nouveaux contacts = compte Baïkal de DAVx5, et **désactivation de la synchro contacts Mi Cloud / Google** pour tarir la fuite</td></tr></tbody></table>

Point important : une fois le compte DAVx5 défini comme compte par défaut sur le téléphone, **le téléphone n'est plus un foyer de création à synchroniser — il écrit directement dans Baïkal.** Même chose pour InfCloud, Roundcube et Claude (via le futur MCP CardDAV). Restent deux vrais flux entrants : Infomaniak et Gmail.

### 2.3 Sur le « reçoit les mises à jour » de l'acte 2 : le sens du flux est la décision structurante

Deux modèles possibles, à trancher **avant** de configurer vdirsyncer :

**Modèle A — Baïkal devient l'unique foyer de création.** Gmail et Infomaniak sont vidés dans Baïkal une fois (sens unique), puis on cesse d'y créer des contacts : nouveaux contacts saisis sur le téléphone (compte DAVx5), dans InfCloud, ou par Claude. Gmail et Infomaniak deviennent des vestiges, ou reçoivent une copie **descendante** (Baïkal → Google) si l'autocomplétion Gmail compte.

- Avantages : aucune synchro bidirectionnelle, donc aucun conflit et aucune propagation de suppression accidentelle ; vdirsyncer peut même être arrêté après la migration.
- Coût : une discipline — ne plus créer de contact dans Gmail ni dans le webmail Infomaniak.

**Modèle B — synchronisation bidirectionnelle permanente** avec Gmail et Infomaniak.

- Avantage : on continue de créer des contacts partout.
- Risques : un contact supprimé d'un côté est supprimé partout ; un doublon fusionné dans Baïkal peut être recréé par la source ; les champs que Google ne connaît pas sont perdus au retour. vdirsyncer sait faire, mais c'est le mode où l'on découvre les problèmes après coup.

**Recommandation : modèle A**, avec vdirsyncer en sens unique le temps de la migration, puis à l'arrêt. C'est aussi la recommandation déjà posée en page 1 §10.1. Si l'autocomplétion Gmail manque, ajouter plus tard un flux **descendant** Baïkal → Google (sens unique aussi) — pas de remontée.

### 2.4 Espace de tri : des carnets de transit dans Baïkal

Plutôt que de trier dans une base à part, créer dans Baïkal, sous le même utilisateur `Julien` :

<table id="bkmrk-carnet-r%C3%B4le-synchron"><thead><tr><th>Carnet</th><th>Rôle</th><th>Synchronisé vers le téléphone ?</th></tr></thead><tbody><tr><td>`default`</td><td>contacts **personnels** de référence</td><td>oui</td></tr><tr><td>`nexte`</td><td>contacts **professionnels** de référence (mairies, DDTM, bureaux d'études, …)</td><td>oui</td></tr><tr><td>`archive`</td><td>contacts conservés mais sortis de l'usage (anciens clients, personnes décédées, …)</td><td>**non** — visible dans InfCloud seulement</td></tr><tr><td>`transit-synology`, `transit-gmail`, `transit-infomaniak`, `transit-xiaomi`</td><td>chaque source importée telle quelle, sans modification</td><td>non</td></tr></tbody></table>

Les carnets de transit sont **éphémères** : une fois vidés par le triage, on les supprime. Tout ce qui est décidé s'exprime par un déplacement d'un carnet à un autre — ce qui est tracé par Baïkal (horodatage, `synctoken`) et sauvegardé chaque nuit (page 1 §8).

Deux carnets de référence (`default` / `nexte`) plutôt qu'un seul : même logique que pour les calendriers (page 2 §1) — le PC NEXTE n'écrit que dans `nexte`, les contacts personnels ne transitent par une conversation Alteris que si Julien le demande, et DAVx5 permet de n'exposer que l'un des deux à une appli donnée. Une mairie qui est « les deux » va dans `nexte` avec une `CATEGORIES` explicite ; on ne duplique pas.

### 2.5 L'outil de triage : c'est là que Claude a une valeur

Le triage manuel de quatre sources est ce qui a fait échouer toutes les tentatives de rangement de contacts depuis vingt ans. Ce que le MCP CardDAV (à écrire, comme pour les tâches — page 3) permettrait :

- **inventaire** : combien de fiches par carnet de transit, combien avec e-mail, téléphone, organisation, combien vides ;
- **rapport de doublons** : même e-mail, même téléphone normalisé (`+33 6 …`), même nom à casse/accent près, entre carnets ;
- **fusion assistée** : Claude propose la fiche fusionnée (union des e-mails et téléphones, `ORG` le plus complet, `NOTE` avec provenance), Julien valide, la fiche entre dans `default` ou `nexte` et les fiches sources sont retirées du transit ;
- **classement par lots** : « tous les contacts dont l'e-mail est en `.gouv.fr` ou `mairie-` → `nexte`, catégorie *collectivité* » ;
- **archivage** : déplacement vers `archive` avec date et motif en `NOTE`.

Rien de tout cela ne demande une base à part : ce sont des opérations CardDAV (REPORT `addressbook-query`, PUT, DELETE, MOVE) sur des vCards 3.0 — le format que Roundcube écrit déjà et que tout le monde relit (page 1 §9).

### 2.6 Acte 3 — InfCloud, Roundcube, Thunderbird, Synology

- **InfCloud** : oui comme répertoire web. Sa moitié CardDavMATE lit et édite les carnets ; il **autocomplète les participants** d'un rendez-vous depuis les carnets chargés — c'est le « matcher les adresses avec les rendez-vous ». Rappel : un nouveau carnet doit être inscrit dans ses réglages serveur (`loadedaddressbookcollections`), même piège qu'en page 2 §5.7 ; on a d'ailleurs vu dans ces réglages une entrée `activeaddressbookcollections: ["https://undefined"]` à corriger au passage.
- **Roundcube** : il n'a plus de rôle propre pour les contacts dès qu'InfCloud les couvre. Il ne se justifie que comme **webmail** du compte `ik.me`. Si Julien ne l'utilise pas comme webmail, il peut être arrêté : un container, un vhost, ~32 Mo et une surface d'attaque de moins (page 1 §6.4 pour ses contraintes IMAP). À décider — pas dans l'urgence.
- **Thunderbird sur le téléphone** : il ne gère pas de carnet ; il lit les **contacts Android**, qui viennent de DAVx5. Rien à faire de plus. **Thunderbird sur PC** (s'il y en a un) parle CardDAV nativement : URL du carnet + identifiants, et il voit `default`/`nexte`.
- **Synology Contacts** : à ma connaissance, l'application Synology **expose** du CardDAV (elle est serveur) et importe des comptes Google/Microsoft, mais **ne consomme pas** un CardDAV tiers. À vérifier sur le DSM. Si c'est confirmé, le NAS n'est pas un client à alimenter mais une source à vider (§2.2), et il n'a plus de rôle après la migration — sauf comme archive figée de l'existant, ce qui est très bien.

### 2.7 Ordre de chantier proposé

Chaque étape est réversible et laisse la stack en état de marche. Rien n'est lancé sans validation de Julien.

<table id="bkmrk-%23-%C3%89tape-o%C3%B9-outil-0-i"><thead><tr><th>\#</th><th>Étape</th><th>Où</th><th>Outil</th></tr></thead><tbody><tr><td>0</td><td>**Inventaire** : nombre de contacts par source, présence d'e-mail/téléphone, date du dernier ajout</td><td>chaque source</td><td>exports `.vcf`, comptage</td></tr><tr><td>1</td><td>**Gel des sources** : arrêt de la création de contacts dans Gmail et le webmail Infomaniak ; compte DAVx5 par défaut sur le Xiaomi ; synchro contacts Mi Cloud/Google coupée</td><td>téléphone, discipline</td><td>réglages Android</td></tr><tr><td>2</td><td>**Carnets** : création de `nexte`, `archive` et des carnets de transit ; inscription dans InfCloud</td><td>Baïkal</td><td>admin Baïkal ou MCP</td></tr><tr><td>3</td><td>**Imports morts** : Synology et Xiaomi → carnets de transit</td><td>VPS</td><td>`.vcf` + PUT CardDAV</td></tr><tr><td>4</td><td>**Imports vivants** : vdirsyncer sens unique Gmail → transit, Infomaniak → transit ; cron le temps de la migration</td><td>VPS</td><td>vdirsyncer (page 1 §10.1)</td></tr><tr><td>5</td><td>**Triage** : inventaire, doublons, fusions validées, classement `default`/`nexte`/`archive`</td><td>MCP CardDAV</td><td>Claude + Julien</td></tr><tr><td>6</td><td>**Vérification clients** : téléphone (DAVx5), InfCloud, Thunderbird PC</td><td>clients</td><td>—</td></tr><tr><td>7</td><td>**Fin de migration** : suppression des carnets de transit, arrêt de vdirsyncer (modèle A), sort de Roundcube</td><td>VPS</td><td>—</td></tr></tbody></table>

Le **MCP CardDAV** (outils `*_contact`) est un préalable à l'étape 5 et sert dès l'étape 0 pour compter. Il se construit comme celui des tâches, dans le même script.

## 3. Ce que Claude ne recommande pas

- Une **SQLite maison** à côté de Baïkal (§2.1).
- Un **conteneur « tout-en-un »** (Nextcloud, SOGo) : déjà écarté en page 1 §13 pour de bonnes raisons de RAM et de surface ; la stack actuelle est plus riche en pièces mais chaque pièce est petite et remplaçable.
- La **bidirectionnalité d'emblée** avec Gmail et Infomaniak (§2.3).
- Le **triage à la main** dans une interface web, fiche par fiche.

## 4. Questions à trancher

1. Modèle A (Baïkal seul foyer de création) ou modèle B (bidirectionnel) ? — §2.3
2. Deux carnets de référence `default`/`nexte`, ou un seul avec catégories ? — §2.4
3. Le pro, c'est NEXTE ou Alteris, ou les deux avec une catégorie ? (même question que pour le calendrier, tranchée « NEXTE » le 15/09)
4. Roundcube : webmail utilisé, ou à arrêter ? — §2.6
5. Synology Contacts : confirmer qu'il ne peut pas être client CardDAV ; sinon, souhaite-t-on qu'il reste alimenté ?
6. L'autocomplétion Gmail manque-t-elle si Gmail cesse d'être alimenté ? (déclenche ou non un flux descendant Baïkal → Google)
7. Ordre de grandeur : combien de contacts au total, toutes sources confondues ? (change la méthode de triage : 300 se font en une séance, 3 000 par lots)

## \# Note de déploiement — miroir contacts Baïkal → Infomaniak (VPS juxjux)

Rédigée le 22/09/2026 par l'instance Claude du \*\*PC NEXTE\*\*, à l'attention de l'instance Claude du \*\*PC perso\*\* qui a l'accès SSH au VPS juxjux. Documentation du chantier : BookStack Alteris page 499 (chapitre 486, livre 280).

\---

#### \## 1. Ce qui est déjà fait (sur le PC NEXTE, en production)

Le miroir tourne et a été validé de bout en bout. Il est \*\*lancé à la main\*\* depuis le PC NEXTE ; il faut maintenant le \*\*planifier sur le VPS\*\* pour qu'il vive sans ce PC.

État au 22/09 au soir :

| Paire | Identifiant court | Carnet Infomaniak | Baïkal | Infomaniak |  
|---|---|---|---|---|  
| \*\*NEXTE\*\* → `julien.bertrand@nexte.fr` | `JB08607` | `Baikal-NEXTE` | 1 584 fiches | 1 584, identiques |  
| \*\*Jux\*\* → `julien.bertrand@ik.me` | `JB06766` | `Baikal-Jux` | 305 fiches | 305, identiques |

Les deux paires sont déclarées dans `PAIRES` en tête du script et tournent en production ; un passage à blanc ne trouve plus rien à faire des deux côtés.

Chaîne vérifiée : correction d'une fiche dans InfCloud → descend dans Infomaniak et sur le téléphone (DAVx5/Fossify). Création \*\*et modification\*\* dans le webmail Infomaniak → remontent dans Baïkal (§2).

#### \## 2. Le script

`260922-miroir\_infomaniak.py` (ce dossier Syncthing, `Jux-scripts/Baikal-Contacts/`). Autonome PEP 723, dépendance `vobject`, lancé par `uv run --script`.

\*\*Principe\*\* — Baïkal est le maître :

\- \*\*descente\*\* : toute fiche Baïkal absente ou sémantiquement différente côté Infomaniak est écrite (PUT, même UID) ; toute fiche que le miroir avait déposée et qui a disparu de Baïkal est supprimée côté Infomaniak ;  
\- \*\*remontée des créations\*\* : une fiche présente côté Infomaniak dont l'UID n'a \*\*jamais\*\* été déposé par le miroir (donc créée dans le webmail) est recopiée dans Baïkal, avec une note de provenance ;  
\- \*\*remontée des modifications\*\* \*(ajoutée le 22/09, avant déploiement)\* : une fiche existante modifiée dans le webmail remonte dans Baïkal. L'arbitrage repose sur les \*\*ETag mémorisés des deux côtés\*\* : si seul Infomaniak a bougé, la fiche remonte ; si seul Baïkal a bougé, elle redescend ; si les deux ont bougé, le \*\*`REV` le plus récent gagne\*\* et le conflit est compté dans la sortie (`conflits\_arbitres`) ;  
\- \*\*aucune suppression ne remonte jamais\*\* — une suppression dans le webmail est annulée au passage suivant. C'est voulu : protection contre les suppressions intempestives et contre une compromission de la boîte.

Deux propriétés vérifiées avant de coder l'arbitrage, à re-vérifier si Infomaniak change de moteur : leurs \*\*ETag sont stables\*\* entre deux lectures sans modification (1 585/1 585 testées) et le \*\*`REV` de Baïkal est conservé à l'identique\*\* au stockage.

Le fichier d'état `miroir\_&lt;paire&gt;.json` (\*\*version 2\*\* : `{uid: {"ik": etag, "bk": etag}}`) est \*\*ce qui distingue\*\* « supprimée dans Baïkal » de « créée dans le webmail », et quel côté a bougé. \*\*Ne pas le perdre\*\* : sans lui, le passage suivant retombe sur le comportement « Baïkal gagne » et les modifications faites dans le webmail seraient écrasées. Il migre automatiquement depuis le format v1 (simple liste d'UID).

Comparaison \*\*sémantique\*\* (nom, N, e-mails, téléphones, organisation, fonction, catégories, note, adresses) et non octet à octet : Infomaniak réécrit les vCards au stockage, une comparaison stricte réécrirait tout à chaque passage.

#### \## 3. Ce qu'il faut faire sur le VPS

##### \### 3.1 Fichiers à déposer

| Fichier | Où | Contenu |  
|---|---|---|  
| `260922-miroir\_infomaniak.py` | `/home/debian/baikal-miroir/` | le script (copie depuis Syncthing) |  
| `260915-mcp\_caldav\_server.py` | `/home/debian/baikal-miroir/` | \*\*requis\*\* : le miroir l'importe comme bibliothèque (accès Baïkal) |  
| `infomaniak.env` | `/home/debian/baikal-miroir/` | `NEXTE\_FR\_PASSWORD=…` et `IK\_ME\_PASSWORD=…` — mots de passe d'application Infomaniak (16 caractères chacun). `chmod 600`. \*\*À consigner dans le Vault 2603\*\* : le fichier a disparu une fois du PC NEXTE sans explication, le 22/09 |  
| `~/.claude.json` \*\*ou\*\* variables d'environnement | — | le script serveur lit `CALDAV\_URL`, `CALDAV\_USER`, `CALDAV\_PASSWORD` dans `~/.claude.json` → \*\*à adapter\*\* (voir §3.2) |

##### \### 3.2 Adaptation à faire dans `load\_server()`

Sur le PC NEXTE, les identifiants Baïkal viennent de `~/.claude.json` (config du MCP Claude Code). \*\*Sur le VPS il n'y a pas de Claude Code\*\* : remplacer, dans `260922-miroir\_infomaniak.py` (et dans les autres scripts si tu les déploies), la fonction :

```python  
def load\_server():  
 cfg = json.load(open(os.path.expanduser("~/.claude.json"), encoding="utf-8"))  
 os.environ.update(cfg\["mcpServers"\]\["caldav"\]\["env"\])  
 ...  
```

par une lecture directe d'un fichier `baikal.env` (même dossier, `chmod 600`) :

```  
CALDAV\_URL=https://baikal.juxjux.ovh/dav.php/  
CALDAV\_USER=Julien  
CALDAV\_PASSWORD=&lt;mot de passe DAV de l'utilisateur Julien — Vault 2603&gt;  
CALDAV\_DEFAULT\_CALENDAR=NEXTE  
```

Remarque : le VPS juxjux héberge Baïkal lui-même ; le miroir peut donc taper sur `http://127.0.0.1:8088/dav.php/` plutôt que de ressortir par nginx. À toi de voir — l'URL publique fonctionne et évite de dépendre du port interne (page 487 §4).

##### \### 3.3 Cron

```  
\*/15 \* \* \* \* cd /home/debian/baikal-miroir &amp;&amp; /usr/local/bin/uv run --script 260922-miroir\_infomaniak.py --env infomaniak.env --etat /home/debian/baikal-miroir/etat --real &gt;&gt; /var/log/baikal-miroir.log 2&gt;&amp;1  
```

\- `uv` à installer si absent (`curl -LsSf https://astral.sh/uv/install.sh | sh`) ; sinon un venv avec `vobject`, `caldav`, `icalendar`, `python-dateutil`, `mcp&lt;2`.  
\- \*\*Verrou\*\* : ajouter `flock` pour qu'un passage lent ne chevauche pas le suivant :  
 `flock -n /tmp/baikal-miroir.lock -c '…'`  
\- \*\*Alerte\*\* : en cas de sortie contenant `"echecs": \[1-9\]` ou d'un code de retour non nul, envoyer un e-mail comme le fait `vps\_backup.sh` (même mécanisme, page 487 §8).

Durée observée d'un passage sans changement : quelques secondes. Un passage qui réécrit 1 500 fiches : ~5 minutes.

##### \### 3.4 Export `.vcf` quotidien

Décision de Julien : \*\*un seul fichier, toujours à jour, réécrit à chaque passage\*\* — pas de rotation, le versionnage est assuré par la sauvegarde nocturne de Baïkal vers kDrive (page 487 §8). Destination : le dossier Syncthing `Jux\_univers/jux\_contacts/` (si le VPS est dans le maillage ; sinon `/home/debian/baikal-miroir/export/`).

Poids mesuré le 22/09 : \*\*1,92 Mo\*\* pour les deux carnets réunis (1 890 fiches), dont 62 % de photos (116 photos). À écrire une fois par nuit, pas à chaque passage du miroir.

Le code d'export existe déjà, dupliqué dans plusieurs scripts (`260921-restaurer\_carnets.py`, `260921-fusion\_lots.py`) :

```python  
with open(dest, "w", encoding="utf-8", newline="") as f:  
 for c in m.\_fetch\_contacts(m.\_find\_addressbook(book)):  
 f.write(c\["\_raw"\].rstrip("\\r\\n") + "\\r\\n")  
```

#### \## 4. Pièges rencontrés — à ne pas redécouvrir

1\. \*\*Identifiant CardDAV Infomaniak\*\* : ce n'est \*\*pas\*\* l'adresse e-mail mais un \*\*identifiant court\*\* de synchronisation (`JB08607` pour `julien.bertrand@nexte.fr`), obtenu sur `https://config.infomaniak.com` → \*Contacts &amp; calendriers\* → appareil \*\*GNU/Linux\*\*. Avec l'adresse e-mail, l'authentification passe (207 à la racine) mais toute requête répond `Principal with name … not found` — cherche-erreur garanti.  
2\. \*\*Mots de passe d'application Infomaniak\*\* : ils n'ont \*\*pas\*\* de périmètre par service (contrairement à ce que j'avais supposé) ; un seul mot de passe vaut pour IMAP, CalDAV, CardDAV. Ne pas confondre avec les mots de passe \*\*de boîte mail\*\* générés par `config.infomaniak.com` pour un appareil : ceux-là ne donnent pas accès au CardDAV.  
3\. \*\*MKCOL interdit\*\* : Infomaniak refuse la création d'un carnet par CardDAV (403 Forbidden). Le carnet cible doit être créé \*\*à la main dans le webmail\*\*, avec le nom exact attendu par `PAIRES` (`Baikal-NEXTE`, `Baikal-Jux`).  
4\. \*\*Lignes pliées\*\* : Infomaniak lit mal les vCards pliées à 75 caractères (RFC 6350 §3.2) — la catégorie `Important` devenait `Imp`. Le script \*\*déplie\*\* les vCards avant le PUT (`data.replace("\\r\\n ", "")`). Ne pas retirer cette ligne.  
5\. \*\*Suivre `carnet\_ik\_cree`\*\* dans la sortie JSON : `true` signifie que le carnet n'a pas été trouvé par son nom — souvent un carnet renommé ou une faute de frappe, pas une vraie création (elle est interdite, cf. 3).

#### \## 5. Reste à faire

1\. ~~Paire `Jux` → `ik.me`~~ — \*\*faite le 22/09\*\* : identifiant court `JB06766`, carnet `Baikal-Jux`, 305 fiches déposées, 0 échec.

 \*Au passage, un piège à connaître\* : le compte `ik.me` ne contenait qu'un seul carnet, « Julien BERTRAND », avec \*\*1 664 fiches jamais triées\*\* (ce compte ne faisait pas partie des cinq exports du 17/09). Julien les a jugées « doublon de doublon de doublon » et fait supprimer — sauvegarde préalable dans `jux\_contacts/260922-1643-sauvegarde-ikme-JulienBERTRAND.vcf` (736 Ko). \*\*Il ne fallait surtout pas renommer ce carnet en `Baikal-Jux` avant de le vider\*\* : le miroir aurait pris ses 1 664 fiches pour des créations faites dans le webmail et les aurait remontées dans le carnet Jux de Baïkal, puis sur le téléphone. Vider d'abord, renommer ensuite.  
2\. ~~Remontée des modifications~~ — \*\*faite le 22/09\*\*, avant déploiement (voir §2). Testée deux fois : mise à jour substantielle d'une fiche dans le webmail (nom, organisation, fonction, note de 483 caractères) et \*\*ajout d'une photo de 70 Ko\*\* — remontées intactes dans Baïkal, base64 et attribut de recadrage compris.  
3\. \*\*Surveiller le poids du carnet\*\* (question posée par Julien le 22/09, laissée ouverte) : le miroir relit \*\*l'intégralité\*\* des deux carnets à chaque passage. À 2 Mo aujourd'hui c'est indolore ; mais les photos ajoutées par le webmail pèsent ~70 Ko chacune (contre 7 Ko pour les vignettes héritées), et le base64 ajoute 33 %. À 500 photos, le carnet ferait ~46 Mo, soit ~4 Go de trafic par jour à raison d'un passage tous les quarts d'heure. Deux parades, à préparer avant d'en arriver là : \*\*lecture incrémentale\*\* (`sync-collection` + `sync-token`, supportés par Baïkal et Infomaniak — vérifié) et/ou \*\*redimensionnement des photos à l'entrée\*\* (400 px, ~25 Ko). Julien a choisi de laisser tel quel pour l'instant et d'en discuter avec toi.  
4\. \*\*Ne pas oublier\*\* : le troisième compte `nexte@ik.me` (trash) \*\*ne doit jamais être branché\*\* — c'est une consigne explicite de Julien.

#### \## 6. Contrat entre nos deux instances

Le script maître reste celui du \*\*pivot Syncthing\*\* `Jux-scripts/Baikal-Contacts/`. Si tu le modifies pour le VPS (notamment `load\_server()`), garde la version VPS distincte et datée (`2609xx-miroir\_infomaniak\_vps.py`) plutôt que d'écraser le fichier commun — le PC NEXTE continue de s'en servir pour les opérations manuelles sur les carnets.

Renvoie-moi une note dans ce même dossier quand le cron tourne, avec : chemin d'installation, contenu exact de la crontab, emplacement du journal, et le premier passage observé (JSON de sortie). Je la consignerai en page 499.