# Migration electron-updater + Gitea — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Remplacer le système de mise à jour custom (download HTTP + script batch) par `electron-updater`, en gardant Gitea comme source de releases.

**Architecture:** Approche hybride — appel API Gitea pour découvrir la dernière version (car Gitea n'a pas d'URL fixe "latest"), puis `electron-updater` (generic provider) pour le cycle download → vérification sha512 → installation NSIS → restart. Les IPC channels restent identiques pour que `UpdateBanner.ts` et `preload.ts` n'aient aucun changement.

**Tech Stack:** electron-updater 6.x, Electron 32.x, TypeScript

---

### Task 1: Installer electron-updater et configurer package.json

**Files:**
- Modify: `package.json`

**Step 1: Installer la dépendance**

```bash
npm install electron-updater
```

> `electron-updater` doit être dans `dependencies` (pas `devDependencies`) car il tourne dans l'app packagée.

**Step 2: Ajouter la config `publish` dans la section `build`**

Dans `package.json`, ajouter dans `"build"` :

```json
"publish": {
  "provider": "generic",
  "url": "https://gitea.mickael-pol.fr/mickael/dd-timer/releases/download/latest"
}
```

> Cette URL est un placeholder — au runtime, on la remplace dynamiquement via `setFeedURL()`.
> Son seul rôle est de déclencher la génération de `latest.yml` par electron-builder.

**Step 3: Commit**

```bash
git add package.json package-lock.json
git commit -m "chore: add electron-updater dependency + publish config"
```

---

### Task 2: Réécrire la section mise à jour dans main.ts

**Files:**
- Modify: `src/infrastructure/electron/main.ts:1-16` (imports)
- Modify: `src/infrastructure/electron/main.ts:37-48` (UpdateInfo + state vars)
- Delete: `src/infrastructure/electron/main.ts:254-450` (tout le bloc update custom)
- Add: nouveau bloc update avec electron-updater (~60 lignes)

**Step 1: Ajouter l'import electron-updater, supprimer les imports inutiles**

Remplacer les imports en haut du fichier :

```typescript
import {
  app,
  BrowserWindow,
  Tray,
  Menu,
  nativeImage,
  ipcMain,
  Notification,
  dialog,
} from 'electron';
import path from 'path';
import fs from 'fs';
import { autoUpdater } from 'electron-updater';
```

> Supprimer : `import https from 'https'`, `import http from 'http'`, `import os from 'os'`, `import { spawn } from 'child_process'` — ils ne sont plus nécessaires pour les mises à jour.
>
> **Attention** : `https` et `http` sont encore utilisés par le bloc ntfy (lignes 214-243). Vérifier si d'autres usages existent avant de supprimer. Si ntfy les utilise → garder `https` et `http`. Supprimer seulement `os` et `spawn` si plus aucun usage.

**Step 2: Simplifier les variables d'état**

Remplacer l'interface `UpdateInfo` et les variables `updateInfo` / `downloading` :

```typescript
interface UpdateInfo {
  version: string;
}

let updateInfo: UpdateInfo | null = null;
```

> On supprime `downloadUrl`, `assetName`, `releaseNotes` — electron-updater gère tout ça.
> On supprime `downloading` — electron-updater gère l'état.

**Step 3: Supprimer tout le bloc update custom**

Supprimer entièrement (lignes 254-450) :
- `compareVersions()`
- Interfaces `GiteaAsset`, `GiteaRelease`
- `checkForUpdates()`
- `startDownload()`
- `sendUpdateError()`
- `launchUpdater()`

**Step 4: Écrire le nouveau système de mise à jour**

Ajouter à la place du code supprimé :

