New /reports/cartes page surfaces a dashboard-style snapshot of the reference month: - 4 KPI cards (income / expenses / net / savings rate) showing MoM and YoY deltas simultaneously, each with a 13-month sparkline highlighting the reference month - 12-month income vs expenses overlay chart (bars + net balance line) - Top 5 category increases + top 5 decreases MoM, clickable through to the category zoom report - Budget adherence card: on-target count + 3 worst overruns with progress bars - Seasonality card: reference month vs same calendar month averaged over the two previous years, with deviation indicator All data is fetched in a single getCartesSnapshot() service call that runs four queries concurrently (25-month flow, MoM category deltas, budget-vs-actual, seasonality). Missing months are filled with zeroes in the sparklines but preserved as null in the MoM/YoY deltas so the UI can distinguish "no data" from "zero spend". - Exported pure helpers: shiftMonth, defaultCartesReferencePeriod - 13 vitest cases covering zero data, MoM/YoY computation, January wrap-around, missing-month handling, division by zero for the savings rate, seasonality with and without history, top mover sign splitting and 5-cap Note: src/components/reports/CompareReferenceMonthPicker.tsx is a temporary duplicate — the canonical copy lives on the issue-96 branch (refactor: compare report). Once both branches merge the content is identical and git will dedupe. Keeping the local copy here means the Cartes branch builds cleanly on main without depending on #96. Closes #97 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
330 lines
19 KiB
Markdown
330 lines
19 KiB
Markdown
# Architecture technique — Simpl'Résultat
|
||
|
||
> Document mis à jour le 2026-04-13 — Version 0.7.3
|
||
|
||
## Stack technique
|
||
|
||
| Couche | Technologie | Version |
|
||
|--------|------------|---------|
|
||
| Framework desktop | Tauri | v2 |
|
||
| Frontend | React | 19.1 |
|
||
| Langage frontend | TypeScript | 5.8 |
|
||
| Bundler | Vite | 6.4 |
|
||
| CSS | Tailwind CSS | v4 |
|
||
| Backend | Rust (via Tauri) | stable |
|
||
| Base de données | SQLite (tauri-plugin-sql) | — |
|
||
| Graphiques | Recharts | 3.7 |
|
||
| Icônes | Lucide React | 0.563 |
|
||
| i18n | i18next + react-i18next | 25.8 / 16.5 |
|
||
| Drag & Drop | @dnd-kit | 6.3 / 10.0 |
|
||
| CSV | PapaParse | 5.5 |
|
||
| Chiffrement | aes-gcm (Rust) | 0.10 |
|
||
| Hachage PIN | Argon2 (Rust) | 0.5 |
|
||
|
||
## Structure du projet
|
||
|
||
```
|
||
simpl-resultat/
|
||
├── src/ # Frontend React/TypeScript
|
||
│ ├── components/ # 58 composants organisés par domaine
|
||
│ │ ├── adjustments/ # 3 composants
|
||
│ │ ├── budget/ # 5 composants
|
||
│ │ ├── categories/ # 5 composants
|
||
│ │ ├── dashboard/ # 2 composants
|
||
│ │ ├── import/ # 13 composants (wizard d'import)
|
||
│ │ ├── layout/ # AppShell, Sidebar
|
||
│ │ ├── profile/ # 3 composants (PIN, formulaire, switcher)
|
||
│ │ ├── reports/ # ~25 composants (hub, faits saillants, tendances, comparables, zoom catégorie)
|
||
│ │ ├── settings/ # 5 composants (+ LogViewerCard, LicenseCard, AccountCard)
|
||
│ │ ├── shared/ # 6 composants réutilisables
|
||
│ │ └── transactions/ # 5 composants
|
||
│ ├── contexts/ # ProfileContext (état global profil)
|
||
│ ├── hooks/ # 18+ hooks custom (useReducer, 5 hooks rapports par domaine)
|
||
│ ├── pages/ # 14 pages (dont 4 sous-pages rapports)
|
||
│ ├── services/ # 14 services métier
|
||
│ ├── shared/ # Types et constantes partagés
|
||
│ ├── utils/ # 4 utilitaires (parsing, CSV, charts)
|
||
│ ├── i18n/ # Config i18next + locales FR/EN
|
||
│ ├── App.tsx # Router principal
|
||
│ └── main.tsx # Point d'entrée
|
||
├── src-tauri/ # Backend Rust
|
||
│ ├── src/
|
||
│ │ ├── commands/ # 6 modules de commandes Tauri
|
||
│ │ │ ├── fs_commands.rs
|
||
│ │ │ ├── export_import_commands.rs
|
||
│ │ │ ├── profile_commands.rs
|
||
│ │ │ ├── license_commands.rs
|
||
│ │ │ ├── auth_commands.rs
|
||
│ │ │ └── entitlements.rs
|
||
│ │ ├── database/ # Schémas SQL et migrations
|
||
│ │ │ ├── schema.sql
|
||
│ │ │ ├── seed_categories.sql
|
||
│ │ │ └── consolidated_schema.sql
|
||
│ │ ├── lib.rs # Point d'entrée, migrations, plugins
|
||
│ │ └── main.rs
|
||
│ ├── capabilities/ # Permissions Tauri
|
||
│ └── Cargo.toml
|
||
├── .github/workflows/ # CI/CD
|
||
│ └── release.yml
|
||
├── docs/ # Documentation technique
|
||
└── config/ # Configuration
|
||
```
|
||
|
||
## Base de données
|
||
|
||
### Tables (13)
|
||
|
||
| Table | Description |
|
||
|-------|-------------|
|
||
| `import_sources` | Configuration des sources d'import CSV |
|
||
| `imported_files` | Suivi des fichiers importés (hash anti-doublons) |
|
||
| `categories` | Catégories hiérarchiques (dépenses/revenus) |
|
||
| `suppliers` | Fournisseurs avec auto-catégorisation |
|
||
| `keywords` | Mots-clés pour catégorisation automatique |
|
||
| `transactions` | Transactions individuelles |
|
||
| `adjustments` | Ajustements manuels (ponctuels ou récurrents) |
|
||
| `adjustment_entries` | Montants par catégorie pour chaque ajustement |
|
||
| `budget_entries` | Allocations budgétaires mensuelles par catégorie |
|
||
| `budget_templates` | Modèles de budget réutilisables |
|
||
| `budget_template_entries` | Catégories et montants dans les modèles |
|
||
| `import_config_templates` | Modèles prédéfinis de config d'import |
|
||
| `user_preferences` | Préférences applicatives (clé-valeur) |
|
||
|
||
### Index (9)
|
||
|
||
Index sur : `transactions` (date, category, supplier, source, file, parent), `categories` (parent, type), `suppliers` (category, normalized_name), `keywords` (category, keyword), `budget_entries` (year, month), `adjustment_entries` (adjustment_id), `imported_files` (source).
|
||
|
||
## Système de migrations
|
||
|
||
Les migrations sont définies inline dans `src-tauri/src/lib.rs` via `tauri_plugin_sql::Migration` :
|
||
|
||
| # | Version | Description |
|
||
|---|---------|-------------|
|
||
| 1 | v1 | Schéma initial (13 tables) |
|
||
| 2 | v2 | Seed des catégories et mots-clés |
|
||
| 3 | v3 | Ajout `has_header` sur `import_sources` |
|
||
| 4 | v4 | Ajout `is_inputable` sur `categories` |
|
||
| 5 | v5 | Création de `import_config_templates` |
|
||
| 6 | v6 | Changement contrainte unique `imported_files` (hash → filename) |
|
||
| 7 | v7 | Ajout sous-catégories d'assurance (niveau 3) |
|
||
|
||
Pour les **nouveaux profils**, le fichier `consolidated_schema.sql` contient le schéma complet avec toutes les migrations pré-appliquées (pas besoin de rejouer les migrations).
|
||
|
||
## Services TypeScript (17)
|
||
|
||
| Service | Responsabilité |
|
||
|---------|---------------|
|
||
| `db.ts` | Wrapper de connexion (tauri-plugin-sql) |
|
||
| `profileService.ts` | Gestion des profils |
|
||
| `categoryService.ts` | CRUD catégories hiérarchiques |
|
||
| `transactionService.ts` | CRUD et filtrage des transactions |
|
||
| `importSourceService.ts` | Configuration des sources d'import |
|
||
| `importedFileService.ts` | Suivi des fichiers importés |
|
||
| `importConfigTemplateService.ts` | Modèles de configuration d'import |
|
||
| `categorizationService.ts` | Catégorisation automatique + helpers édition de mot-clé (`validateKeyword`, `previewKeywordMatches`, `applyKeywordWithReassignment`) |
|
||
| `adjustmentService.ts` | Gestion des ajustements |
|
||
| `budgetService.ts` | Gestion budgétaire |
|
||
| `dashboardService.ts` | Agrégation données tableau de bord |
|
||
| `reportService.ts` | Génération de rapports : `getMonthlyTrends`, `getCategoryOverTime`, `getHighlights`, `getCompareMonthOverMonth`, `getCompareYearOverYear`, `getCategoryZoom` (CTE récursive bornée anti-cycle), `getCartesSnapshot` (snapshot dashboard Cartes, requêtes parallèles) |
|
||
| `dataExportService.ts` | Export de données (chiffré) |
|
||
| `userPreferenceService.ts` | Stockage préférences utilisateur |
|
||
| `logService.ts` | Capture des logs console (buffer circulaire, sessionStorage) |
|
||
| `licenseService.ts` | Validation et gestion de la clé de licence (appels commandes Tauri) |
|
||
| `authService.ts` | OAuth2 PKCE / Compte Maximus (appels commandes Tauri auth_*) |
|
||
|
||
## Hooks (14)
|
||
|
||
Chaque hook encapsule la logique d'état via `useReducer` :
|
||
|
||
| Hook | Domaine |
|
||
|------|---------|
|
||
| `useCategories` | Catégories avec hiérarchie |
|
||
| `useTransactions` | Transactions et filtrage |
|
||
| `useDataImport` | Import de données |
|
||
| `useImportWizard` | Assistant d'import multi-étapes |
|
||
| `useImportHistory` | Historique des imports |
|
||
| `useAdjustments` | Ajustements |
|
||
| `useBudget` | Budget |
|
||
| `useDashboard` | Métriques du tableau de bord |
|
||
| `useReportsPeriod` | Période de reporting synchronisée via query string (bookmarkable) |
|
||
| `useHighlights` | Panneau de faits saillants du hub rapports |
|
||
| `useTrends` | Rapport Tendances (sous-vue flux global / par catégorie) |
|
||
| `useCompare` | Rapport Comparables (mode `actual`/`budget`, sous-toggle MoM ↔ YoY, mois de référence explicite avec wrap-around janvier) |
|
||
| `useCategoryZoom` | Rapport Zoom catégorie avec rollup sous-catégories |
|
||
| `useCartes` | Rapport Cartes (snapshot KPI + sparklines + top movers + budget + saisonnalité via `getCartesSnapshot`) |
|
||
| `useDataExport` | Export de données |
|
||
| `useTheme` | Thème clair/sombre |
|
||
| `useUpdater` | Mise à jour de l'application (gated par entitlement licence) |
|
||
| `useLicense` | État de la licence et entitlements |
|
||
| `useAuth` | Authentification Compte Maximus (OAuth2 PKCE, subscription status) |
|
||
|
||
## Commandes Tauri (35)
|
||
|
||
### `fs_commands.rs` — Système de fichiers (6)
|
||
|
||
- `scan_import_folder` — Scan récursif de dossier pour fichiers CSV/TXT
|
||
- `read_file_content` — Lecture avec gestion de l'encodage
|
||
- `hash_file` — Hash SHA-256 (détection de doublons)
|
||
- `detect_encoding` — Détection auto (UTF-8, Windows-1252, ISO-8859-15)
|
||
- `get_file_preview` — Aperçu des N premières lignes
|
||
- `pick_folder` — Dialogue de sélection de dossier
|
||
|
||
### `export_import_commands.rs` — Export/Import de données (5)
|
||
|
||
- `pick_save_file` — Dialogue de sauvegarde
|
||
- `pick_import_file` — Dialogue de sélection de fichier
|
||
- `write_export_file` — Écriture fichier chiffré (format SREF)
|
||
- `read_import_file` — Lecture fichier chiffré
|
||
- `is_file_encrypted` — Vérification magic SREF
|
||
|
||
### `profile_commands.rs` — Gestion des profils (7)
|
||
|
||
- `load_profiles` — Chargement depuis `profiles.json`
|
||
- `save_profiles` — Sauvegarde de la configuration
|
||
- `delete_profile_db` — Suppression du fichier de base de données
|
||
- `get_new_profile_init_sql` — Récupération du schéma consolidé
|
||
- `hash_pin` — Hachage Argon2id du PIN (format `argon2id:salt:hash`)
|
||
- `verify_pin` — Vérification du PIN (supporte Argon2id et legacy SHA-256 pour rétrocompatibilité)
|
||
- `repair_migrations` — Réparation des checksums de migration (rusqlite)
|
||
|
||
### `license_commands.rs` — Licence et activation machine (10)
|
||
|
||
- `validate_license_key` — Validation offline d'une clé de licence (JWT Ed25519)
|
||
- `store_license` — Stockage de la clé dans le répertoire app data
|
||
- `store_activation_token` — Stockage du token d'activation
|
||
- `read_license` — Lecture de la licence stockée
|
||
- `get_edition` — Détection de l'édition active (free/base/premium)
|
||
- `get_machine_id` — Génération d'un identifiant machine unique
|
||
- `activate_machine` — Activation en ligne (appel API serveur de licences, issue #49)
|
||
- `deactivate_machine` — Désactivation d'une machine enregistrée
|
||
- `list_activated_machines` — Liste des machines activées pour la licence
|
||
- `get_activation_status` — État d'activation de la machine courante
|
||
|
||
### `auth_commands.rs` — Compte Maximus / OAuth2 PKCE (5)
|
||
|
||
- `start_oauth` — Génère un code verifier PKCE et retourne l'URL d'authentification Logto
|
||
- `refresh_auth_token` — Rafraîchit l'access token via le refresh token
|
||
- `get_account_info` — Lecture du cache d'affichage (via `account_cache::load_unverified`, accepte les payloads legacy)
|
||
- `check_subscription_status` — Vérifie l'abonnement (max 1×/jour, fallback cache gracieux). Déclenche aussi la migration `tokens.json` → keychain via `token_store::load`
|
||
- `logout` — Efface tokens (`token_store`) + cache signé (`account_cache`) + clé HMAC du keychain
|
||
|
||
Note : `handle_auth_callback` n'est PAS exposée comme commande — elle est appelée depuis le handler deep-link `on_open_url` dans `lib.rs`. Voir section "OAuth2 et deep-link" plus bas.
|
||
|
||
### `token_store.rs` — Stockage des tokens OAuth (1)
|
||
|
||
- `get_token_store_mode` — Retourne `"keychain"`, `"file"` ou `null`. Utilisé par la bannière de sécurité `TokenStoreFallbackBanner` dans Settings pour alerter l'utilisateur quand les tokens sont dans le fallback fichier.
|
||
|
||
Module non-command : `save`, `load`, `delete`, `store_mode` — toute la logique de persistance passe par ce module, `auth_commands.rs` ne touche jamais directement `tokens.json`. Voir l'ADR 0006 pour la conception complète.
|
||
|
||
### `account_cache.rs` — Cache d'abonnement signé (aucune commande)
|
||
|
||
Module privé appelé uniquement par `auth_commands.rs` et `license_commands.rs`. Expose :
|
||
- `save(app, &AccountInfo)` — écrit l'enveloppe signée `{data, sig}` dans `account.json`, avec clé HMAC-SHA256 stockée dans le keychain.
|
||
- `load_unverified(app)` — lecture pour affichage UI (accepte legacy et signé).
|
||
- `load_verified(app)` — lecture pour gating licence (refuse legacy, tampering, absence de clé). Utilisé par `license_commands::check_account_edition`.
|
||
- `delete(app)` — efface le fichier et la clé HMAC du keychain.
|
||
|
||
### `entitlements.rs` — Entitlements (1)
|
||
|
||
- `check_entitlement` — Vérifie si une feature est autorisée selon l'édition
|
||
- Source de vérité : `FEATURE_TIERS` dans `entitlements.rs`. Modifier cette constante pour changer les gates, jamais ailleurs dans le code
|
||
- Temporaire : `auto-update` est ouvert à `free` en attendant le serveur de licences (issue #49). À re-gater à `[base, premium]` quand l'activation payante sera live
|
||
|
||
## Plugins Tauri
|
||
|
||
Ordre d'initialisation dans `lib.rs` (certains plugins ont des contraintes d'ordre) :
|
||
|
||
| Plugin | Rôle | Contrainte |
|
||
|--------|------|-----------|
|
||
| `tauri-plugin-single-instance` | Empêche les doubles lancements et forwarde les URLs deep-link au processus existant | **Doit être le premier plugin** ; feature `deep-link` requise pour le forwarding d'URL |
|
||
| `tauri-plugin-opener` | Ouverture d'URLs externes et de fichiers | — |
|
||
| `tauri-plugin-dialog` | Dialogues de sélection de fichier/dossier | — |
|
||
| `tauri-plugin-process` | Relaunch après mise à jour | — |
|
||
| `tauri-plugin-deep-link` | Gère le scheme custom `simpl-resultat://` | Doit être initialisé avant `setup()` pour que `on_open_url` soit disponible |
|
||
| `tauri-plugin-updater` | Mise à jour auto (gated par entitlement `auto-update`) | Initialisé dans `setup()` derrière `#[cfg(desktop)]` |
|
||
| `tauri-plugin-sql` | SQLite + migrations | Doit être initialisé avec les migrations pour que le schéma soit prêt |
|
||
|
||
## OAuth2 et deep-link (Compte Maximus)
|
||
|
||
Flow complet (v0.7.3+) :
|
||
|
||
1. Frontend appelle `start_oauth` → génère un code verifier PKCE (64 chars), le stocke dans `OAuthState` (Mutex en mémoire du processus), retourne l'URL Logto
|
||
2. Frontend ouvre l'URL via `tauri-plugin-opener` → le navigateur système affiche la page Logto
|
||
3. L'utilisateur s'authentifie (ou Logto auto-consent si session existante) → redirection 303 vers `simpl-resultat://auth/callback?code=...`
|
||
4. L'OS route le custom scheme vers une nouvelle instance de l'app → `tauri-plugin-single-instance` (feature `deep-link`) détecte l'instance existante, **ne démarre PAS un nouveau processus**, et forwarde l'URL à l'instance vivante
|
||
5. Le callback `app.deep_link().on_open_url(...)` enregistré via `DeepLinkExt` reçoit les URLs. Pour chaque URL :
|
||
- Si un param `code` est présent → appelle `handle_auth_callback` (token exchange vers `/oidc/token`, fetch `/oidc/me`, écriture des tokens via `token_store::save` (keychain OS, fallback fichier 0600) + cache signé via `account_cache::save` (HMAC-SHA256), émission de l'event `auth-callback-success`)
|
||
- Si un param `error` est présent → émission de l'event `auth-callback-error` avec `error: error_description`
|
||
6. Le hook `useAuth` (frontend) écoute `auth-callback-success` / `auth-callback-error` et met à jour l'état
|
||
|
||
Pourquoi cet enchaînement est critique :
|
||
- **Sans `tauri-plugin-single-instance`** : une nouvelle instance démarre à chaque callback, le `OAuthState` est vide (pas de verifier), le token exchange échoue
|
||
- **Sans `on_open_url`** : l'ancien listener `app.listen("deep-link://new-url", ...)` ne recevait pas les URLs forwardées par single-instance. L'API canonique v2 via `DeepLinkExt` est nécessaire
|
||
- **Sans gestion des erreurs** : un callback `?error=...` laissait l'UI bloquée en état "loading" infini
|
||
|
||
Fichiers : `src-tauri/src/lib.rs` (wiring), `src-tauri/src/commands/auth_commands.rs` (PKCE + token exchange), `src-tauri/src/commands/token_store.rs` (persistance keychain + fallback), `src-tauri/src/commands/account_cache.rs` (cache signé HMAC), `src/hooks/useAuth.ts` (frontend), `src/components/settings/TokenStoreFallbackBanner.tsx` (UI de l'état dégradé).
|
||
|
||
## Pages et routing
|
||
|
||
Le routing est défini dans `App.tsx`. Toutes les pages sont englobées par `AppShell` (sidebar + layout). L'accès est contrôlé par `ProfileContext` (gate).
|
||
|
||
### Gestion d'erreurs
|
||
|
||
- **`ErrorBoundary`** (class component) : wrape `<App />` dans `main.tsx`, attrape les crashs React et affiche `ErrorPage` en fallback
|
||
- **`ErrorPage`** : page d'erreur réutilisable avec détails techniques (collapsible), bouton "Actualiser", vérification de mises à jour, et liens de contact/issues
|
||
- **Timeout au démarrage** : `App.tsx` applique un timeout de 10 secondes sur `connectActiveProfile()` — affiche `ErrorPage` au lieu d'un spinner infini si la connexion DB échoue
|
||
- **Retry au démarrage** : `connectActiveProfile()` réessaie jusqu'à 3 fois avec 1s de délai avant d'afficher l'erreur
|
||
- **Réparation de migrations** : `repair_migrations` (Rust/rusqlite) supprime les checksums invalides de `_sqlx_migrations` avant le chargement de la DB
|
||
- **Log viewer** : `logService.ts` capture les `console.log/warn/error` dans un buffer circulaire (500 entrées, persisté en `sessionStorage`), affiché dans la page Paramètres via `LogViewerCard`
|
||
|
||
| Route | Page | Description |
|
||
|-------|------|-------------|
|
||
| `/` | `DashboardPage` | Tableau de bord (résumé, pie chart, budget vs réel, dépenses dans le temps) |
|
||
| `/import` | `ImportPage` | Assistant d'import CSV |
|
||
| `/transactions` | `TransactionsPage` | Liste avec filtres |
|
||
| `/categories` | `CategoriesPage` | Gestion hiérarchique |
|
||
| `/adjustments` | `AdjustmentsPage` | Ajustements manuels |
|
||
| `/budget` | `BudgetPage` | Planification budgétaire |
|
||
| `/reports` | `ReportsPage` | Hub des rapports : panneau faits saillants + 4 cartes de navigation |
|
||
| `/reports/highlights` | `ReportsHighlightsPage` | Faits saillants détaillés (soldes, top mouvements, top transactions) |
|
||
| `/reports/trends` | `ReportsTrendsPage` | Tendances (flux global + par catégorie) |
|
||
| `/reports/compare` | `ReportsComparePage` | Comparables (MoM / YoY / Réel vs budget) |
|
||
| `/reports/category` | `ReportsCategoryPage` | Zoom catégorie avec rollup + édition contextuelle de mots-clés |
|
||
| `/reports/cartes` | `ReportsCartesPage` | Tableau de bord KPI avec sparklines, top movers, budget et saisonnalité |
|
||
| `/settings` | `SettingsPage` | Paramètres |
|
||
| `/docs` | `DocsPage` | Documentation in-app |
|
||
| `/changelog` | `ChangelogPage` | Historique des versions (bilingue FR/EN) |
|
||
|
||
Page spéciale : `ProfileSelectionPage` (affichée quand aucun profil n'est actif).
|
||
|
||
## Internationalisation
|
||
|
||
- **Librairie** : i18next + react-i18next
|
||
- **Langue par défaut** : Français (`fr`)
|
||
- **Langue de fallback** : Anglais (`en`)
|
||
- **Fichiers** : `src/i18n/locales/fr.json`, `src/i18n/locales/en.json`
|
||
- **Clés organisées** hiérarchiquement par domaine (`nav.*`, `dashboard.*`, `import.*`, etc.)
|
||
|
||
## CI/CD
|
||
|
||
Deux workflows Forgejo Actions (avec miroir GitHub) dans `.forgejo/workflows/` :
|
||
|
||
### `check.yml` — Vérifications sur branches et PR
|
||
|
||
Déclenché sur chaque push de branche (sauf `main`) et chaque PR vers `main`. Lance en parallèle :
|
||
- `cargo check` + `cargo test` (Rust)
|
||
- `npm run build` (tsc + vite)
|
||
- `npm test` (vitest)
|
||
|
||
Doit être vert avant tout merge. Évite de découvrir des régressions au moment du tag de release.
|
||
|
||
### `release.yml` — Build et publication
|
||
|
||
Déclenché par les tags `v*`. Deux jobs :
|
||
1. **build-windows** (windows-latest) → Installeur `.exe` (NSIS)
|
||
2. **build-linux** (ubuntu-22.04) → `.deb` + `.rpm`
|
||
|
||
Fonctionnalités :
|
||
- Signature des binaires (clés TAURI_SIGNING_PRIVATE_KEY)
|
||
- JSON d'updater publié sur `https://git.lacompagniemaximus.com/api/packages/maximus/generic/simpl-resultat/latest/latest.json`
|
||
- Release Forgejo automatique avec assets et release notes extraites du CHANGELOG.md
|