# 📸 PhotoSync — Synchronisation photo/vidéo (Android & iOS) → NAS

> **Objectif premier (v1, foyer) :** synchroniser **automatiquement** les photos et
> vidéos de nos téléphones (**mon Android** + l'**iPhone de ma femme**) vers mon
> **NAS QNAP TS-228**, via mon **Raspberry Pi**, **sans cloud** et **sans rien
> exposer sur Internet**.
>
> **Vision future (si ouverture) :** en faire une vraie **application mobile de
> partage** de photos/vidéos (pleine qualité, sans pub, albums collaboratifs).

> 🔒 **Projet propriétaire** — code source privé, non open source.

---

## 🌱 Genèse : pourquoi ce projet

Mon **QNAP TS-228** n'est plus synchronisable depuis une mise à jour du firmware
QNAP : impossible d'y envoyer automatiquement les nouvelles photos prises sur mon
**Android**. Le NAS fonctionne pourtant très bien et parle toujours **SMB2,
WebDAV, FTP**.

Ce projet construit l'outil qui me manque : **mon téléphone → mon NAS, tout seul**,
en passant par mon **Raspberry Pi** comme relais sécurisé. Si ça résout *mon*
problème, ça résout celui de milliers de gens avec un vieux NAS abandonné par le
support constructeur — d'où la **vision future** d'en faire une app grand public.

---

## 🎯 Périmètre

### ✅ v1 — MVP perso / foyer (ce qu'on construit **maintenant**)
- Sauvegarde **automatique** Android → NAS, en **pleine qualité** (zéro compression).
- Fonctionne **à la maison** (Wi-Fi local) **et à distance** (tunnel chiffré).
- **Aucun cloud** : le Raspberry Pi est le backend. Coût d'infra ≈ 0.
- **Aucune exposition** du NAS sur Internet.
- **Multi-utilisateur de foyer** avec rôles : **moi (admin, accès partout)** et
  **ma femme (restreinte à ses dossiers)** — droits **gérés par le NAS**, en
  réutilisant les **comptes QNAP déjà existants**.

### 🔮 Plus tard — ouverture (hors v1)
- App mobile de partage (feed, profils, social).
- **Albums collaboratifs événementiels** (QR code, upload invité).
- Multi-utilisateurs, comptes, backend cloud, publication sur les stores.
- Modèle économique sans pub (abonnement de soutien).

> ⚠️ Tout ce qui est « ouverture » est **repoussé** : la v1 reste un **outil perso
> autonome**. On ne construit le cloud/social que **si** on décide d'ouvrir.

---

## 🧱 Matériel (mon installation)

| Élément | Détail | Rôle |
|---|---|---|
| 📱 **Mon téléphone** | Android | Source (sync proche du temps réel) |
| 📱 **Téléphone de ma femme** | iPhone (iOS) | Source (sync opportuniste, voir note iOS) |
| 🖥️ **Mac** | pour Xcode / builds iOS | Compilation & debug iOS (EAS cloud possible aussi) |
| 🍓 **Raspberry Pi 3** | quad-core ARM, 1 Go RAM | **Hub** : reçoit des téléphones, écrit sur le NAS, tunnel distant |
| 💾 **NAS QNAP TS-228** | QTS 4.3.6 (EOL), SMB2 | **Stockage final** des médias |
| 🔌 Stockage du Pi | carte SD (ou **petit SSD USB**) | OS du Pi |

### Notes matériel
- **Pi 3 = largement suffisant** pour ce rôle (tunnel + écriture SMB).
  Limite à connaître : Ethernet 100 Mbit/s sur le 3B (~300 sur le 3B+) → photos
  instantanées, grosses vidéos en quelques minutes. OK pour de la sync en tâche de fond.
- **Carte SD en 24/7 → usure.** Idéalement **booter sur un petit SSD USB**
  (faible consommation, sûr à alimenter par le Pi).
- **Disque dur USB auto-alimenté : déconseillé** sur Pi 3 (pic au démarrage du moteur
  ~0,7–1 A → sous-tension/corruption possibles). Si besoin d'un HDD : **hub USB
  alimenté**. **Mais inutile pour les photos** : le **stockage, c'est le NAS** ; le Pi
  ne fait que relayer.

---

## 🏗️ Architecture (v1 perso, sans cloud)

```
                      ╔══════════ Réseau maison (LAN) ═══════════╗
                      ║                                          ║
   📱 Android  ──── HTTP ──►  🍓 Raspberry Pi 3  ── CIFS ──►  💾 QNAP TS-228
   (app sync)    │       ║   - reçoit les médias          (partage monté
                 │       ║   - déduplique + journalise      /mnt/nas/homes)
                 │       ║   - écrit dans le dossier monté ║
                 │       ║   - point d'entrée Tailscale    ║
                 │       ╚════════════════════════════════════════╝
                 │
                 └── à distance : tunnel chiffré (Tailscale/WireGuard) vers le Pi
                     → le Pi est joignable de PARTOUT, le NAS n'est JAMAIS exposé
```

### Comment ça marche (tel qu'implémenté)
1. L'app lit les **albums / dossiers** du téléphone (`expo-media-library`) selon les
   **règles** définies par l'utilisateur.
2. Elle choisit une **adresse joignable** parmi celles enregistrées (Tailscale en
   priorité, repli local) en testant `/` → évite les coupures quand on change de réseau.
3. Pour chaque règle, elle **déduplique** en demandant à l'agent la liste de l'existant
   (`GET /files`) : comparaison **par nom**, puis **nom + taille** pour les cas
   ambigus, avec un **cache local** pour rester instantané sur une sync établie.
4. Les nouveaux médias partent au Pi **en HTTP** (`POST /upload`, binaire brut), avec
   **progression octet par octet** et **1 retry** sur coupure réseau.
5. L'agent **écrit le fichier** dans le dossier NAS visé. Le NAS étant **monté en CIFS**
   sur le Pi, l'agent fait juste une **écriture de fichier** (le noyau Linux gère le SMB).
   Anti-écrasement : même nom + même contenu (SHA1) → ignoré ; contenu différent → `nom(1).ext`.
6. L'agent **journalise** chaque média (`.photosync/journal.jsonl`) → visible dans
   l'écran **Synchros**, même après une réinstallation de l'app.
7. Le téléphone ne parle qu'à **une seule adresse** : l'agent du Pi.

> ℹ️ **État réel** : un **seul compte NAS** (Taaazzz) est utilisé, via le montage CIFS
> `homes`. Le multi-profil par personne (Alexia) **n'est pas encore implémenté**
> (voir la roadmap). Pas de file SQLite : la fiabilité vient de la **déduplication +
> retry + journal**.

### Règles de synchronisation
On associe librement une **source du téléphone** (album, ou dossier avec toute son
arborescence) à un **dossier du NAS**, autant de règles que voulu. Chaque règle a des
options : **inclure les sous-dossiers** (recrée l'arborescence sur le NAS) et **ranger
par date** (sous-dossiers Année/Mois/Jour). L'app **parcourt l'arborescence du NAS**
et peut **créer des dossiers**.

```
  📱 Téléphone (source)        →   💾 NAS (destination)        options
  Album « Camera »             →   homes/admin/.../DCIM         📅 par date
  Dossier DCIM/ (arborescence) →   homes/Backup/Telephone       📂 sous-dossiers
  Album « Screenshots »        →   homes/Backup/Captures
```

### 📱 Comportement par OS (à assumer dans l'app)
| | **Android (moi)** | **iOS (ma femme)** |
|---|---|---|
| Déclenchement | proche du **temps réel** (WorkManager) | **opportuniste** : à l'ouverture de l'app, ou en charge + Wi-Fi |
| Envoi en arrière-plan | service / WorkManager | **background URLSession** (reprend même app fermée) |
| Dossiers sources | vrais dossiers (DCIM…) | photothèque / albums (sélection via le système) |

> ⚠️ **iOS ≠ temps réel.** Apple interdit l'exécution libre en arrière-plan : sur
> l'iPhone, la sauvegarde se fait **automatiquement mais par opportunité** (souvent
> la nuit en charge, ou dès l'ouverture de l'app). Acceptable pour une sauvegarde.
> Astuce d'archi : comme le téléphone envoie au **Pi en HTTP(S)** (pas en SMB), iOS
> utilise `background URLSession` → uploads fiables et **repris** même app fermée.

> Le téléphone n'écrit **pas** directement sur le NAS : passer par le Pi (toujours
> allumé) est **plus fiable** (Android limite le SMB en arrière-plan) et donne
> l'**accès distant** sans exposer le NAS.