```typescript
// ─── COMPARAISON DE VERSIONS ────────────────────────────────────────────────
function compareVersions(a: string, b: string): number {
  const pa = a.replace(/^v/, '').split('.').map(Number);
  const pb = b.replace(/^v/, '').split('.').map(Number);
  for (let i = 0; i < 3; i++) {
    if ((pb[i] || 0) > (pa[i] || 0)) return 1;
    if ((pb[i] || 0) < (pa[i] || 0)) return -1;
  }
  return 0;
}

// ─── ELECTRON-UPDATER : CONFIGURATION ───────────────────────────────────────
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = false;

// ─── ELECTRON-UPDATER : EVENTS ──────────────────────────────────────────────
autoUpdater.on('update-available', (info) => {
  updateInfo = { version: info.version };
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('update-available', updateInfo);
  }
  fireNotification(
    `Mise a jour v${info.version} disponible !`,
    'Téléchargement en cours...'
  );
  rebuildTrayMenu();
});

autoUpdater.on('download-progress', (progress) => {
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('update-progress', { percent: Math.round(progress.percent) });
  }
});

autoUpdater.on('update-downloaded', () => {
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('update-ready', updateInfo ?? {});
  }
});

autoUpdater.on('error', (err) => {
  console.error('Update error:', err.message);
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('update-error', { message: err.message });
  }
});

// ─── VÉRIFICATION VIA API GITEA ─────────────────────────────────────────────
function checkForUpdates(silent = false): void {
  const https = require('https') as typeof import('https');
  const options: import('https').RequestOptions = {
    hostname: GITEA_HOST,
    port: 443,
    path: `/api/v1/repos/${GITEA_USER}/${GITEA_REPO}/releases?limit=1`,
    method: 'GET',
    headers: {
      'User-Agent': `MinuteurDragodinde/${CURRENT_VERSION}`,
      'Accept': 'application/json',
    },
  };

  const req = https.request(options, (res) => {
    let data = '';
    res.on('data', (chunk: string) => { data += chunk; });
    res.on('end', () => {
      try {
        const releases = JSON.parse(data);
        if (!Array.isArray(releases) || releases.length === 0) return;
        const release = releases[0];
        const latestVersion = release.tag_name;
        if (!latestVersion || compareVersions(CURRENT_VERSION, latestVersion) <= 0) {
          if (!silent && mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.webContents.send('update-not-available');
          }
          return;
        }

        // Pointer electron-updater vers le tag de la release
        const tag = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
        autoUpdater.setFeedURL({
          provider: 'generic',
          url: `https://${GITEA_HOST}/${GITEA_USER}/${GITEA_REPO}/releases/download/${tag}`,
        });

        // electron-updater prend le relais : lit latest.yml, télécharge, vérifie
        autoUpdater.checkForUpdates();
      } catch (e: unknown) {
        console.error('Update check parse error:', (e as Error).message);
      }
    });
  });

  req.on('error', (e: Error) => console.error('Update check error:', e.message));
  req.end();
}
```

**Step 5: Mettre à jour le handler `install-update`**

Remplacer :
```typescript
ipcMain.on('install-update', () => startDownload());
```

Par :
```typescript
ipcMain.on('install-update', () => {
  autoUpdater.quitAndInstall(true, true);
});
```

> `quitAndInstall(isSilent, isForceRunAfter)` : installe en silencieux et relance l'app.

**Step 6: Mettre à jour le tray menu**

Dans `rebuildTrayMenu()`, remplacer le click du menu update :

```typescript
if (updateInfo) {
  items.push({ type: 'separator' });
  items.push({
    label: `⬆ Mise a jour v${updateInfo.version} disponible !`,
    click: () => autoUpdater.quitAndInstall(true, true),
  });
}
```

**Step 7: Simplifier le bloc did-finish-load**

Le bloc `did-finish-load` (ligne 115-133) reste identique — il envoie déjà `update-available` si `updateInfo` existe.

**Step 8: Commit**

```bash
git add src/infrastructure/electron/main.ts
git commit -m "feat: migrate update system to electron-updater"
```

---

### Task 3: Vérifier que preload.ts et UpdateBanner.ts n'ont pas besoin de changement

**Files:**
- Read: `src/infrastructure/electron/preload.ts`
- Read: `src/presentation/components/UpdateBanner.ts`

**Step 1: Vérifier les IPC channels**

Les channels IPC n'ont pas changé :
- `update-available` → `{ version }` ✓
- `update-downloading` → **ATTENTION** : l'ancien code envoyait `update-downloading`, mais electron-updater n'a pas cet event. Il passe directement de `update-available` à `download-progress`. Le banner state `downloading` est activé par `onUpdateDownloading` OU `onUpdateProgress`. Vérifier que `UpdateBanner` gère bien la transition.

**Step 2: Envoyer `update-downloading` explicitement**

Dans les events autoUpdater, ajouter après `update-available` :

```typescript
autoUpdater.on('update-available', (info) => {
  updateInfo = { version: info.version };
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('update-available', updateInfo);
    // Envoyer aussi downloading car autoDownload = true
    mainWindow.webContents.send('update-downloading', { version: info.version });
  }
  fireNotification(
    `Mise a jour v${info.version} disponible !`,
    'Téléchargement en cours...'
  );
  rebuildTrayMenu();
});
```

> Cela garantit que `UpdateBanner` passe bien en état `downloading` comme avant.

**Step 3: Commit (si changement)**

```bash
git add src/infrastructure/electron/main.ts
git commit -m "fix: send update-downloading event for banner compatibility"
```

---

### Task 4: Vérifier la génération de latest.yml

**Step 1: Lancer un build**

```bash
npm run build
```

**Step 2: Vérifier que `dist/latest.yml` existe**

```bash
ls dist/latest.yml
cat dist/latest.yml
```

Contenu attendu (exemple) :
```yaml
version: 1.1.5
files:
  - url: Minuteur-Dragodinde-Setup-1.1.5.exe
    sha512: <hash>
    size: <size>
