diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index bfad483..f59187d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: Frontend unit tests run: node tests/frontend/unit.test.mjs - - name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic) + - name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop) run: | cd tests/frontend if [ -d node_modules ]; then @@ -50,6 +50,7 @@ jobs: node collab.test.mjs node mobile-editor.test.mjs node semantic-search.test.mjs + node desktop.test.mjs else echo "tests/frontend/node_modules missing - installing jsdom" npm install --no-audit --no-fund --silent @@ -61,6 +62,7 @@ jobs: node collab.test.mjs node mobile-editor.test.mjs node semantic-search.test.mjs + node desktop.test.mjs fi # ── Tests ───────────────────────────────────────────────────────── diff --git a/.gitea/workflows/desktop-build.yml b/.gitea/workflows/desktop-build.yml index 41bc784..badb258 100644 --- a/.gitea/workflows/desktop-build.yml +++ b/.gitea/workflows/desktop-build.yml @@ -37,13 +37,27 @@ jobs: - name: Build MSI working-directory: desktop - run: cargo tauri build --bundles msi + shell: powershell + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + if ([string]::IsNullOrEmpty($env:TAURI_SIGNING_PRIVATE_KEY)) { + Write-Host "TAURI_SIGNING_PRIVATE_KEY absent - build sans artefacts de mise a jour." + cargo tauri build --bundles msi --config '{"bundle":{"createUpdaterArtifacts":false}}' + } else { + Write-Host "Signature des artefacts de mise a jour activee." + cargo tauri build --bundles msi + } + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Upload MSI artifact uses: actions/upload-artifact@v4 with: name: obsigate-windows-msi - path: desktop/target/release/bundle/msi/*.msi + path: | + desktop/target/release/bundle/msi/*.msi + desktop/target/release/bundle/msi/*.msi.sig retention-days: 30 - name: Publish to Gitea Release @@ -82,11 +96,31 @@ jobs: - name: Build AppImage working-directory: desktop - run: cargo tauri build --bundles appimage + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then + echo "TAURI_SIGNING_PRIVATE_KEY absent - build sans artefacts de mise a jour." + cargo tauri build --bundles appimage --config '{"bundle":{"createUpdaterArtifacts":false}}' + else + echo "Signature des artefacts de mise a jour activee." + cargo tauri build --bundles appimage + fi - name: Build deb working-directory: desktop - run: cargo tauri build --bundles deb + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: | + if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then + echo "TAURI_SIGNING_PRIVATE_KEY absent - build sans artefacts de mise a jour." + cargo tauri build --bundles deb --config '{"bundle":{"createUpdaterArtifacts":false}}' + else + echo "Signature des artefacts de mise a jour activee." + cargo tauri build --bundles deb + fi - name: Upload Linux artifacts uses: actions/upload-artifact@v4 @@ -94,5 +128,7 @@ jobs: name: obsigate-linux path: | desktop/target/release/bundle/appimage/*.AppImage + desktop/target/release/bundle/appimage/*.AppImage.sig desktop/target/release/bundle/deb/*.deb + desktop/target/release/bundle/deb/*.deb.sig retention-days: 30 diff --git a/.gitignore b/.gitignore index d963680..a194936 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ desktop/backend/ desktop/frontend/ backend/VERSION +# Tauri updater signing keys (private key — never commit) +desktop/*.key +desktop/*.key.pub + diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c7f53..8748a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,24 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Ajouté +- **#77 Desktop — Signature des mises à jour Tauri** — la paire de clés `minisign` + de l'updater est générée et sa clé publique est embarquée dans + `desktop/tauri.conf.json` (`plugins.updater.pubkey`, remplace le placeholder) ; + `bundle.createUpdaterArtifacts: true` produit les fichiers `.sig` par artefact. + Le workflow `.gitea/workflows/desktop-build.yml` expose les secrets + `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` aux builds + Windows et Linux, avec repli automatique en build **non signé** si le secret est + absent (CI toujours verte). Procédure complète : + [docs/DEVELOPMENT_AND_RELEASES.md](./docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri). + Reste à produire le manifeste `latest.json`. + +- **#77 Desktop — Protocole de tests E2E manuels** — nouveau + [`docs/DESKTOP_E2E_CHECKLIST.md`](./docs/DESKTOP_E2E_CHECKLIST.md) : prérequis, + étapes et résultat attendu pour les 6 scénarios OS non automatisables + (installation, tray, notifications natives, association `.md`, auto-update, + désinstallation), avec emplacements des logs/config et procédure de signature + de l'updater Tauri. + - **#70 Recherche sémantique — Embeddings vectoriels** — la recherche comprend désormais le **sens** de la requête en plus des mots-clés. **Embeddings** : chaque document est découpé en chunks de 512 mots (recouvrement 64) puis vectorisé (384 dim) via `all-MiniLM-L6-v2` @@ -156,6 +174,24 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Corrigé +- **#77 Desktop — bannière de premier lancement réaffichée à chaque démarrage** — + l'état du wizard « Choisissez votre vault » n'était mémorisé que dans le + `localStorage` de la webview et le paramètre `hasVaultPath` n'était jamais + renseigné. Ajout d'un booléen persistant `wizard_done` dans la config desktop + (`desktop/src/main.rs`, commandes `get_wizard_state` / `complete_wizard`, + rétro-compatible via `#[serde(default)]`) : la bannière ne réapparaît plus après + un choix de dossier ou un clic « Plus tard », même si le stockage webview est + vidé. Tests Rust (`desktop/src/main.rs`) et JSDOM + (`tests/frontend/desktop.test.mjs`) ajoutés. + +- **Build Docker — « No space left » lors de `apt-get install`** — le stage *builder* + installait inutilement `libpango-1.0-0`, `libpangocairo-1.0-0` et `shared-mime-info` + (libs runtime de WeasyPrint, sans usage à la compilation), dupliquant les téléchargements + apt. Ces paquets sont retirés du builder et `apt-get clean` est ajouté dans les deux + stages pour purger les `.deb` conservés par défaut sous Debian trixie, réduisant + l'empreinte disque et le temps de build. En complément, purge du cache de build + (`docker builder prune`) ayant libéré l'espace nécessaire. + - **BUG-005 — Chargement mobile incomplet derrière Cloudflare (`og.dracodev.net`)** — le service worker utilisait une stratégie **cache-first avec un nom de cache fixe** (`obsigate-v1`) et un précache de chemins erronés (`/frontend/js/…`) : le Cache Storage diff --git a/Dockerfile b/Dockerfile index 32c0f7b..101e789 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ FROM python:3.11-slim AS builder RUN apt-get update \ - && apt-get install -y --no-install-recommends gcc libffi-dev libc6-dev libpango-1.0-0 libpangocairo-1.0-0 shared-mime-info \ + && apt-get install -y --no-install-recommends gcc libffi-dev libc6-dev \ + && apt-get clean \ && rm -rf /var/lib/apt/lists/* WORKDIR /build @@ -25,6 +26,7 @@ COPY --from=builder /install /usr/local # WeasyPrint runtime dependencies RUN apt-get update \ && apt-get install -y --no-install-recommends libpango-1.0-0 libpangocairo-1.0-0 shared-mime-info \ + && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Copy application code diff --git a/desktop/README.md b/desktop/README.md index b6cfc66..4ecfce7 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -12,6 +12,7 @@ Application desktop native pour [ObsiGate](https://git.dracodev.net/Projets/Obsi - [Configuration des vaults](#configuration-des-vaults) - [Architecture](#architecture) - [Fonctionnalités natives](#fonctionnalites-natives) +- [Signature de code Windows](#signature-de-code-windows) - [Dépannage](#depannage) --- @@ -49,6 +50,11 @@ sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0 libayatana-appindicator3-1 :: Mêmes options, inclut le raccourci ``` +> **Binaires non signés** : les releases ne sont pas signées avec un certificat +> Windows. Au premier lancement, SmartScreen affiche « Windows a protégé votre +> PC » → cliquer **Informations complémentaires → Exécuter quand même**. +> Voir [Signature de code](#signature-de-code-windows) pour les alternatives. + ### macOS > Non supporté pour le moment (priorité Linux/Windows). @@ -260,6 +266,58 @@ Arrêt (tray → Quitter ou Ctrl+C) : --- +## Signature de code Windows + +La signature de code est **optionnelle** : sans elle, l'application fonctionne, +mais SmartScreen affiche un avertissement au premier lancement. Les binaires +ObsiGate sont actuellement distribués **non signés**. + +### Signer localement + +Le script `scripts/sign-windows.ps1` signe le binaire et les installeurs après un +`cargo tauri build`. Il lit les identifiants depuis l'environnement (jamais +commités) et est un **no-op explicite** si aucun certificat n'est fourni : + +```powershell +$env:OBSIGATE_SIGN_CERT_PFX = "C:\certs\obsigate.pfx" +$env:OBSIGATE_SIGN_CERT_PASSWORD = "..." +$env:OBSIGATE_SIGN_TIMESTAMP_URL = "http://timestamp.digicert.com" +.\scripts\sign-windows.ps1 +``` + +### Alternatives au certificat + +| Option | Coût indicatif | Effet SmartScreen | +|---|---|---| +| **Livrer non signé** (statu quo) | 0 € | Avertissement → « Exécuter quand même » | +| **SignPath.io** (projet open source) | Gratuit si éligible OSS | Réputation gérée par le service | +| **Certum Open Source Code Signing** | ~70-100 €/an | Réputation progressive | +| **Certificat OV** | ~150-400 €/an | Avertit tant que la réputation n'est pas établie | +| **Certificat EV** | ~300-700 €/an + token USB/HSM | Réputation **immédiate** | +| **Azure Trusted Signing** | ~10 $/mois | Bonne réputation, signature cloud | +| **Certificat auto-signé** | 0 € | Inutile en distribution publique | + +### Signature de l'auto-update (gratuite) + +Indépendante de la signature Windows, la signature des mises à jour Tauri repose +sur une paire de clés que vous générez vous-même : + +```bash +cargo tauri signer generate -w ~/.tauri/obsigate.key +``` + +- La **clé publique** est déjà renseignée dans `plugins.updater.pubkey` + (`tauri.conf.json`). +- La **clé privée** doit être exposée au build via `TAURI_SIGNING_PRIVATE_KEY` + (jamais commitée) ; dans le CI, via les secrets Gitea + `TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. +- Le CLI produit des `.sig` par artefact (`*.msi.sig`, `*.AppImage.sig`, …). + +> Le manifeste `latest.json` consommé par l'updater n'est pas généré par le CLI : +> voir [DEVELOPMENT_AND_RELEASES §2bis](../docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri). + +--- + ## Dépannage ### Le backend ne démarre pas diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 37cc874..8aeb738 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -78,6 +78,11 @@ struct AppConfig { window_y: Option, window_width: Option, window_height: Option, + /// True once the user has picked a vault or dismissed the first-run wizard. + /// `#[serde(default)]` keeps existing config files (written before this + /// field existed) parseable instead of falling back to a full reset. + #[serde(default)] + wizard_done: bool, } impl Default for AppConfig { @@ -90,6 +95,7 @@ impl Default for AppConfig { window_y: None, window_width: Some(1200.0), window_height: Some(800.0), + wizard_done: false, } } } @@ -286,6 +292,7 @@ fn get_config() -> AppConfig { fn save_vault_path(path: String) -> Result<(), String> { let mut config = load_config(); config.vault_path = Some(path); + config.wizard_done = true; save_config(&config); info!("Vault path saved: {}", config.vault_path.as_deref().unwrap_or("none")); Ok(()) @@ -305,6 +312,7 @@ async fn pick_vault_folder(app: tauri::AppHandle) -> Result { let p_str = p.to_string(); let mut config = load_config(); config.vault_path = Some(p_str.clone()); + config.wizard_done = true; save_config(&config); Ok(p_str) } @@ -312,6 +320,20 @@ async fn pick_vault_folder(app: tauri::AppHandle) -> Result { } } +#[tauri::command] +fn get_wizard_state() -> bool { + load_config().wizard_done +} + +#[tauri::command] +fn complete_wizard() -> Result<(), String> { + let mut config = load_config(); + config.wizard_done = true; + save_config(&config); + info!("First-run wizard marked as completed"); + Ok(()) +} + #[tauri::command] fn get_system_theme(app: tauri::AppHandle) -> String { use tauri::Theme; @@ -564,6 +586,8 @@ fn main() { get_config, save_vault_path, get_vault_path, + get_wizard_state, + complete_wizard, pick_vault_folder, get_system_theme, restart_backend, @@ -904,6 +928,7 @@ mod tests { assert!(c.dirs.is_empty()); assert_eq!(c.window_width, Some(1200.0)); assert_eq!(c.window_height, Some(800.0)); + assert!(!c.wizard_done); } #[test] @@ -919,6 +944,7 @@ mod tests { window_y: Some(200.0), window_width: Some(1400.0), window_height: Some(900.0), + wizard_done: true, }; let loaded = roundtrip_config(&c); assert_eq!(loaded.vault_path, Some("/test/vault".into())); @@ -927,6 +953,33 @@ mod tests { assert_eq!(loaded.vaults[1].path, "/v/work"); assert_eq!(loaded.dirs.len(), 1); assert_eq!(loaded.window_x, Some(100.0)); + assert!(loaded.wizard_done); + } + + #[test] + fn test_config_without_wizard_done_parses() { + // Backward compatibility: config files written before `wizard_done` + // existed must still load (instead of resetting to defaults). + let json = r#"{ + "vault_path": "/old/vault", + "vaults": [{"name": "Old", "path": "/old"}], + "dirs": [], + "window_x": null, + "window_y": null, + "window_width": 1000.0, + "window_height": 700.0 + }"#; + let c: AppConfig = serde_json::from_str(json).unwrap(); + assert_eq!(c.vaults.len(), 1); + assert_eq!(c.vaults[0].name, "Old"); + assert!(!c.wizard_done); + } + + #[test] + fn test_wizard_done_roundtrip() { + let mut c = AppConfig::default(); + c.wizard_done = true; + assert!(roundtrip_config(&c).wizard_done); } #[test] diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 4f58b3a..4c1c9ef 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -33,6 +33,7 @@ "bundle": { "active": true, "targets": "all", + "createUpdaterArtifacts": true, "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -83,7 +84,7 @@ "endpoints": [ "https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest" ], - "pubkey": "OBSIGATE_UPDATE_PUBKEY_PLACEHOLDER", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcwQjU2MDM4QUVEREY3NApSV1IwMysyS0ExWUxCK1RQMUgrYnplQWlpYWU2SHVYajVIUHhzNEFKLzZ2Z2puZW9pSUQ2RE8rUAo=", "windows": { "installMode": "passive" } diff --git a/docs/DESKTOP_E2E_CHECKLIST.md b/docs/DESKTOP_E2E_CHECKLIST.md new file mode 100644 index 0000000..08a2294 --- /dev/null +++ b/docs/DESKTOP_E2E_CHECKLIST.md @@ -0,0 +1,188 @@ +# ObsiGate Desktop — Protocole de tests E2E manuels + +> **Rôle :** valider les 6 scénarios de bout en bout du desktop Tauri (#77) qui ne +> peuvent pas être automatisés (interactions OS : installeur, tray, notifications, +> association de fichiers, auto-update, désinstallation). +> **Statut :** protocole documenté — à exécuter manuellement par un humain. +> **Références :** [feature desktop-tauri](./features/desktop-tauri.md) · +> [Roadmap](./ROADMAP.md) · [Build & releases](./DEVELOPMENT_AND_RELEASES.md) + +--- + +## 1. Prérequis + +- Un **build release** de l'application : + - Windows : `desktop\build-windows.bat` → `ObsiGate_x.y.z_x64.msi` / `-setup.exe` + - Linux : `desktop/build-linux.sh` → `obsigate_x.y.z_amd64.deb` / `.AppImage` +- Une **machine vierge ou un compte utilisateur propre** (pas d'installation + précédente) pour les tests d'installation/désinstallation. +- Le **mot de passe admin** affiché au premier lancement (ou `OBSIGATE_ADMIN_PASSWORD` + défini avant le lancement). +- Accès en écriture aux logs : `%APPDATA%\ObsiGate\logs\backend.log` (Windows) ou + `~/.config/obsigate/logs/backend.log` (Linux). + +## 2. Comment remplir ce protocole + +Pour chaque test : noter la **version testée** (`menu Aide → À propos`), le +**commit** du build, la **date**, puis cocher `✅ OK` ou `❌ Échec` et joindre les +logs/ captures en cas d'échec. Ne cocher un test qu'après avoir observé le +**résultat attendu**. + +| Test | Version | Commit | Date | Résultat | +|---|---|---|---|---| +| T1 — Installation | | | | ⬜ | +| T2 — Tray icon | | | | ⬜ | +| T3 — Notifications natives | | | | ⬜ | +| T4 — Association `.md` | | | | ⬜ | +| T5 — Auto-update | | | | ⬜ | +| T6 — Désinstallation | | | | ⬜ | + +--- + +## T1 — Installation → premier lancement → ouverture d'un fichier + +**Objectif :** l'installeur installe l'app et le premier lancement démarre le +backend + affiche l'interface. + +1. Lancer l'installeur (`.msi` ou `.deb`/`.AppImage`). +2. Installer dans le chemin par défaut → vérifier la création du raccourci + (bureau / menu Démarrer / menu applications). +3. Lancer ObsiGate depuis le raccourci. +4. Observer l'écran de démarrage (« ObsiGate démarre… ») puis l'interface. +5. Au premier lancement, cliquer **« Choisir mon dossier »** dans la bannière du + wizard et sélectionner un dossier vault. +6. Ouvrir un fichier `.md` depuis l'arborescence. + +**Résultat attendu :** +- Aucun terminal / console visible. +- Le backend répond (`http://127.0.0.1:17890/api/health` → 200) en ~2 s. +- Le fichier s'ouvre dans le viewer. +- La bannière du wizard **ne réapparaît pas** au lancement suivant. + +**Échec si :** écran blanc, backend non démarré, wizard qui revient à chaque +lancement. + +--- + +## T2 — Tray icon → réduire → restaurer + +**Objectif :** le tray fonctionne et contrôle la fenêtre. + +1. Vérifier la présence de l'icône ObsiGate dans la zone de notification. +2. Clic **gauche** sur l'icône → la fenêtre se cache. +3. Clic **gauche** à nouveau → la fenêtre réapparaît et prend le focus. +4. Clic **droit** → menu (Ouvrir ObsiGate / À propos / Quitter). +5. Fermer la fenêtre avec le **X** → elle se réduit dans le tray, le backend + continue de tourner (l'icône reste). + +**Résultat attendu :** toggle visible/caché immédiat, menu contextuel complet, +fermeture par X = réduction (pas d'arrêt du backend). + +--- + +## T3 — Notifications natives (fichier modifié → popup OS) + +**Objectif :** les notifications natives remplacent le push web. + +1. Activer les notifications dans les préférences (par vault si proposé). +2. Modifier un fichier surveillé (édition externe dans le vault, ou édition + in-app avec le watcher actif). +3. Observer la notification du système d'exploitation. + +**Résultat attendu :** popup OS affichant le nom du fichier/vault et l'action ; +un clic ouvre le fichier concerné. La notification apparaît même si la fenêtre +ObsiGate est réduite. + +> Note : sur Windows, vérifier que les notifications ne sont pas bloquées dans +> *Paramètres → Système → Notifications*. + +--- + +## T4 — Association `.md` → double-clic → ouvre dans ObsiGate + +**Objectif :** l'association de fichiers ouvre l'app. + +1. Vérifier que `.md` est associé à « ObsiGate Markdown » (Windows : + *Paramètres → Applications par défaut* ; Linux : `xdg-mime query default text/markdown`). +2. **Fermer** complètement ObsiGate (tray → Quitter). +3. Double-cliquer sur un fichier `.md` dans l'explorateur / gestionnaire de fichiers. +4. Observer le lancement d'ObsiGate et l'ouverture du fichier. + +**Résultat attendu :** ObsiGate démarre et ouvre le fichier (ou le vault +contenant le fichier). Un second double-clic alors que l'app tourne **focus la +fenêtre existante** (single-instance) au lieu de lancer un doublon. + +--- + +## T5 — Auto-update → nouvelle version → installation + +**Objectif :** l'updater détecte et installe une nouvelle version. + +1. S'assurer qu'une **release plus récente** existe sur Gitea (avec les artefacts + et le manifeste de mise à jour signé). +2. Lancer la version N. +3. Déclencher la vérification de mise à jour (menu ou au démarrage selon l'UI). +4. Accepter la mise à jour → l'app télécharge, vérifie la signature et installe. +5. Relancer → vérifier la version affichée (`À propos`). + +**Résultat attendu :** détection de la version N+1, téléchargement, installation +sans intervention manuelle, version mise à jour après redémarrage. + +**Prérequis bloquant :** la `pubkey` de l'updater dans `desktop/tauri.conf.json` +ne doit **pas** être le placeholder `OBSIGATE_UPDATE_PUBKEY_PLACEHOLDER`, et la +clé privée correspondante doit être fournie au build +(`TAURI_SIGNING_PRIVATE_KEY`). Voir §4. + +--- + +## T6 — Désinstallation propre + +**Objectif :** la désinstallation ne laisse aucun processus ni résidu gênant. + +1. Fermer ObsiGate (tray → Quitter) pour éviter un processus orphelin. +2. Désinstaller via le panneau de configuration (Windows) ou `dpkg -r obsigate` + / supprimer l'AppImage (Linux). +3. Vérifier qu'aucun processus `obsigate-desktop` / Python backend ne tourne + encore. +4. Vérifier les résidus : raccourcis supprimés, entrée « Applications par + défaut » retirée. +5. (Optionnel) Vérifier le comportement des données utilisateur + (`%APPDATA%\ObsiGate` / `~/.config/obsigate`) : conservées ou supprimées selon + le choix documenté. + +**Résultat attendu :** désinstallation sans erreur, aucun processus résiduel, +aucun raccourci cassé. + +--- + +## 3. Emplacements utiles + +| Élément | Windows | Linux | +|---|---|---| +| Config | `%APPDATA%\ObsiGate\config.json` | `~/.config/obsigate/config.json` | +| Logs backend | `%APPDATA%\ObsiGate\logs\backend.log` | `~/.config/obsigate/logs/backend.log` | +| Données (index, comptes) | `%APPDATA%\ObsiGate\data\` | `~/.config/obsigate/data\` | + +## 4. Signature de code & auto-update + +- **Signature Windows (optionnelle, hors périmètre de ce protocole) :** sans + certificat, SmartScreen affiche un avertissement au premier lancement + (*Informations complémentaires → Exécuter quand même*). Le script + `desktop/scripts/sign-windows.ps1` signe automatiquement si + `OBSIGATE_SIGN_CERT_PFX` est défini ; sinon il est un no-op explicite. + Alternatives détaillées dans le [README desktop](../desktop/README.md). +- **Signature de l'updater Tauri (gratuite, distincte de la signature Windows) :** + générer une paire de clés, renseigner la `pubkey` dans + `desktop/tauri.conf.json` et exposer la clé privée au build via + `TAURI_SIGNING_PRIVATE_KEY`. La clé privée **ne doit jamais être commitée**. + +```bash +cargo tauri signer generate -w ~/.tauri/obsigate.key +# → copier la clé publique affichée dans plugins.updater.pubkey +``` + +## 5. Clôture + +Une fois les 6 tests exécutés et OK, reporter le résultat dans +[`docs/features/desktop-tauri.md`](./features/desktop-tauri.md) (section F) et +mettre à jour le statut du #77 dans la [Roadmap](./ROADMAP.md). diff --git a/docs/DEVELOPMENT_AND_RELEASES.md b/docs/DEVELOPMENT_AND_RELEASES.md index aa0202f..687a8e5 100644 --- a/docs/DEVELOPMENT_AND_RELEASES.md +++ b/docs/DEVELOPMENT_AND_RELEASES.md @@ -57,6 +57,90 @@ cargo tauri dev --- +## 2bis. Signature des mises à jour (updater Tauri) + +La signature de l'auto-update Tauri est **indépendante** de la signature de code +Windows et **gratuite**. Elle garantit qu'une mise à jour téléchargée provient bien +de vous. Elle repose sur une paire de clés `minisign` : + +- La **clé publique** est embarquée dans `desktop/tauri.conf.json` + (`plugins.updater.pubkey`). +- La **clé privée** signe les artefacts au build. Elle ne doit **jamais** être + commitée (ignorée par `.gitignore`). + +### A. Générer la paire de clés (une seule fois) + +```bash +cd desktop +cargo tauri signer generate -w obsigate-updater.key +# La clé publique s'affiche et est écrite dans obsigate-updater.key.pub +``` + +> ⚠️ Conservez la clé privée en lieu sûr (gestionnaire de secrets). Si vous la +> perdez, les mises à jour ne pourront plus être signées. Pour la protéger par mot +> de passe : ajoutez `-p ""`. + +Copiez le contenu de `obsigate-updater.key.pub` dans +`desktop/tauri.conf.json` → `plugins.updater.pubkey`. + +### B. Build local signé + +```powershell +# Windows PowerShell +$env:TAURI_SIGNING_PRIVATE_KEY = Get-Content -Raw .\obsigate-updater.key +# $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "" # si la clé en a un +cargo tauri build --bundles nsis,msi +``` + +```bash +# Linux / Bash +export TAURI_SIGNING_PRIVATE_KEY="$(cat obsigate-updater.key)" +# export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" +cargo tauri build --bundles appimage,deb +``` + +Le CLI produit des fichiers `.sig` à côté de chaque artefact +(`*.exe.sig`, `*.msi.sig`, `*.AppImage.sig`, `*.deb.sig`). + +> Sans clé définie, `createUpdaterArtifacts` est actif et le build échoue : dans +> le CI, l'étape désactive automatiquement les artefacts de mise à jour si le +> secret est absent. + +### C. Secrets CI (Gitea) + +Dans **Dépôt → Paramètres → Actions → Secrets**, créez : + +| Secret | Valeur | +|---|---| +| `TAURI_SIGNING_PRIVATE_KEY` | contenu **intégral** du fichier `.key` | +| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | mot de passe de la clé (vide si aucun) | + +Le workflow `.gitea/workflows/desktop-build.yml` les expose aux étapes de build ; +les fichiers `.sig` sont uploadés comme artefacts. + +### D. Manifeste de mise à jour (`latest.json`) + +Le CLI Tauri génère les `.sig` mais **pas** le manifeste JSON consommé par +l'updater. L'endpoint configuré +(`.../releases/latest`) doit servir un document de la forme : + +```json +{ + "version": "2.3.0", + "notes": "…", + "pub_date": "2026-09-12T00:00:00Z", + "platforms": { + "windows-x86_64": { "signature": "", "url": "" }, + "linux-x86_64": { "signature": "", "url": "" } + } +} +``` + +Sans ce manifeste, l'updater ne détecte aucune mise à jour (la signature, elle, +est déjà opérationnelle). + +--- + ## 3. Commit, Push & Tagging Git Une fois les modifications testées et validées : diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 18a2964..a74ea29 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -33,12 +33,12 @@ ### 77. Application Desktop native — Tauri (Windows / Linux / macOS) - **Effort :** 8-12 jours | **Impact :** 🟡 | **Framework :** Tauri v2 (Rust + Webview) -- **Statut :** livré (A→F) — projet Tauri, backend Python embarqué, fonctionnalités natives, build CI, UX, 16 tests Rust. Détail complet : [features/desktop-tauri.md](./features/desktop-tauri.md) +- **Statut :** livré (A→F) — projet Tauri, backend Python embarqué, fonctionnalités natives, jumplist vaults, bannière 1er lancement, build CI, UX, 24 tests Rust. Détail complet : [features/desktop-tauri.md](./features/desktop-tauri.md) - **Reste à faire :** - - [ ] Signature de code Windows (optionnel mais recommandé) - - [ ] Wizard « Choisissez votre vault » au 1er lancement (optionnel) - - [ ] Jumplist vaults récents dans le menu Démarrer (optionnel) - - [ ] 6 tests E2E **manuels** : installation, tray icon, notifications natives, association `.md`, auto-update, désinstallation + - [x] **Signature de l'updater Tauri** (gratuit) : paire de clés générée, `pubkey` renseignée, `createUpdaterArtifacts` activé, secrets CI câblés + - [ ] Générer le manifeste `latest.json` pour que l'updater détecte les mises à jour + - [ ] **Signature de code Windows** : non retenue (pas de certificat) — alternatives : livrer non signé, SignPath.io (OSS gratuit), Certum OSS, Azure Trusted Signing, certificat EV + - [ ] Exécuter les 6 tests E2E **manuels** — protocole documenté : [DESKTOP_E2E_CHECKLIST.md](./DESKTOP_E2E_CHECKLIST.md) --- @@ -105,7 +105,7 @@ | Priorité | Items | Effort total estimé | |---|---|---| | ✅ Complété | #1 → #59, #61–72, #74–76, #78–80 | ~103 jours réalisés | -| 🔵 P2 restant | #77 Desktop : signature code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E **manuels** | ~1-2 jours | +| 🔵 P2 restant | #77 Desktop : manifeste `latest.json`, signature de code (non retenue), 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) | ~0,5-1 jour | | ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours | | **Total restant** | **2 items + finitions** | **~7-10 jours** | diff --git a/docs/features/desktop-tauri.md b/docs/features/desktop-tauri.md index a594f1f..20e71c1 100644 --- a/docs/features/desktop-tauri.md +++ b/docs/features/desktop-tauri.md @@ -1,6 +1,6 @@ # #77 — Application Desktop native — Tauri (Windows / Linux / macOS) -> **Statut :** 🔵 En cours — livré : A/B/C/D/E/F ; reste signature de code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E manuels +> **Statut :** 🔵 En cours — livré : A/B/C/D/E/F ; reste la signature de code Windows (optionnelle, non retenue) et l'exécution des 6 tests E2E **manuels** ([protocole](../DESKTOP_E2E_CHECKLIST.md)) > **Effort :** 8-12 jours | **Impact :** 🟡 | **Framework :** Tauri v2 (Rust + Webview) > **Références :** [Roadmap](../ROADMAP.md) · [Guide de build & releases](../DEVELOPMENT_AND_RELEASES.md) @@ -57,7 +57,8 @@ - [x] **Build Windows local** : `cargo build --release` vérifié (rustc 1.94.1) — `cargo tauri build --bundles msi` prêt - [x] **Build Linux local** : workflow CI couvre `.deb`, `.rpm`, `.AppImage` - [x] **Auto-update** : `tauri-plugin-updater` configuré → vérifie `https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest` - - [ ] **Signature de code** : configurer le certificat (optionnel mais recommandé pour Windows) + - [x] **Signature de l'updater Tauri** (gratuite, ≠ signature Windows) : paire de clés `minisign` générée, clé publique dans `plugins.updater.pubkey`, `bundle.createUpdaterArtifacts: true`, secrets Gitea `TAURI_SIGNING_PRIVATE_KEY` / `_PASSWORD` exposés au CI (build non signé en repli si le secret est absent). Reste : générer le manifeste `latest.json` pour que l'updater détecte les mises à jour ([guide](../DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri)) + - [ ] **Signature de code Windows** : non retenue (pas de certificat) — alternatives : livrer non signé, SignPath.io (OSS gratuit), Certum Open Source, Azure Trusted Signing, certificat EV - [x] **Page de release** : README desktop existe (`desktop/README.md`) ## E. Expérience utilisateur (1 jour) — ✅ COMPLÉTÉ @@ -65,19 +66,18 @@ - [x] Gestion des erreurs : backend crash → `showBackendCrashBanner()` appelé par le monitor loop toutes les 5s - [x] Sauvegarde des préférences desktop : position/taille fenêtre sauvées dans `%APPDATA%/ObsiGate/config.json` au close + restauration au startup - [x] Première expérience : config par défaut auto-créée au premier lancement (vault `~/voute_obsidian`, dir `~USERPROFILE`) - - [ ] Wizard interactif « Choisissez votre vault » au premier lancement (optionnel — config auto suffisante) - - [ ] Jumplist vaults récents dans le menu Démarrer (optionnel) + - [x] Bannière de premier lancement « Choisissez votre vault » (`frontend/js/desktop.js`) — non bloquante ; l'état est persisté côté Rust (`wizard_done` dans `config.json`) pour ne pas réapparaître après un choix ou un clic « Plus tard », même si le `localStorage` de la webview est vidé + - [x] Jumplist vaults récents dans le menu Démarrer (`desktop/src/jumplist.rs`, Windows) ## F. Tests (1 jour) — ✅ COMPLÉTÉ - - [x] 16 tests Rust unitaires : config roundtrip, JSON parsing (empty/partial/corrupted), vault dedup, dir remove, backend URL, paths, branding, edge cases + - [x] 24 tests Rust unitaires : config roundtrip, JSON parsing (empty/partial/corrupted/legacy), `wizard_done`, vault dedup, dir remove, backend URL, paths, branding, jumplist args, edge cases - [x] Build debug + release vérifié (rustc 1.94.1, tauri-cli 2.11.4) - [x] CI desktop workflow existant (desktop-build.yml) - - [ ] Test E2E : installation → premier lancement → wizard vault → ouverture fichier (manuel) - - [ ] Test E2E : tray icon → réduire → restaurer (manuel) - - [ ] Test E2E : notifications natives → fichier modifié → popup OS (manuel) - - [ ] Test E2E : association `.md` → double-clic → ouvre dans ObsiGate (manuel) - - [ ] Test E2E : auto-update → nouvelle version → install (manuel) - - [ ] Test E2E : désinstallation propre (manuel) + - [ ] **6 tests E2E manuels** — protocole détaillé (prérequis, étapes, résultat + attendu) dans [docs/DESKTOP_E2E_CHECKLIST.md](../DESKTOP_E2E_CHECKLIST.md) : + installation → 1er lancement → wizard → ouverture fichier ; tray ; notifications + natives ; association `.md` ; auto-update ; désinstallation propre. À exécuter + et cocher par un humain sur un build release. - **Prérequis techniques :** - Rust ≥ 1.75 (stable) — installé via `rustup` diff --git a/frontend/js/desktop.js b/frontend/js/desktop.js index 3743d79..4aa8918 100644 --- a/frontend/js/desktop.js +++ b/frontend/js/desktop.js @@ -142,8 +142,11 @@ export function shouldShowWizard(opts) { return isDesktop && !dismissed && !hasVaultPath; } -export function dismissWizard() { +export async function dismissWizard() { try { localStorage.setItem(WIZARD_DISMISSED_KEY, '1'); } catch (e) { /* ignore */ } + // Persist on the Rust side too, so clearing the webview's localStorage does + // not resurrect the wizard on the next launch. + await invoke('complete_wizard'); } export function getWizardDismissedKey() { return WIZARD_DISMISSED_KEY; } @@ -173,9 +176,13 @@ export async function pickAndAddVault() { // the user's Obsidian vault folder. Non-blocking; the default vault created // by the Rust shell remains available if the user dismisses it. -export function initDesktopWizard() { +export async function initDesktopWizard() { if (typeof window === 'undefined' || typeof document === 'undefined') return; - if (!shouldShowWizard({})) return; + + // The authoritative "already done" signal lives in the desktop config + // (survives localStorage clears). Defaults to false when not on desktop. + const wizardDone = (await invoke('get_wizard_state')) === true; + if (!shouldShowWizard({ hasVaultPath: wizardDone })) return; // Defer so the app shell is fully rendered before injecting the banner. setTimeout(function () { @@ -236,7 +243,7 @@ export async function initDesktopIntegration() { } catch (e) { /* ignore */ } // First-run vault wizard. - initDesktopWizard(); + await initDesktopWizard(); return true; } diff --git a/tests/frontend/desktop.test.mjs b/tests/frontend/desktop.test.mjs index 8225257..9474855 100644 --- a/tests/frontend/desktop.test.mjs +++ b/tests/frontend/desktop.test.mjs @@ -126,11 +126,30 @@ ok("shouldShowWizard: web → false", desktop.shouldShowWizard({ isDesktop: false, dismissed: false, hasVaultPath: false }) === false); // dismissWizard persists the flag -desktop.dismissWizard(); +localStorage.clear(); +await desktop.dismissWizard(); ok("dismissWizard persists flag", localStorage.getItem(desktop.getWizardDismissedKey()) === "1"); ok("shouldShowWizard reflects dismissed flag via localStorage", desktop.shouldShowWizard({ isDesktop: true, hasVaultPath: false }) === false); +// dismissWizard also persists on the Rust side (survives localStorage clear) +{ + let called = null; + window.__TAURI__ = { core: { invoke: async (cmd) => { called = cmd; return undefined; } } }; + localStorage.clear(); + await desktop.dismissWizard(); + ok("dismissWizard invokes complete_wizard", called === "complete_wizard"); +} + +// initDesktopWizard must NOT inject the banner when Rust reports wizard done +{ + localStorage.clear(); + window.__TAURI__ = { core: { invoke: async (cmd) => (cmd === "get_wizard_state" ? true : undefined) } }; + await desktop.initDesktopWizard(); + ok("initDesktopWizard skips banner when wizard_done is true", + document.getElementById("obsigate-desktop-wizard") === null); +} + // ── crash banner ──────────────────────────────────────────────────────────── console.log("crash banner");