---

## 🧰 Stack technique (v1)

| Couche | Techno (réelle) | Détail |
|---|---|---|
| **App mobile** | **React Native + Expo SDK 54** (JavaScript), **pnpm** | Android (build preview). iOS pas encore buildé |
| Accès médias | `expo-media-library` | albums, dossiers, assets |
| Lecture vidéo | **`expo-video`** (ExoPlayer/AVPlayer) | lecteur natif plein écran dans la galerie (play/pause/seek), auth par header |
| Envoi / progression | `expo-file-system` (`createUploadTask`, binaire) | upload + progression octets + retry |
| Détection WiFi | `expo-network` | option « WiFi only » |
| Sync arrière-plan | `expo-background-task` + `expo-task-manager` | tâche périodique (~15-30 min) Android |
| Mises à jour | `expo-updates` (OTA JS) + `expo-application` / `expo-intent-launcher` (APK natif) | OTA au lancement + updater APK in-app |
| **Agent Pi** | **Node.js natif — 0 dépendance npm** | `http, fs, crypto, child_process`… |
| Miniatures | **ffmpeg** (binaire système) | photos **et** vidéos, cache sur le Pi |
| Écriture NAS | **montage CIFS** (noyau Linux) | l'agent écrit un fichier ; le noyau gère SMB |
| État / config / journal | **fichiers JSON** sur le NAS (`.photosync/`) | `config.json`, `journal.jsonl`, `apk-latest.json` — **pas de SQLite** |
| **Tunnel distant** | **Tailscale** | accès de partout, NAS jamais exposé |

> Pas de S3, pas de base de données, pas de backend cloud. L'agent est **sans
> dépendance** (seul `ffmpeg` requis pour les miniatures). La config et le journal
> vivent dans un dossier caché `.photosync/` **sur le NAS** (restaurés après réinstall).

---

## 🔐 Sécurité & accès distant (le NAS n'est JAMAIS exposé)

Le TS-228 est **EOL** (peu de correctifs) → **interdiction de l'exposer sur Internet**.

### Principes
- Sur le Wi-Fi, le NAS est sur le **LAN**, **invisible depuis Internet** (NAT +
  pare-feu de la box).
- **Aucun port ouvert** sur la box. **UPnP désactivé.** myQNAPcloud / DMZ désactivés.
- L'accès distant passe **uniquement** par un **tunnel privé chiffré** (Tailscale /
  WireGuard) **sur le Pi**. Sans clé → NAS injoignable. Fonctionne même derrière
  le NAT opérateur (CGNAT).

### Rôles & permissions (multi-utilisateur de foyer)
La séparation des accès est **gérée par le NAS lui-même** (natif QTS), en
**réutilisant les comptes QNAP déjà créés** :

| Personne | Compte NAS | Accès |
|---|---|---|
| Moi | utilisateur « large » | tous les dossiers de sauvegarde |
| Ma femme | utilisateur restreint | **uniquement ses dossiers** |

- Chaque profil de l'app utilise **les identifiants SMB de la personne** → le NAS
  ne renvoie **que les dossiers autorisés** (la restriction « marche toute seule »,
  aucun bug logiciel ne peut la contourner).