path: Minuteur-Dragodinde-Setup-1.1.5.exe
sha512: <hash>
releaseDate: '2026-04-04T...'
```

**Step 3: Vérifier que le .exe est aussi généré**

```bash
ls dist/Minuteur-Dragodinde-Setup-*.exe
```

---

### Task 5: Mettre à jour le CHANGELOG

**Files:**
- Modify: `CHANGELOG.md`

**Step 1: Ajouter l'entrée**

Ajouter sous `## v1.1.6` (ou créer `## v1.1.7` si nouvelle version) :

```markdown
### Mise à jour automatique

- **Migration electron-updater** : remplacement du système custom (download HTTP + script batch) par `electron-updater` (generic provider)
  - Vérification sha512 automatique des mises à jour
  - Installation NSIS native (plus de script batch hack)
  - Restart automatique après installation
  - Code simplifié (~40 lignes vs ~200)
  - Compatible Gitea : découverte via API + `latest.yml` uploadé en asset de release
```

**Step 2: Commit**

```bash
git add CHANGELOG.md
git commit -m "docs: add electron-updater migration to CHANGELOG"
```

---

### Task 6: Mettre à jour le workflow de release

**Step 1: Documenter le nouveau process**

Le nouveau workflow de release est :

1. Mettre à jour `version` dans `package.json`
2. `npm run build`
3. Deux fichiers sont générés dans `dist/` :
   - `Minuteur-Dragodinde-Setup-X.Y.Z.exe`
   - `latest.yml`
4. Commit + tag + push :
   ```bash
   git add -A && git commit -m "vX.Y.Z"
   git tag vX.Y.Z && git push && git push --tags
   ```
5. Sur Gitea → Releases → Créer release avec le tag
6. **Uploader les 2 fichiers** : le `.exe` ET `latest.yml`

> **IMPORTANT** : `latest.yml` DOIT être uploadé à chaque release, sinon electron-updater ne pourra pas vérifier l'intégrité du fichier.

---

## Récapitulatif des fichiers modifiés

| Fichier | Action |
|---------|--------|
| `package.json` | Ajout dep `electron-updater` + config `publish` |
| `package-lock.json` | Mis à jour automatiquement |
| `src/infrastructure/electron/main.ts` | Réécriture section update (~200 lignes → ~60 lignes) |
| `src/infrastructure/electron/preload.ts` | **Aucun changement** |
| `src/presentation/components/UpdateBanner.ts` | **Aucun changement** |
| `CHANGELOG.md` | Entrée migration |
