feat(desktop): signature updater Tauri + wizard persistant + protocole E2E (#77)
CI / lint (push) Successful in 1m11s
CI / security (push) Successful in 43s
CI / test (push) Successful in 2m22s
CI / build (push) Successful in 1m41s
CI / e2e (push) Successful in 10m50s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

- Signature des mises a jour Tauri : paire de cles minisign generee, cle publique dans tauri.conf.json, createUpdaterArtifacts actif, secrets CI exposes (repli build non signe si secret absent).

- Wizard 1er lancement : etat persistant wizard_done cote Rust (get_wizard_state/complete_wizard) pour ne plus reafficher la banniere, retro-compatible.

- Protocole des 6 tests E2E manuels : docs/DESKTOP_E2E_CHECKLIST.md.

- CI : desktop.test.mjs ajoute au job lint.

- Docs : ROADMAP, fiche desktop-tauri, CHANGELOG, README desktop, guide releases.

- fix(build): retirer les libs WeasyPrint inutiles du stage builder Docker.
This commit is contained in:
2026-09-12 09:29:47 -04:00
parent 31d4be8015
commit d3299166bb
14 changed files with 519 additions and 29 deletions
+3 -1
View File
@@ -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 ─────────────────────────────────────────────────────────
+40 -4
View File
@@ -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
+4
View File
@@ -31,3 +31,7 @@ desktop/backend/
desktop/frontend/
backend/VERSION
# Tauri updater signing keys (private key — never commit)
desktop/*.key
desktop/*.key.pub
+36
View File
@@ -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
+3 -1
View File
@@ -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
+58
View File
@@ -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
+53
View File
@@ -78,6 +78,11 @@ struct AppConfig {
window_y: Option<f64>,
window_width: Option<f64>,
window_height: Option<f64>,
/// 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<String, String> {
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<String, String> {
}
}
#[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]
+2 -1
View File
@@ -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"
}
+188
View File
@@ -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).
+84
View File
@@ -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 "<mot de passe>"`.
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 = "<mot de passe>" # 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="<mot de passe>"
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": "<contenu .msi.sig>", "url": "<URL du .msi>" },
"linux-x86_64": { "signature": "<contenu .AppImage.sig>", "url": "<URL .AppImage>" }
}
}
```
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 :
+6 -6
View File
@@ -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** |
+11 -11
View File
@@ -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`
+11 -4
View File
@@ -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;
}
+20 -1
View File
@@ -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");