- 💡 Évite d'utiliser le compte **`admin` système** du QNAP dans l'app ; garde-le à
  part et sers-toi de ton compte utilisateur « large ».

### Verrouillage du NAS
*(état vérifié sur le Pi le 2026-06-15)*
- [ ] **Réutiliser les 2 comptes QNAP existants** (moi large / femme restreinte) —
  *partiel* : seul `taaazzz` est monté (`//192.168.1.124/homes` → `/mnt/nas/homes`).
  Le compte restreint d'Alexia dépend du **multi-profil** (non démarré).
- [x] **SMB2 minimum** : côté QNAP, *Win/Mac/NFS → Microsoft Networking → Options
  avancées* → **version SMB la plus ancienne = SMB 2** (SMB1 désactivé, supportées :
  SMB 2 / 2.1 / 3). Le montage CIFS du Pi force aussi `vers=2.0`. *(Mot de passe fort
  = responsabilité du compte QNAP.)*
- [x] **NTLMv2 obligatoire** : activé sur le QNAP le 2026-06-15 (*Microsoft Networking
  → Options avancées → « Autoriser uniquement l'authentification NTLMv2 »*). Durcit
  l'auth SMB. ⚠️ **Piste de dépannage** : si un appareil ancien — ou le montage CIFS
  du Pi — n'arrive plus à s'authentifier après coup, vérifier ce réglage en premier
  (Linux `mount.cifs` utilise NTLMv2 par défaut, donc le Pi devrait rester OK).
- [x] **Identifiants NAS non lisibles en clair sur le Pi** : creds dans
  `/etc/photosync-taaazzz.cred`, permissions **`600` root** (corrigé depuis `644`).
  CIFS n'autorise pas de creds chiffrés — la protection = permissions strictes.
- [x] **NAS non exposé ; tout passe par le Pi** — vérifié côté QNAP (myQNAPcloud) le
  2026-06-15 : **UPnP / réacheminement de port désactivé** (aucun port ouvert sur la
  box), **myQNAPcloud Link désactivé** (pas de relais distant qlink.to), **aucun
  service publié** (Publication décochée). Accès distant = uniquement Tailscale→Pi.
- [x] **Pas le compte `admin` système dans l'app** : compte `taaazzz` (≠ `admin`), et
  l'app ne détient **jamais** les creds NAS (c'est le montage CIFS de l'OS). ⚠️ mais
  `taaazzz` reste niveau admin → moindre privilège pas pleinement atteint.
- [x] **Surface d'attaque réduite** (vérifié le 2026-06-15) : **Telnet, SSH et FTP
  désactivés** sur le QNAP (on accède au NAS uniquement en SMB depuis le Pi).
- [x] **Corbeille réseau QNAP (`@Recycle`) désactivée** le 2026-06-15 (+ corbeilles
  vidées). Elle faisait doublon avec `.photosync-corbeille`, piégeait les `.part` et
  faussait l'espace libéré. Vérifié depuis le Pi : plus aucun `@Recycle`, 853 Go libres.
- [x] **Sécurité QNAP** (vérifié le 2026-06-15) : **anti-force-brute activé sur
  HTTP(S)** (5 échecs/min → blocage 30 min) ; **politique de mot de passe forte**
  (types de caractères, anti-répétition, ≠ nom d'utilisateur) ; certificat ramené au
  **auto-signé QNAP par défaut** (l'ancien Let's Encrypt via myQNAPcloud était expiré
  et non renouvelable une fois le distant coupé). *Choix délibérés* : pas de liste
  blanche IP (NAS déjà non exposé) ; anti-force-brute SAMBA **laissé off** (le Pi
  s'auth en SMB en continu — éviter de le bloquer si les creds dérivent ; menace LAN
  improbable à la maison).

### Verrouillage du Pi (la vraie passerelle)
*(durci le 2026-06-15 — le Pi détient l'agent, l'accès SSH et les creds NAS : s'il tombe, tout tombe)*
- [x] **SSH par clé uniquement** (drop-in `/etc/ssh/sshd_config.d/99-photosync-hardening.conf`,
  vérifié via `sshd -T`) : `PasswordAuthentication no`, `KbdInteractiveAuthentication no`,
  `PermitRootLogin no`, `PubkeyAuthentication yes`. Brute-force SSH impossible ; root SSH
  refusé (réglage **`no`** + aucune clé root). Clé `~/.ssh/id_ed25519` du poste de dev →
  voir mémoire `pi-ssh-deploy`.
- [x] **Firewall `ufw`** : `deny incoming` / `allow outgoing` par défaut, et en entrée
  **seulement** : tout sur `tailscale0` (app à distance + admin), **SSH (22)** et
  **agent (8080)** depuis `192.168.1.0/24`, UDP 41641 (Tailscale direct). Tout le reste
  bloqué → même un port-forward accidentel de la box vers le Pi serait rejeté (source
  hors LAN). Persistant au reboot.
- [x] **Surface réduite** : **CUPS désactivé** (service d'impression inutile, plus de
  `:631`). Ports en écoute restants : 22 + 8080 (LAN/tailnet via ufw), 443 (Tailscale
  serve, tailnet only).
- [x] **Mises à jour de sécurité automatiques** : `unattended-upgrades` couvrant
  **Debian + Raspbian** (origines ajoutées, sinon presque rien n'est patché sur
  Raspbian), **sans redémarrage auto** (reboot manuel pour un Pi de sauvegarde).
- [x] **Runtime Node hors EOL** (2026-06-15) : l'agent tournait sur **Node 20** (fin de
  support fin avril 2026). Passé à **Node 22 LTS** (`/opt/node22`, dernière LTS encore
  dispo en 32-bit armv7l), Node 20 gardé pour rollback. Cible sur le futur Pi 64-bit :
  **Node 24 LTS** (cf. [`PI-MIGRATION-BOOKWORM.md`](PI-MIGRATION-BOOKWORM.md)).
- [x] **Token agent non lisible en clair** (2026-06-16) : `PHOTOSYNC_TOKEN` sorti de
  l'unité systemd (`Environment=`, lisible en 644 via `systemctl cat`) vers un
  **`EnvironmentFile=/etc/photosync-agent.env` en `600` root** — même standard que les
  creds CIFS. Token toujours exigé (vérifié : 200 avec / 401 sans).
- ⚠️ **Chaîne de privilèges à surveiller** (analyse 2026-06-15) : **clé SSH sans
  passphrase** (facteur unique) + **`sudo` NOPASSWD** pour `taaazzz` + **creds NAS de
  niveau admin** sur le Pi. Empilés, ces 3 choix (chacun défendable isolément)
  suppriment toute marche d'escalier entre « poste de dev compromis » et « contrôle
  total du NAS » : poste → SSH Pi → root (NOPASSWD) → admin NAS (creds). Deux leviers
  réduisent la chaîne **sans casser l'automatisation** :
  - **Compte NAS en utilisateur standard** (≠ admin) → coupe le **dernier** maillon :
    Pi rooté = accès aux fichiers des partages seulement, pas à la config du NAS. *Gain
    le plus net* — **à faire**, lié au [verrouillage NAS](#verrouillage-du-nas) (le
    compte `taaazzz` du montage est aujourd'hui admin).
  - **Passphrase sur la clé SSH + ssh-agent** → coupe le **premier** maillon. Compatible
    si les déploiements sont **interactifs** (agent chargé en session) ; incompatible en
    non-interactif (cron/CI) → là le NOPASSWD garde son sens.
- ℹ️ **Résiduels assumés (choix permanents)** : agent en **HTTP clair sur le LAN** (token
  48 car. en en-tête — nécessaire pour la sync Wi-Fi locale ; chiffré via Tailscale à
  distance).
- 🔴 **ÉCHÉANCE DURE — `bullseye` → `bookworm`** : Debian 11 **fin de support LTS le
  2026-08-31** (source officielle Debian). Après cette date, `unattended-upgrades` ne
  trouvera **plus aucun correctif** sur les origines Bullseye → la passerelle qui porte
  toute la sécurité deviendrait non maintenue (exactement ce qu'on évite côté NAS).
  Debian 12 redonne du support jusqu'en ~2028. **Stratégie retenue** : **install neuve
  sur une carte SD neuve** (Pi OS bookworm 64-bit), ancienne carte gardée en **rollback**
  — pas de `dist-upgrade` en place. La config/les données vivent sur le NAS → rien à
  migrer. **Plan complet prêt à dérouler : [`PI-MIGRATION-BOOKWORM.md`](PI-MIGRATION-BOOKWORM.md).**

### Synthèse des chemins
| Situation | Chemin | Exposition |
|---|---|---|
| À la maison | Android → Pi (Wi-Fi local) → NAS (SMB2) | Aucune ✅ |
| À distance | Android → **tunnel Tailscale → Pi** → NAS (SMB2) | Aucune ✅ |
| À éviter | Port forwarding / UPnP / NAS exposé | ❌ |

---

## 📁 Structure du projet (v1)

```
photo/
├── mobile/                    # App Expo (Android) — JavaScript, pnpm
│   ├── App.js                 # écran principal + tableau de bord + menu Réglages
│   ├── Gallery.js             # galerie NAS (miniatures, lecture vidéo, sélection multiple)
│   ├── Maintenance.js         # doublons, réorganisation par date, accès corbeille
│   ├── SyncLog.js             # écran « Synchros » (fichiers envoyés + échecs)
│   ├── TrashView.js           # corbeille NAS (voir / restaurer / supprimer)
│   ├── FolderPicker.js        # navigateur de dossiers du NAS
│   ├── PhoneFolderPicker.js   # navigateur de dossiers du téléphone
│   ├── lib/
│   │   ├── sync.js            # moteur : règles, dédup nom+taille, upload, retry, arbo
│   │   ├── config.js          # sauvegarde / restauration de la config sur le NAS
│   │   ├── appupdate.js       # mise à jour de l'APK in-app (depuis le NAS)
│   │   ├── auth.js            # injection du token X-PhotoSync-Token
│   │   ├── history.js         # historique local des syncs
│   │   ├── notify.js          # notifications
│   │   └── toast.js           # toasts
│   ├── assets/                # icône + logo (logo_apk)
│   ├── app.json · eas.json · package.json
│
├── pi-agent/
│   ├── src/server.js          # agent : Node natif, 0 dépendance (déployé sur le Pi)
│   └── photosync-agent.service # unit systemd
│
├── logo_apk.png · logo_apk.svg
├── LICENSE                    # licence propriétaire
└── README.md
```

> Le déploiement réel de l'agent est `~/photosync-agent/server.js` **sur le Pi**
> (hors dépôt), poussé par `scp` (voir « Déploiement de l'agent »).

---

## 🔧 Système réel & workflow (état actuel)

> Cette section décrit ce qui est **réellement en place et utilisé**, pas la théorie.

### 📇 Adresses & accès (à ne pas oublier)
| Élément | Adresse |
|---|---|
| **NAS QNAP TS-228** (LAN) | `192.168.1.124` |
| **Raspberry Pi** (LAN) | `192.168.1.136` |
| **Raspberry Pi** (Tailscale) | `100.105.226.90` |
| **Box / passerelle** | `192.168.1.254` |
| **Tailnet** (domaine Tailscale) | `tail8e8ec4.ts.net` |
| **DNS externe du Pi** (Tailscale, HTTPS) | `raspberrypi.tail8e8ec4.ts.net` |
| **URL de l'agent (à distance, HTTPS)** | `https://raspberrypi.tail8e8ec4.ts.net` |
| **URL de l'agent (local, HTTP)** | `http://192.168.1.136:8080` |
| Compte NAS utilisé par l'app | `Taaazzz` (admin) |
| Dossier monté sur le Pi | `/mnt/nas` → **tous les partages QNAP** montés en CIFS (`/mnt/nas/<partage>`) avec les identifiants `Taaazzz` |

### Composants
| Élément | Détail |
|---|---|
| **App mobile** | `mobile/` — Expo **SDK 54**, gestionnaire **pnpm**. Distribuée en **build preview** (APK autonome) + **OTA** pour le JS. |
| **Agent Pi** | `pi-agent/src/server.js` — Node natif (**0 dépendance**), service **systemd** `photosync-agent`. `NAS_ROOT=/mnt/nas` (tous les partages), métadonnées sur `homes` via `META_ROOT`. |
| **NAS** | QNAP **TS-228** (`192.168.1.124`), **tous les partages** accessibles à **Taaazzz** (homes, Multimedia, Public, Download, Recordings, Pc-Portable-dossier, Web, Sauvegarde S22 Bruno, Sauvegarde Google photo, PhotoAlexia\_IOS2026 en **lecture seule**). |
| **Pi** | Raspberry Pi 3 (`192.168.1.136`), Raspbian bullseye, Node 22 LTS. |

### Endpoints de l'agent (HTTP, port 8080)
> `GET /` est public (santé). **Tous les autres** exigent le token (header
> `X-PhotoSync-Token` ou `?token=`). **Fail-closed** : sans `PHOTOSYNC_TOKEN` chargé,
> l'agent **refuse tout (401)** au lieu de s'ouvrir — sauf mode ouvert *volontaire*
> `PHOTOSYNC_ALLOW_OPEN=1` (dev / réseau de confiance).

**Fichiers & dossiers**
| Méthode | Endpoint | Rôle |
|---|---|---|
| GET | `/` | santé (`PhotoSync agent OK`) — public |
| GET | `/folders?path=` | sous-dossiers (non récursif) |
| GET | `/ls?path=` | contenu non récursif : dossiers + fichiers typés (image/vidéo/autre) |
| GET | `/files?path=&withSize=1` | noms (récursif) pour la **dédup** ; `withSize=1` ajoute les tailles |
| POST | `/mkdir?path=` | créer un dossier |
| POST | `/upload?name=&dest=&src=` | recevoir un média (binaire) ; **dédup SHA1 / renommage** ; **journalise** |
| GET | `/file?path=` | servir un fichier — **requêtes Range / HTTP 206** (streaming + seek vidéo) ; `.apk` en téléchargement installable |
| GET | `/stats` | espace disque NAS (`statfs`) |

**Miniatures / galerie**
| Méthode | Endpoint | Rôle |
|---|---|---|
| GET | `/thumb?path=&w=` | miniature **ffmpeg** (photo **et** vidéo), largeur 64–2048, cache Pi |
| POST | `/thumbs-generate?path=` · GET `/thumbs-status` · POST `/thumbs-cancel` | job de pré-génération + suivi + annulation |

**Maintenance**
| Méthode | Endpoint | Rôle |
|---|---|---|
| GET | `/scan-dupes?path=` | doublons (taille puis SHA1) + octets gaspillés |
| POST | `/reorganize?path=&apply=` · GET `/reorganize-status` | ranger Année/Mois/Jour (apply=1) + progression |
| POST | `/trash?path=` | mettre à la corbeille (`.photosync-corbeille`) |
| GET | `/trash-list` · POST `/trash-restore` · `/trash-delete` · `/trash-empty` | lister / restaurer / supprimer / vider la corbeille |

**Config, journal, mises à jour, logs**
| Méthode | Endpoint | Rôle |
|---|---|---|
| GET·POST | `/config` | lire / **sauvegarder la config** de l'app (`.photosync/config.json`) |
| GET | `/journal?limit=` · POST `/journal-clear` | **journal** des médias (`.photosync/journal.jsonl`) / vider |
| GET·POST | `/apk-latest` | métadonnée de la **dernière version APK** (updater in-app) |
| GET | `/logs?n=` | journal mémoire des requêtes (diagnostic) |

> Les métadonnées vivent dans `.photosync/` et la corbeille dans
> `.photosync-corbeille/` — dossiers cachés, **ignorés** des scans/réorganisations.

### Fonctionnalités de l'app (en place et fonctionnelles)
- **Tableau de bord** (accueil) : médias sauvegardés (cumul persistant), règles
  actives, **espace libre NAS** + barre d'occupation, dernière sauvegarde, **progression
  en direct** du transfert (fichier + octets, utile pour les vidéos), statut, historique.
- **Règles de synchronisation multiples** :
  - source = **album** du téléphone **ou** **dossier complet** (arborescence) ;
  - option **📂 Inclure les sous-dossiers** → recrée l'arborescence sur le NAS ;
  - option **📅 Par date** (par règle) + interrupteur global **« Ranger tous les
    dossiers par date »** → range aussi le fourre-tout déjà présent (Année/Mois/Jour),
    dossier par dossier, idempotent.
- **Sync manuelle** + **automatique en arrière-plan** (~15-30 min), option **WiFi only**.
- **Sélection d'adresse robuste** : teste les adresses enregistrées (Tailscale / local)
  et prend la première joignable → pas de « Network request failed » au changement de réseau.
- **Déduplication par le NAS** : **nom**, puis **nom + taille** pour les cas ambigus
  (un même nom de taille différente n'est plus masqué), avec **cache local** (instantané).
- **Écran « Synchros »** : fichiers réellement envoyés (miniature, **source → dossier
  NAS**, taille, date — vient de l'agent, survit aux réinstallations) **+ liste des
  échecs** avec bouton **« Relancer la synchro »**.
- **Galerie NAS** : grille de miniatures (ffmpeg, photos **et** vidéos) avec le **nom
  de fichier** sous chaque tuile, aperçu photo plein écran (1600 px, rapide) et
  **lecture vidéo en plein écran** (lecteur natif `expo-video` : play/pause/seek,
  plein écran, streaming via Range), **sélection multiple + suppression groupée**,
  **pré-génération des miniatures en fond** avec progression.
- **Maintenance NAS** : doublons **côte à côte** (miniatures) + mise à la corbeille ;
  **réorganisation par date** (aperçu → appliquer, progression, sécurité « déjà rangé ») ;
  **corbeille** (voir / restaurer / supprimer / vider).
- **Authentification optionnelle par token** (Réglages › Serveur) si l'agent l'exige.
- **Config sauvegardée sur le NAS** + **restauration automatique** après réinstallation
  (on ressaisit juste l'adresse du Pi, le reste revient seul).
- **Mises à jour** : **OTA** (JS) avec **écran au démarrage** + **updater APK in-app**
  (télécharge depuis le NAS, lance l'installateur) — bandeau « nouvelle version » + Réglages › À propos.
- **Menu Réglages** structuré (Serveur · Règles · Sauvegarde auto · Maintenance · À propos),
  **notifications**, **toasts**, **marges adaptatives** (safe-area), multi-adresses serveur.

### Build & mise à jour (comme missioflow-mobile)
```bash
# Build APK autonome — installe et marche SANS Metro/tunnel.
# A refaire uniquement si on ajoute un module NATIF.
pnpm dlx eas-cli@latest build --profile preview --platform android

# Mises a jour JS ensuite : OTA, sans rebuild ni reinstallation.
pnpm dlx eas-cli@latest update --branch preview -m "message"
```
- Le **preview build** embarque le JS → l'app s'installe et tourne **seule**.
- `eas update` pousse les changements **JS** → l'app se met à jour **au lancement**.
- **runtimeVersion = fingerprint** : un changement **natif** est détecté → impose un nouveau build (l'OTA ne livre jamais du natif).
- Canal **`preview`** (réglé dans `eas.json`) relié à la branche `preview` des updates.

#### Build local (quota EAS épuisé)
Les builds Android EAS du plan gratuit sont limités par mois. Quand ils sont
épuisés, **builder en local** (ne consomme pas de quota), avec le **keystore EAS**
(donc installable par-dessus) :
```bash
pnpm dlx eas-cli@latest build --local --profile preview --platform android --non-interactive
```
> JDK 17 (pas 21), SDK Android présent. Astuce mémoire Gradle : mettre
> `org.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=1536m` dans `~/.gradle/gradle.properties`
> (prioritaire sur le projet) pour éviter l'`OutOfMemoryError: Metaspace`.
> Versioning : `eas.json` `appVersionSource=local` + `version`/`android.versionCode` dans `app.json`.

#### Mise à jour de l'app depuis le NAS (sans store, in-app)
Pour livrer un **nouveau build natif** sans QR/transfert manuel :
1. Build local → **upload** de l'APK sur `Taaazzz/_apk/` (via `POST /upload`).
2. **`POST /apk-latest`** avec `{version, versionCode, name, rel, size, notes}`
   (penser à **incrémenter** `version`/`versionCode` dans `app.json` au build).
3. L'app compare le `versionCode` installé (`expo-application`) à `/apk-latest` →
   bandeau « nouvelle version » → télécharge depuis `/file` → installe via
   `expo-intent-launcher` (permission `REQUEST_INSTALL_PACKAGES`).

### Tailscale (accès distant HTTPS)
- `tailscaled` activé au boot (`sudo systemctl enable tailscaled`) → connexion + config
  `serve` restaurées automatiquement.
- Front HTTPS : `tailscale serve --bg 8080` (proxy `https://raspberrypi.tail8e8ec4.ts.net` → `localhost:8080`).
- Vérif après reboot : `tailscale serve status` + `curl https://raspberrypi.tail8e8ec4.ts.net/`.
- Secours si le serve ne persiste pas : service `photosync-serve.service` (oneshot
  qui relance `tailscale serve --bg 8080` au boot).

### Déploiement de l'agent (Pi)
```bash
# Depuis le PC :
scp ~/projects/photo/pi-agent/src/server.js taaazzz@192.168.1.136:~/photosync-agent/server.js
# Sur le Pi :
sudo systemctl restart photosync-agent
```

### 💡 Idées d'amélioration (prochaines)
Repérées en construisant l'état ci-dessus :

- ✅ **Authentifier l'agent.** *(fait)* Token `PHOTOSYNC_TOKEN` exigé (header
  `X-PhotoSync-Token` ou `?token=`) sur tous les endpoints sauf `/`, comparé à
  **temps constant** (`crypto.timingSafeEqual`, anti-attaque temporelle). Saisi dans
  l'app (Réglages > Serveur), non sauvegardé sur le NAS (secret), chargé sur le Pi via
  `EnvironmentFile` en 600. **Fail-closed** : sans token, l'agent **refuse tout (401)**
  (mode ouvert seulement si `PHOTOSYNC_ALLOW_OPEN=1` explicite).
- ✅ **Déduplication plus sûre.** *(fait)* Dedup **nom + taille** (`/files?withSize=1`)
  au lieu du nom seul → un fichier de même nom mais de taille différente n'est plus
  masqué. Rapide grâce au repérage en 2 temps + cache local (`syncedKeys` = id:mtime).
  **Anti-collision + intégrité à l'écriture** *(fait)* : `finalizePart` ne remplace
  JAMAIS un fichier différent — même nom + même **SHA1** → doublon sauté (`deduped`),
  même nom + contenu différent → renommage `name(1).jpg` (`freeNamePath`, `renamed`).
- ✅ **Affichage des échecs + réessai automatique.** *(fait)* Les échecs sont mémorisés
  (`AsyncStorage.failures`) et affichés dans **Synchros** (bouton « Relancer »). Le
  réessai est **automatique** : côté **JS** (1er plan + tâche de fond 15 min), la
  dédup se fait sur la **vérité du NAS** (`/files`) — un fichier non présent est
  forcément repris au passage suivant. Côté **natif** (option B, app fermée), une
  **file de réessai persistante** (`retryQueue` en SharedPreferences,
  `Uploader.loadRetry/saveRetry`) re-tente chaque échec jusqu'au succès ; on ne lâche
  un élément que si sa **source a disparu** du téléphone (bornée à 500, le JS restant
  le filet ultime). Avant, le natif avançait `lastAdded` et perdait l'échec.
- ✅ **Corbeille dans l'app.** *(fait)* `TrashView` : voir / restaurer / supprimer /
  vider `.photosync-corbeille`. Mise à la corbeille **depuis la Galerie** (à l'unité
  depuis l'aperçu, ou en **sélection multiple**) **et depuis Synchros** (détail d'un
  fichier → 🗑 ; déplace via `/trash` puis retire l'entrée via `/journal-remove`).
- ✅ **Upload résumable** des gros fichiers (par morceaux). *(fait)* Au-delà de
  **24 Mo**, l'envoi se fait par tranches de **6 Mo** vers `/upload-chunk` (fichier
  `.part` côté agent) : une coupure ne reperd qu'une tranche, on reprend à l'**offset
  réel** (`GET /upload-offset`, resync sur `409`) au lieu de tout renvoyer. Couvre le
  **premier plan** (JS, `mobile/lib/sync.js`) **et l'arrière-plan natif** (Kotlin,
  `modules/photosync-bg` — `Uploader.uploadChunkedNative`).
- ✅ **Rotation du journal** côté agent. *(fait)* `journal.jsonl` est borné : au-delà
  de **6 000 lignes**, `appendJournal` rogne la tête (les plus anciennes) pour n'en
  garder que les **5 000** dernières → la SD du Pi ne sature pas et l'écran Synchros
  reste rapide (sinon `/journal` relisait un fichier qui ne faisait que grossir).
- 👥 **Multi-profil** (Alexia / iOS) — *non démarré* : profil par personne avec ses
  identifiants NAS, sa config et son dossier ; **+ version iOS**.
- 📡 **Découverte auto du Pi** (mDNS / scan LAN) pour éviter la saisie d'adresse.
- 🌐 **MagicDNS** : non résolu dans l'app → on utilise l'**IP Tailscale** (stable).

---

## 🚀 Plan de développement

### Phase 0 — MVP perso / foyer (objectif premier) 🎯
1. **Agent Pi** : petit service qui reçoit un fichier et l'écrit en **SMB2** sur le
   TS-228 (test manuel d'abord : un fichier → dossier NAS, avec mon compte).
2. File d'attente + reprises + journal (SQLite) sur le Pi.
3. **App mobile** (une base RN/Expo) : détection des nouveaux médias + envoi au Pi
   en **Wi-Fi local**. Cible **Android d'abord**, **iOS en parallèle** (même code).
4. **Profils du foyer** : connexion par personne (moi / ma femme) avec leurs
   **identifiants SMB QNAP existants**.
5. **Règles de sync** : associer dossier(s) du tél → dossier(s) du NAS + navigateur
   de dossiers NAS (filtré par les droits du compte).
6. Sauvegarde **automatique** en arrière-plan :
   - Android : WorkManager (proche temps réel)
   - iOS : `BGProcessingTask` + **background URLSession** (opportuniste)
7. **Build iOS** : test sur l'iPhone via Xcode/Mac, puis **TestFlight** (compte
   Apple Developer requis pour un usage quotidien durable).
8. **Tailscale** sur le Pi + les téléphones → sync **à distance**.
9. Réglages : dossiers à surveiller, Wi-Fi only, en charge, état/journal.

➡️ **Fin de Phase 0 = mon besoin est résolu.** Le reste est optionnel.

### Phase 1 — Confort & robustesse (perso)
7. Sync des vidéos ✅ + gros fichiers avec **progression en direct** ✅ + **lecture
   vidéo en plein écran dans l'app** ✅ (`expo-video` + streaming Range côté agent ;
   reprise résumable des uploads ✅ premier plan + arrière-plan natif)
8. Dédup à l'envoi : ne pas re-pousser ce qui est déjà sur le NAS ✅ (l'agent
   `GET /files` liste l'existant, dédup **nom + taille** côté app ✅ + anti-collision
   et intégrité **SHA1** à l'écriture côté agent ✅ `finalizePart`/`freeNamePath`)
9. **Scanner de doublons sur le NAS** ✅ (`/scan-dupes` taille + SHA1, miniatures
   côte à côte + corbeille dans l'écran Maintenance)
10. **Réorganisation par date** ✅ (`/reorganize`, par règle + global à la sync)
11. **Journal des synchros** ✅ (écran Synchros : source → destination, miniature)
12. **Config sauvegardée/restaurée sur le NAS** ✅ + **mise à jour de l'app in-app** ✅
13. Boot du Pi sur SSD USB, service systemd, redémarrage auto
14. Notifications (sync faite / échec) ✅

### Phase 2+ — Ouverture (seulement si on décide d'ouvrir)
11. Backend cloud + comptes multi-utilisateurs
12. App de partage : feed, profils, lecteur
13. **Albums collaboratifs événementiels** (QR code, upload invité, dédup pHash)
14. Connecteurs génériques (WebDAV/FTP, autres NAS)
15. Publication App Store + Play Store
16. Modèle économique sans pub (abonnement de soutien)

---

## 🔮 Vision future (si ouverture au public)

Ces idées **ne font pas partie de la v1**, mais cadrent la direction si le projet
s'ouvre. Elles reposent sur les **3 piliers** :

1. **Qualité originale préservée** + édition non destructive (vs compression
   d'Instagram/WhatsApp).
2. **Albums collaboratifs événementiels** : un mariage/anniversaire où tout le monde
   dépose ses photos via QR code, **sans compte**, en pleine qualité, dédupliquées.
3. **Sync vers ta propre destination** (NAS) : la version produit de ce qu'on
   construit déjà en v1.

**Modèle économique envisagé :** **sans publicité**, sans revente de données.
**Abonnement de soutien** optionnel (~3–5 €/mois) pour financer le serveur — il
**finance l'infra**, il ne verrouille pas l'app. La sync-vers-NAS allège aussi le
coût (la copie « pour toujours » vit chez l'utilisateur).

> Différence de promesse vs Google Photos : *« tes photos, en pleine qualité, sans
> pub ni surveillance — et déposées chez toi, sur ton NAS. »*

---

## ⚙️ Installation (à venir)

### Raspberry Pi (agent)
L'agent est **un seul fichier Node sans dépendance npm**. Prérequis sur le Pi :
**Node**, **ffmpeg** (miniatures), et le **NAS monté en CIFS** (ex. `/mnt/nas/homes`).
```bash
# Prérequis
sudo apt install -y ffmpeg cifs-utils
# (montage CIFS du partage NAS dans /etc/fstab → /mnt/nas/homes)

# Déployer l'agent (depuis le PC)
scp pi-agent/src/server.js taaazzz@192.168.1.136:~/photosync-agent/server.js
scp pi-agent/photosync-agent.service taaazzz@192.168.1.136:/tmp/
# Sur le Pi : installer le service + Tailscale
sudo cp /tmp/photosync-agent.service /etc/systemd/system/
sudo systemctl enable --now photosync-agent
curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up
tailscale serve --bg 8080           # front HTTPS optionnel (navigateur)
```
> Auth optionnelle : décommenter `Environment=PHOTOSYNC_TOKEN=...` dans le service.

### App mobile (Android — gestionnaire **pnpm**)
```bash
cd mobile
pnpm install
pnpm exec expo start --dev-client --tunnel   # dev (ou directement un build preview)

# Build APK autonome (installe et marche sans Metro) :
pnpm dlx eas-cli@latest build --profile preview --platform android
# Mises à jour JS ensuite (OTA, sans rebuild) :
pnpm dlx eas-cli@latest update --branch preview -m "message"
```
> **iOS : pas encore buildé** (prévu pour le téléphone d'Alexia). La base de code
> Expo est cross-plateforme ; voir la note iOS / distribution ci-dessous le moment venu.

> **iOS — prérequis** : un **compte Apple Developer (99 $/an)** pour un usage
> quotidien durable (TestFlight ; sinon l'install libre expire tous les 7 jours).
> Le **Mac** sert à Xcode/debug ; les builds peuvent aussi se faire via **EAS cloud**.

#### 📦 Distribution iOS : TestFlight vs App Store « non répertorié » (à ne pas oublier)

| Méthode | Expiration | Pour qui | Note |
|---|---|---|---|
| **TestFlight** | **90 jours / build** | démarrage / test | il faut **republier une build tous les ~3 mois** ; sinon l'app **ne se lance plus** (pas supprimée, juste bloquée). Mise à jour = 1 tap côté iPhone |
| **App Store *unlisted*** | **aucune** ✅ | usage long terme | app **normale**, **mises à jour auto**, **n'expire jamais** ; nécessite de passer **la revue Apple une fois** ; lien privé non cherchable |
| **Ad Hoc** | ~1 an (certificat) | alternative | lié aux **UDID** des iPhones, install plus manuelle, renouvellement annuel |

- ⚠️ **« 90 jours » = la build cesse de démarrer**, ce n'est **pas** une réinstallation
  complète : on republie (quelques min via EAS), l'iPhone fait **« Mettre à jour »**.
- ⚠️ TestFlight nécessite l'**app TestFlight** installée sur l'iPhone.
- 🎯 **Plan** : démarrer en **TestFlight**, puis basculer en **App Store unlisted**
  pour du **zéro-entretien** (plus de limite 90 jours) une fois l'app stable.
- 🔔 **Rappel** : tant qu'on est en TestFlight → **republier une build avant chaque
  échéance de 90 jours**.

### Variables d'environnement (agent Pi — dans le service systemd)
```env
PORT=8080                          # port d'écoute (défaut 8080)
NAS_ROOT=/mnt/nas                  # racine = dossier contenant un sous-dossier par PARTAGE QNAP monté
PHOTOSYNC_META_ROOT=/mnt/nas/homes # où vivent config/journal/APK (partage homes), indépendant de NAS_ROOT
PHOTOSYNC_DEFAULT_SHARE=homes      # partage de rattachement des chemins de sync « nus » (compat ancienne app)
PHOTOSYNC_TOKEN=                   # vide = ouvert ; défini = auth par token exigée
```

> **Multi-partages & permissions.** `NAS_ROOT=/mnt/nas` expose **tous les partages**
> auxquels le compte `Taaazzz` a accès (montés en CIFS sous `/mnt/nas/<partage>`). La
> galerie de l'app les liste via **`GET /shares`** (avec un drapeau `readOnly` **lu en
> direct sur le NAS** par une écriture témoin — on **reproduit** les droits QNAP, on
> n'en invente pas). Toute écriture vers un partage en lecture seule (ex.
> `PhotoAlexia_IOS2026`) est **refusée (403)** par l'agent, en plus du refus du QNAP.
> Endpoints de gestion : **`POST /trash`** (fichier **ou dossier**, vers une corbeille
> par partage), **`POST /move?from=&to=&name=`** (renommer / déplacer ; instantané dans
> un partage, copie+suppression entre partages). Compat : un chemin de sync « nu »
> envoyé par une ancienne APK (`Camera Uploads`) est rattaché à `homes/` automatiquement.

> Les **identifiants du NAS** ne sont **pas** dans l'agent : ils servent au **montage
> CIFS** (fichier de credentials référencé dans `/etc/fstab`, lisible par root seul).
> L'agent ne fait qu'écrire dans le dossier monté.

---

## 📜 Propriété & licence

**Code source propriétaire et privé** — ce projet **n'est pas open source**.
Tous droits réservés. Licence : voir le fichier **[`LICENSE`](LICENSE)** (et `mobile/LICENSE`).
Contact : **contact@taaazzz-prog.fr**.
