From b399c7c975c7950a858abc047f84f90425d0a3fb Mon Sep 17 00:00:00 2001 From: Anthony Mittica Date: Wed, 29 Oct 2025 12:09:21 +0100 Subject: [PATCH 1/4] =?UTF-8?q?docs:=20purge=20anciens=20fichiers=20et=20a?= =?UTF-8?q?jouter=20Guide=5Ftechnique=5FReadRSS.md=20(30=20sections,=20ext?= =?UTF-8?q?raits=20de=20code,=20pr=C3=AAt=20pour=20pr=C3=A9sentation)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide_technique_ReadRSS.md | 419 ++++++++++++++++++++++++++++++++ docs/RECAP_AMELIORATIONS.md | 183 -------------- docs/packaging.md | 18 -- docs/plan_travail.md | 49 ---- docs/qa_checklist.md | 12 - docs/recap_2025-10-28.md | 42 ---- docs/recapitulatif_actions.md | 47 ---- 7 files changed, 419 insertions(+), 351 deletions(-) create mode 100644 docs/Guide_technique_ReadRSS.md delete mode 100644 docs/RECAP_AMELIORATIONS.md delete mode 100644 docs/packaging.md delete mode 100644 docs/plan_travail.md delete mode 100644 docs/qa_checklist.md delete mode 100644 docs/recap_2025-10-28.md delete mode 100644 docs/recapitulatif_actions.md diff --git a/docs/Guide_technique_ReadRSS.md b/docs/Guide_technique_ReadRSS.md new file mode 100644 index 0000000..2cdae95 --- /dev/null +++ b/docs/Guide_technique_ReadRSS.md @@ -0,0 +1,419 @@ +# ReadRSS — Guide technique complet (≈ 30 minutes) + +Ce document sert de support d’oral (1 section ≈ 1 minute) et de référence technique exhaustive pour ReadRSS. Chaque section est concise, progressive et illustrée de code du projet. + +--- + +## 01 — Vision et objectifs + +ReadRSS est un lecteur RSS/Atom local, rapide et simple: +- Polling en arrière‑plan avec limites, timeouts, retries. +- UI fluide (egui/wgpu), sans WebView: ouverture des liens dans le navigateur système. +- Données et configuration persistées côté utilisateur, par OS. +- Sécurité: HTTPS obligatoire en production (loopback autorisé en dev/tests). + +Contrats d’expérience: +- Démarre vite, ne bloque jamais l’UI sur les E/S. +- Lire hors‑ligne ce qui a déjà été récupéré. +- Paramètres sauvegardés immédiatement. + +--- + +## 02 — Architecture d’ensemble + +Workspace Cargo: +- `rss-core` (bibliothèque): modèles, parsing, polling, persistance, erreurs, seen store. +- `rss-gui` (application): eframe/egui, navigation, thèmes, intégration `rss-core`. + +Flux de données (simplifié): + +``` +HTTP (reqwest) → parse (rss/atom) → FeedEntry → SeenStore (dédup) → DataApi (persist) + ↓ + UI (egui) +``` + +--- + +## 03 — Modules principaux (core) + +- `config`: AppConfig, gestion du fichier `config.json` (chargement/écriture). +- `poller`: tâche périodique, timeouts, retries, 10 MiB max, HTTPS only. +- `feed`: modèles `FeedDescriptor`, `FeedEntry` et conversions RSS/Atom. +- `data`: API de données (feeds, read-state, cache d’articles) avec persistance atomique .tmp. +- `storage`: `SeenStore` (déduplication persistée). +- `error`: `PollError` centralise les erreurs. + +Exposition publique (`rss-core/src/lib.rs`): + +```rust +pub mod config; pub mod data; pub mod error; pub mod feed; pub mod poller; pub mod storage; +pub use config::{AppConfig, FeedConfig, ThemeConfig, UiConfig}; +pub use data::DataApi; +pub use error::PollError; +pub use feed::{FeedDescriptor, FeedEntry, SharedFeedList, add_feed, list_feeds, remove_feed, shared_feed_list}; +pub use poller::{poll_once, spawn_poller, Event, PollConfig, PollerHandle}; +pub use storage::SeenStore; +``` + +--- + +## 04 — Configuration: AppConfig + +Rôle: centraliser thème, UI et paramètres de polling côté utilisateur. + +```rust +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AppConfig { pub theme: ThemeConfig, pub feeds: FeedConfig, pub ui: UiConfig } + +impl AppConfig { + pub fn config_file_path() -> Result> { /* … */ } + pub fn load() -> Self { /* défaut + save si absent */ } + pub fn save(&self) -> Result<(), Box> { /* … */ } +} +``` + +Design: +- Toujours chargeable (fallback défaut + auto‑save en cas d’erreur). +- Neutralité UI: pas de dépendance forte à egui, seulement des couleurs `[u8;3]`. + +--- + +## 05 — Modèle de données: FeedDescriptor et FeedEntry + +```rust +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct FeedDescriptor { pub id: String, pub title: String, pub url: String } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FeedEntry { + pub feed_id: String, pub title: String, pub summary: Option, pub url: String, + pub published_at: Option>, pub guid: Option, /* … */ +} + +impl FeedEntry { + pub fn identity(&self) -> String { /* GUID > URL > titre@ts */ } +} +``` + +Points clés: +- `identity()` assure une déduplication robuste. +- Conversions depuis `rss::Item` et `atom::Entry` enrichissent les champs (auteur, catégorie…). + +--- + +## 06 — Parsing RSS puis fallback Atom + +`fetch_feed` lit en streaming, limite à 10 MiB, parse RSS sinon tente Atom: + +```rust +let mut cursor_rss = std::io::Cursor::new(bytes.to_vec()); +match rss::Channel::read_from(&mut cursor_rss) { + Ok(channel) => { /* map vers FeedEntry */ } + Err(rss_err) => { + let mut cursor = std::io::Cursor::new(bytes.to_vec()); + match atom_syndication::Feed::read_from(&mut cursor) { + Ok(atom_feed) => { /* map vers FeedEntry */ } + Err(_e2) => Err(PollError::from(rss_err)), + } + } +} +``` + +--- + +## 07 — Politique réseau et sécurité + +- Production: HTTPS obligatoire, sauf loopback (tests/dev). +- Timeout configurable, redirections limitées (côté client GUI). +- Limite de taille 10 MiB pour éviter les abus et OOM. + +Snippet (enforcement): + +```rust +#[cfg(not(test))] +if url.scheme() != "https" { /* autorise localhost/127.0.0.1/::1 sinon UnsupportedScheme */ } +``` + +--- + +## 08 — Poller: cadence, retries, backoff + +```rust +#[derive(Debug, Clone)] +pub struct PollConfig { interval: Duration, request_timeout: Duration, max_retries: usize, retry_backoff_ms: u64 } + +pub fn spawn_poller(/* … */) -> PollerHandle { /* tokio::spawn + interval + select cancel */ } + +async fn fetch_feed_with_retries(/*…*/) -> Result, PollError> { + let mut attempt = 0; /* backoff exponentiel */ +} +``` + +Points d’attention: +- `MissedTickBehavior::Skip` évite l’effet “rattrapage” en cas de blocage. +- Emission d’évènements `Event::NewArticles(feed_id, entries)` via `mpsc`. + +--- + +## 09 — Déduplication: SeenStore + +Objectif: ne pousser vers l’UI que des articles jamais vus. + +```rust +pub async fn is_new_and_mark(&self, entry: &FeedEntry) -> bool { + let key = entry.identity(); /* persist JSON si nouveau */ +} +``` + +Design: +- Structure HashMap> sérialisée en JSON. +- Mode mémoire ou persistant (chemin injecté à l’initialisation). + +--- + +## 10 — API de données: DataApi + +Fonctions: gestion des feeds, marques “lus”, cache d’articles par feed. + +```rust +pub async fn add_feed(&self, feed: FeedDescriptor) { /* persist_feeds */ } +pub async fn mark_read(&self, entry: &FeedEntry) { /* persist_read */ } +pub async fn upsert_articles(&self, feed_id: &str, entries: Vec) { /* dédup + tri + truncate + persist */ } +``` + +Persistance atomique: +- écriture dans `*.json.tmp` puis `rename()` vers le fichier final. + +--- + +## 11 — Entrée GUI: initialisation (main.rs) + +```rust +let runtime = Arc::new(tokio::runtime::Runtime::new()?); +let (update_tx, update_rx) = mpsc::channel(64); +let client = reqwest::ClientBuilder::new().redirect(redirect::Policy::limited(5)).build()?; +let poll_config = load_poll_config(); +let poller = spawn_poller(feeds.clone(), poll_config.clone(), client.clone(), update_tx, seen); +eframe::run_native("ReadRSS", NativeOptions { /* viewport */ }, /* app */) +``` + +Points clés: +- Runtime Tokio propriété de l’appli, partagé aux services. +- Client HTTP partagé GUI/poller (clone, threadsafe). + +--- + +## 12 — Cartographie UI (AppView) + +Vues: `ArticleList`, `ArticleDetail`, `DiscoverHome`, `DiscoverCategory`, `Settings`. + +Principe: `draw_left_panel` pilote la navigation; `draw_main_content` route vers la vue courante. + +--- + +## 13 — Thème et style egui + +Application du thème depuis `AppConfig`: + +```rust +style.visuals.dark_mode = true; style.visuals.panel_fill = panel_color; /* … */ +style.visuals.widgets.active.bg_fill = accent_color; /* … */ +ctx.set_style(style); +``` + +Objectif: look cohérent, lisible, non flashy, contrôlé par l’utilisateur. + +--- + +## 14 — Panneau gauche: ajout/recherche/gestion + +Fonctions clés: +- Ajout d’un flux (HTTPS obligatoire, feedback en UI). +- Recherche locale par titre. +- Découverte (catégories recommandées) et Paramètres. + +Validation URL: + +```rust +if parsed.scheme() != "https" { /* feedback UI: refuser HTTP */ } +``` + +--- + +## 15 — Agrégateur d’articles + +Tri décroissant par date, pagination via `articles_per_page`, badge Non‑lu/Lu. +Ouverture d’un article: + +```rust +if ui.small_button("🔗 Ouvrir").clicked() { let _ = webbrowser::open(&article.url); } +``` + +--- + +## 16 — Lecture d’un article + +Rendu texte simplifié via `html2text` (HTML → texte brut). Options: ouvrir dans le navigateur, copier le lien. + +--- + +## 17 — Paramètres (sauvegarde immédiate) + +Sections: Thème, Interface, Flux. + +```rust +if ui.color_edit_button_rgb(&mut bg).changed() { self.config.theme.background_color = /* … */; let _ = self.config.save(); } +``` + +--- + +## 18 — Discover (recommandations) + +Catégories statiques (tech, dev, science, actu FR), ajout 1‑clic, rafraîchissement immédiat du flux ajouté. + +--- + +## 19 — Concurrency et canaux + +- `mpsc` pour pousser `Event::NewArticles` vers l’UI. +- `broadcast` pour l’arrêt propre du poller. +- `RwLock` pour la liste des feeds. + +--- + +## 20 — Limites, timeouts et robustesse + +- 10 MiB max par flux. +- Timeout requête configurable. +- Backoff exponentiel (base 500 ms). +- Skip des ticks manqués. + +--- + +## 21 — Erreurs et journalisation + +`PollError` centralise les échecs (réseau, parsing, taille, schéma, JoinError…). +`tracing` + `RUST_LOG` pour le debug. + +```rust +#[derive(Debug, Error)] +pub enum PollError { #[error("network error: {0}")] Network(#[from] reqwest::Error), /* … */ } +``` + +--- + +## 22 — Persistance: formats et chemins + +Fichiers par utilisateur: +- `config.json`, `feeds.json`, `read_store.json`, `articles_store.json`, `seen_store.json`. +- Linux: `~/.config/readrss/`; macOS: `~/Library/Application Support/readrss/`; Windows: `%APPDATA%/readrss/`. + +--- + +## 23 — Tests et mocks HTTP + +- Mocks via `wiremock` (levier sur reqwest). +- `poll_once` facilite des tests unitaires d’un seul tour de polling. + +--- + +## 24 — Packaging local et CI + +- Script local `.deb`: `scripts/build_deb.sh` (cargo‑deb en release par défaut). +- Release GitHub Actions: artefacts Linux (.tar.gz + .deb) et Windows (.zip). +- Correctif: `cargo deb --no-build` pour réutiliser le binaire déjà compilé. + +--- + +## 25 — Sécurité: menaces et parades + +- Refus HTTP (downgrade, MITM). +- Taille limitant la surface d’attaque DoS. +- Déduplication empêche l’inflation mémoire sur replays. +- Parsing RSS/Atom sous contrôle, pas d’exécution HTML (texte). + +--- + +## 26 — Performance + +- Streaming réseau; pas de WebView; rendu UI 2D via wgpu/egui. +- Cache articles par feed + pagination. +- Evite copies coûteuses; usage d’`Arc`, `RwLock`, slices. + +--- + +## 27 — UX: principes + +- Minimalisme: 3 gestes clés (ajouter, lire, ouvrir). +- Feedback immédiat pour les erreurs (URL, réseau). +- Paramètres sobres, pertinents. + +--- + +## 28 — Démonstration: add → fetch → read + +Pseudo‑séquence: + +``` +UI (Ajouter) → DataApi.add_feed → poll_once → SeenStore.is_new_and_mark → DataApi.upsert_articles → UI list +``` + +--- + +## 29 — Dépannage + +- Aucun article: vérifier HTTPS, connectivité, taille flux, logs `RUST_LOG=info`. +- Emojis manquants (Linux): installer `fonts-noto-color-emoji`. +- Fichiers corrompus: les .tmp servent de fallback lecture. + +--- + +## 30 — Roadmap + +- macOS artefacts, .desktop + icône pour Linux. +- Recherche plein‑texte, dossiers/étiquettes. +- Export/Import OPML. +- Internationalisation (i18n) et thèmes pré‑définis. + +--- + +## Annexes — extraits clés + +### Poller (extrait) + +```rust +pub fn spawn_poller(/* … */) -> PollerHandle { + let (cancel_tx, mut cancel_rx) = broadcast::channel(1); + let join = tokio::spawn(async move { + let mut ticker = tokio::time::interval(config.interval); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { tokio::select! { _ = cancel_rx.recv() => break, _ = ticker.tick() => { /* fetch */ } } } + }); + PollerHandle { cancel_tx, join } +} +``` + +### Conversion RSS → FeedEntry (extrait) + +```rust +pub fn from_rss_item(feed_id: &str, item: &rss::Item) -> Self { /* auteur, catégorie, content:encoded, enclosure */ } +``` + +### DataApi.upsert_articles (extrait) + +```rust +slot.sort_by(|a, b| b.published_at.cmp(&a.published_at)); +if slot.len() > MAX_PER_FEED { slot.truncate(MAX_PER_FEED); } +``` + +### Entrée main.rs (extrait) + +```rust +eframe::run_native("ReadRSS", NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 800.0]) , ..Default::default() }, + Box::new(move |cc| { install_emoji_friendly_fonts(&cc.egui_ctx); Box::new(RssApp::new(init)) })) +``` + +--- + +Fin du guide. \ No newline at end of file diff --git a/docs/RECAP_AMELIORATIONS.md b/docs/RECAP_AMELIORATIONS.md deleted file mode 100644 index 993d7dc..0000000 --- a/docs/RECAP_AMELIORATIONS.md +++ /dev/null @@ -1,183 +0,0 @@ -# Récapitulatif des améliorations apportées au projet ReadRSS - -## 🎯 Objectif principal -Transformer le lecteur RSS basique existant en une application moderne avec une interface graphique sombre inspirée de VS Code, incluant : -- Un panel latéral pour la gestion des flux -- Une vue principale pour les articles avec métadonnées étendues -- Un système de navigation et de lecture détaillée des articles - -## 📋 Actions réalisées - -### 1. ✅ Analyse du projet existant -**Fichiers examinés :** -- `Cargo.toml` : Structure du workspace et dépendances -- `rss-core/src/lib.rs`, `feed.rs`, `poller.rs` : Core RSS existant -- `rss-gui/src/app.rs`, `main.rs` : Interface basique existante - -**Constats :** -- Base fonctionnelle solide avec polling en arrière-plan -- Interface GUI minimaliste à enrichir -- Structure de données de base à étendre - -### 2. ✅ Amélioration du modèle de données - -**Fichier modifié :** `rss-core/src/feed.rs` - -**Changements apportés :** -```rust -// Ajout de nouveaux champs dans FeedEntry -pub struct FeedEntry { - // ... champs existants ... - pub author: Option, // Nouveau - pub category: Option, // Nouveau -} - -// Amélioration de la méthode from_rss_item -impl FeedEntry { - pub fn from_rss_item(feed_id: &str, item: &rss::Item) -> Self { - // Extraction de l'auteur depuis Dublin Core ou champ author - let author = item.dublin_core_ext() - .and_then(|dc| dc.creators().first().map(|s| s.to_string())) - .or_else(|| item.author().map(|s| s.to_string())); - - // Extraction de la catégorie depuis categories ou Dublin Core subject - let category = item.categories().first() - .map(|cat| cat.name().to_string()) - .or_else(|| { - item.dublin_core_ext() - .and_then(|dc| dc.subjects().first().map(|s| s.to_string())) - }); - // ... - } -} -``` - -### 3. ✅ Refonte complète de l'interface graphique - -**Fichier transformé :** `rss-gui/src/app.rs` - -#### 3.1 Nouveau système de thème sombre -```rust -fn setup_dark_theme(&self, ctx: &egui::Context) { - // Couleurs VS Code Dark Theme - let bg_color = Color32::from_rgb(30, 30, 30); // Arrière-plan principal - let panel_color = Color32::from_rgb(37, 37, 38); // Panneaux latéraux - let border_color = Color32::from_rgb(62, 62, 66); // Bordures - let text_color = Color32::from_rgb(204, 204, 204); // Texte principal - let accent_color = Color32::from_rgb(0, 122, 204); // Bleu accent VS Code - // Configuration complète du style... -} -``` - -#### 3.2 Nouveau système de navigation -```rust -#[derive(Debug, Clone)] -enum AppView { - ArticleList, // Vue liste des articles - ArticleDetail(FeedEntry), // Vue détaillée d'un article -} -``` - -#### 3.3 Fonctionnalités du panel latéral -- **Section d'ajout de flux** avec titre et URL -- **Barre de recherche** pour filtrer les flux -- **Liste des flux** avec sélection et suppression -- **Navigation** : bouton "Tous" pour voir tous les articles - -#### 3.4 Zone principale des articles -- **Mode liste** : Aperçu avec titre, auteur, catégorie, date, résumé tronqué -- **Mode détail** : Vue complète de l'article avec toutes les métadonnées -- **Actions** : Lecture, ouverture dans navigateur, copie de lien - -### 4. ✅ Ajout de nouvelles dépendances - -**Fichiers modifiés :** -- `Cargo.toml` (workspace) : Ajout de `webbrowser = "0.8"` -- `rss-gui/Cargo.toml` : Ajout des dépendances `webbrowser` et `chrono` - -### 5. ✅ Fonctionnalités implémentées - -#### Interface utilisateur -- ✅ Thème sombre VS Code -- ✅ Panel latéral redimensionnable (280-350px) -- ✅ Zone principale responsive -- ✅ Icônes émojis pour améliorer l'UX -- ✅ Groupes visuels et séparateurs - -#### Gestion des flux -- ✅ Ajout de flux avec titre et URL -- ✅ Recherche/filtrage des flux en temps réel -- ✅ Suppression de flux avec confirmation -- ✅ Sélection de flux pour filtrer les articles -- ✅ Bouton "Tous" pour voir tous les articles - -#### Affichage des articles -- ✅ Liste avec titre cliquable, auteur, catégorie, date -- ✅ Résumé tronqué (200 caractères max) -- ✅ Vue détaillée avec contenu complet -- ✅ Navigation retour depuis la vue détaillée -- ✅ Formatage des dates (DD/MM/YYYY HH:MM) - -#### Actions sur les articles -- ✅ Ouverture dans le navigateur système -- ✅ Copie du lien dans le presse-papiers -- ✅ Basculement entre vue liste et vue détaillée - -### 6. ✅ Gestion des erreurs et compilation - -**Problèmes résolus :** -- ❌ Erreur de délimiteur non fermé → ✅ Structure `impl` corrigée -- ❌ Import `chrono` manquant → ✅ Dépendance ajoutée -- ❌ Champs de style egui obsolètes → ✅ Utilisation de `override_text_color` -- ❌ Erreurs d'emprunt (borrow checker) → ✅ Clonage des données avant utilisation -- ❌ Variables non utilisées → ✅ Suppression des avertissements - -### 7. ✅ Documentation et README - -**Nouveau README.md complet :** -- 🚀 Section fonctionnalités avec émojis -- 🛠️ Architecture technique détaillée -- 📦 Instructions d'installation et compilation -- 🎯 Guide d'utilisation complet -- 🔧 Options de configuration -- 🎨 Guide de personnalisation du thème -- 📁 Documentation des structures de données -- 🚧 Roadmap des améliorations futures - -## 🎊 Résultat final - -### Interface transformée -**Avant :** Interface basique avec liste simple des flux et articles -**Après :** Interface moderne VS Code Dark avec : -- Panel latéral organisé avec recherche et gestion des flux -- Zone principale avec vue liste et vue détaillée -- Thème sombre professionnel -- Navigation fluide et intuitive - -### Fonctionnalités ajoutées -1. **Métadonnées enrichies** : auteur et catégorie des articles -2. **Recherche et filtrage** : des flux en temps réel -3. **Navigation avancée** : vue liste ↔ vue détaillée -4. **Actions utilisateur** : ouverture navigateur, copie lien -5. **Thème professionnel** : couleurs et styles VS Code Dark -6. **UX améliorée** : icônes, groupes visuels, feedback utilisateur - -### Stabilité et performance -- ✅ Compilation sans erreurs ni avertissements -- ✅ Gestion mémoire optimisée (250 articles max) -- ✅ Interface responsive et réactive -- ✅ Polling en arrière-plan maintenu -- ✅ Gestion d'erreurs robuste - -## 🔄 Processus de développement - -1. **Analyse** → Compréhension du code existant -2. **Planification** → Définition des objectifs et structure -3. **Extension données** → Ajout champs auteur/catégorie -4. **Refonte UI** → Implémentation thème et navigation -5. **Intégration** → Assemblage des composants -6. **Débogage** → Résolution erreurs compilation -7. **Test** → Vérification fonctionnement -8. **Documentation** → Mise à jour README et guides - -Le projet ReadRSS est maintenant une application moderne et professionnelle avec une interface utilisateur riche et intuitive, tout en conservant la robustesse de l'architecture de base. diff --git a/docs/packaging.md b/docs/packaging.md deleted file mode 100644 index 8ee061a..0000000 --- a/docs/packaging.md +++ /dev/null @@ -1,18 +0,0 @@ -# Packaging - -Objectif: produire des artefacts installables pour Linux/Windows/macOS. - -## Linux -- Binaire `rss-gui` en `target/release/`. Fournir une archive `.tar.gz` contenant le binaire et le README. -- Piste AppImage (à planifier): utiliser `appimagetool` + recette. Vérifier dépendances à l'exécution. - -## Windows -- Archive `.zip` avec `rss-gui.exe`. -- Piste MSI: WiX Toolset ou `cargo wix` (à planifier). - -## macOS -- Piste app bundle via `cargo-bundle` (à planifier). - -## Notes -- Les binaires sont auto-suffisants; la configuration utilisateur est stockée dans `~/.config/readrss`. -- Vérifier les licences des dépendances avant distribution. diff --git a/docs/plan_travail.md b/docs/plan_travail.md deleted file mode 100644 index 9321d32..0000000 --- a/docs/plan_travail.md +++ /dev/null @@ -1,49 +0,0 @@ -# Plan de travail détaillé - -Ce document découpe le travail restant en quatre grands volets afin de répartir efficacement les tâches au sein de l'équipe. Chaque section précise les objectifs, les actions concrètes à mener, les livrables attendus et les critères de validation. - -## 1. Service de surveillance des flux RSS (Monitoring & Polling) -- **Objectif** : disposer d'un service robuste capable de surveiller automatiquement les flux RSS configurés et de détecter les nouveaux articles. -- **Actions à mener** : - - Finaliser la configuration du `PollConfig` (intervalle personnalisable, éventuellement via un fichier de configuration ou une interface CLI). - - Implémenter un mécanisme de persistance légère (fichier JSON ou base SQLite) pour mémoriser les articles déjà vus et éviter les doublons. - - Ajouter une gestion fine des erreurs réseau (timeouts, flux inaccessibles) avec une stratégie de retry et de journalisation claire. - - Exposer des événements structurés (ex. `NewArticles(feed_id, Vec)`) via un canal ou un bus interne pour que l'interface puisse se synchroniser. -- **Livrables** : module `rss-core` enrichi, tests unitaires sur le poller, rapport d'état (log) clair. -- **Résultats attendus** : le service tourne en tâche de fond, détecte les nouveautés et les met à disposition du front sans doublons ni plantages. - -## 2. Gestion des données et synchronisation (Stockage & API interne) -- **Objectif** : fournir un socle de données cohérent (flux, articles, préférences) accessible autant au poller qu'à l'interface. -- **Actions à mener** : - - Définir un modèle de données partagé (structs + sérialisation) pour les feeds, articles, catégories, tags éventuels. - - Mettre en place un stockage persistant (SQLite via `sqlx` ou `rusqlite`, ou fichiers JSON versionnés) avec migrations simples. - - Concevoir une petite API interne (fonctions ou façade) pour : ajouter/supprimer un flux, marquer un article comme lu, récupérer l’état courant (flux + derniers articles). - - Prévoir un mécanisme de synchronisation entre le runtime asynchrone et l’interface (verrous RwLock, cache en mémoire, notifications). -- **Livrables** : module de stockage documenté, scénario de test (ex. ajout d’un flux, récupération d’articles, suppression) et script de migration initiale. -- **Résultats attendus** : données persistées de manière fiable, accessibles par tous les composants, avec une API claire pour le front. - -## 3. Interface graphique (Responsable : Dan) -- **Objectif** : concevoir une application bureau moderne, épurée et ergonomique qui met en valeur les flux et articles suivis. -- **Actions à mener** : - - Définir un design system léger (palette de couleurs, typographies, variantes de cartes d’articles) en respectant l'esprit minimaliste demandé. - - Concevoir les écrans principaux : liste des flux, détail d’un flux avec ses articles, vue d’article détaillée (titre, résumé, contenu enrichi si possible), panneau de gestion (ajout/suppression de flux, paramétrage des intervalles, thèmes). - - Implémenter les composants `egui` correspondants : navigation, panneaux latéraux, listes scrollables, zones de recherche, boutons d’action. - - Ajouter des animations ou transitions légères (éventuellement via `egui` custom painting) pour donner un aspect « ultra moderne ». - - Prévoir la gestion des modes clair/sombre et l’adaptation responsive (fenêtre redimensionnée). -- **Livrables** : maquettes (sketch ou Figma), puis implémentation `egui` complète avec un thème personnalisable. -- **Résultats attendus** : l’utilisateur peut parcourir les flux et articles de manière fluide, avec une interface soignée et cohérente. - -## 4. Intégration, QA et packaging -- **Objectif** : garantir que l’ensemble fonctionne de bout en bout et préparer la distribution de l’application. -- **Actions à mener** : - - Configurer des tests d’intégration : lancement du poller, arrivée d’un nouvel article, affichage dans l’interface. - - Mettre en place un pipeline de CI simple (GitHub Actions ou autre) pour exécuter `cargo fmt`, `cargo clippy`, `cargo test`, et vérifier les builds. - - Documenter les procédures d’installation (README et wiki) : dépendances système (X11/Wayland), commandes de build, étapes de packaging. - - Préparer un packaging multiplateforme (AppImage, MSI, DMG ou zip) pour livrer l’application finale. - - Organiser une session de QA interne : checklist de fonctionnalités, tests manuels, collecte de feedback et corrections. -- **Livrables** : scripts CI, documentation utilisateur/développeur, artefacts de build. -- **Résultats attendus** : application prête à être testée/présentée, processus de build reproductible, documentation claire pour l’équipe. - ---- - -En répartissant les tâches ainsi, chaque membre peut se concentrer sur un bloc cohérent tout en respectant les interdépendances (le storage alimente le poller et l’UI, le packaging dépend de la stabilité des trois autres volets, etc.). Ce plan constitue la feuille de route jusqu’à une version aboutie du lecteur RSS. diff --git a/docs/qa_checklist.md b/docs/qa_checklist.md deleted file mode 100644 index b488b08..0000000 --- a/docs/qa_checklist.md +++ /dev/null @@ -1,12 +0,0 @@ -# Checklist QA interne - -- [ ] Lancement de l'application sans erreur (Linux/Wayland et X11) -- [ ] Ajout d'un flux valide (ex: https://blog.rust-lang.org/feed.xml) -- [ ] Réception d'articles et affichage dans la liste -- [ ] Pas de doublons après plusieurs cycles de poll -- [ ] Suppression d'un flux et arrêt des mises à jour associées -- [ ] Gestion des erreurs réseau: feed injoignable -> logs visibles, application stable -- [ ] Délais/intervalle: modification via `~/.config/readrss/config.json` prise en compte au prochain lancement -- [ ] Performance UI: scroll fluide avec 200+ articles -- [ ] Mode sombre/clair (si activé par l'OS) respecté par `egui` -- [ ] Tests automatisés passent (`cargo test`) diff --git a/docs/recap_2025-10-28.md b/docs/recap_2025-10-28.md deleted file mode 100644 index 639dcef..0000000 --- a/docs/recap_2025-10-28.md +++ /dev/null @@ -1,42 +0,0 @@ -# Récapitulatif des modifications — 2025-10-28 - -Ce document résume l’ensemble des corrections et améliorations réalisées ce soir. - -## Correctifs critiques - -- Correction d’un crash lors de l’affichage des aperçus d’articles (UTF-8): - - Problème: la troncature du texte utilisait des indices d’octets, provoquant un panic sur les caractères accentués/emoji (« byte index … is not a char boundary »). - - Solution: troncature Unicode-safe via `.chars()` avec ellipse, dans `rss-gui/src/app.rs`. - --- WebView (wry + tao): la tentative d’intégration a été abandonnée en raison de soucis de compatibilité Linux persistants (fenêtre blanche). Décision: suppression complète de la fonctionnalité et des dépendances; retour au comportement simple « ouvrir dans le navigateur ». - -## Améliorations UX / stabilité - -- Police/emoji: le chargement via fontconfig (Noto Color Emoji / Noto Sans Symbols2 / DejaVu Sans) est conservé; les logs confirment les polices trouvées. -- La fenêtre WebView n’est plus forcée « always-on-top » et affiche des bordures système pour limiter les soucis d’empilement. - -### Fonctionnalités UI ajoutées/précisées - -- Vue agrégée « Tous »: affiche les articles de l’ensemble des flux, avec labels de source colorés pour la lisibilité. -- Boutons de rafraîchissement: - - « Refresh » au niveau d’un flux (rafraîchit un seul flux) - - « Refresh all » global (rafraîchit tous les flux) -- Onglet « Discover »: catégories de flux recommandés et action « Suivre » rapide. -- Barre de recherche (latérale): recherche par titre d’article avec filtrage en direct. -- Cartes d’articles: correctif de hauteur du premier card et meilleure lisibilité (métadonnées auteur/catégorie/date, état lu/non-lu). - -## Dépendances / build - -- Suppression de `wry` et `tao`, et de la feature `webview`. Plus de dépendances WebKitGTK nécessaires. - - Le README a été mis à jour en conséquence. - -## Points à vérifier côté utilisateur - -- La lecture complète d’un article s’effectue via le bouton « Ouvrir dans le navigateur » (navigateur par défaut du système). - -## Suivis/Idées futures - -- Améliorer l’aperçu texte optionnel (extraction plus robuste, limites configurables). -- Ajouter des favoris et un marquage « à lire plus tard ». -- Raccourcis clavier (navigation, marquer lu/non‑lu, rafraîchir). -- Export/Import OPML pour les flux. diff --git a/docs/recapitulatif_actions.md b/docs/recapitulatif_actions.md deleted file mode 100644 index 71ccc6f..0000000 --- a/docs/recapitulatif_actions.md +++ /dev/null @@ -1,47 +0,0 @@ -# Récapitulatif des actions réalisées - -Ce document résume, pas à pas et avec un ton pédagogique, tout ce qui a été mis en place dans le projet jusqu'à présent. - -## 1. Mise en place de l'environnement de travail -- Création du fichier `.github/copilot-instructions.md` pour suivre les étapes de production et garder la même ligne directrice tout au long du projet. -- Validation des besoins : projet Rust en groupe de 4, lecteur RSS avec service d'actualisation en arrière-plan et interface graphique dédiée. - -## 2. Structure du workspace Cargo -- Création d'un workspace `Cargo.toml` à la racine contenant deux membres : - - `rss-core` : bibliothèque où vit toute la logique métier (gestion des flux, poller, modèles partagés). - - `rss-gui` : application graphique qui consomme la bibliothèque `rss-core`. -- Ajout d'une section `[workspace.dependencies]` pour déclarer les dépendances communes (Tokio, Reqwest, RSS, Chrono, Serde, egui/eframe, tracing…). -- Ajout d'un `.gitignore` adapté aux projets Rust (répertoire `target/`, sauvegardes temporaires, dossiers éditeur). - -## 3. Contenu de la bibliothèque `rss-core` -- `src/lib.rs` : expose les modules et ré-exporte les types/fonctions utiles pour le front-end. -- `src/error.rs` : définit une énumération `PollError` avec le derive `thiserror::Error` pour une gestion des erreurs claire. -- `src/feed.rs` : - - Modèles `FeedDescriptor` et `FeedEntry` sérialisables (Serde) pour décrire un flux et ses articles. - - Fonctions utilitaires `add_feed`, `remove_feed`, `list_feeds` opérant sur un `Arc>>` (alias `SharedFeedList`). - - Conversion d'un `rss::Item` vers notre `FeedEntry` avec normalisation de la date de publication. -- `src/poller.rs` : - - Structure `PollConfig` (intervalle) et `PollerHandle` (contrôle du poller asynchrone). - - Fonction `spawn_poller` qui boucle avec `tokio::time::interval`, récupère chaque flux via `reqwest`, parse le contenu RSS, enrichit les entrées et pousse les nouveautés sur un canal `mpsc`. - -## 4. Contenu de l'application `rss-gui` -- `src/main.rs` : - - Initialisation du tracing. - - Création d'un runtime Tokio partagé (`Arc`), du magasin de flux (`shared_feed_list`), du poller et du canal d'updates. - - Lancement de l'application `eframe` en passant une instance de `RssApp`. -- `src/app.rs` : - - Structure `AppInit` (données préparées avant l'UI) et `RssApp` (état de l'application). - - Gestion de l'ajout/suppression de flux, rafraîchissement des nouvelles entrées et tri des articles. - - Interface `egui` avec panneau latéral pour les flux suivis et panneau central listant les articles. - - Arrêt propre du poller dans `Drop` pour éviter les tâches orphelines. - -## 5. Documentation projet (`README.md`) -- Introduction au projet en français, description du rôle de chaque crate. -- Pré-requis techniques et commandes courantes (formatage, linting, compilation, exécution). -- Guide de collaboration Git : initialisation locale, création du dépôt distant, workflow de branches et Pull Requests. - -## 6. Préparation aux prochaines étapes -- Instructions prêtes pour lancer `cargo check`, `cargo fmt`, `cargo clippy`, `cargo run -p rss-gui` (commandes à lancer directement dans le terminal standard, sans `flatpak-spawn`). -- Liste de pistes d'évolution (persistance locale, notifications, préférences utilisateur). - -Ce récapitulatif servira de base de référence avant l'initialisation Git et le partage avec les autres membres du groupe. From f56802f244e615ed87cd6ff2f6369472d43170ca Mon Sep 17 00:00:00 2001 From: Anthony Mittica Date: Wed, 29 Oct 2025 12:20:09 +0100 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20r=C3=A9=C3=A9criture=20du=20Guide?= =?UTF-8?q?=20technique=20=E2=80=94=20explications=20d=C3=A9taill=C3=A9es,?= =?UTF-8?q?=20lexique,=20contrats=20I/O,=20d=C3=A9pendances=20et=20p=C3=A9?= =?UTF-8?q?dagogie=20d=C3=A9butant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide_technique_ReadRSS.md | 454 ++++++++++++++++---------------- 1 file changed, 230 insertions(+), 224 deletions(-) diff --git a/docs/Guide_technique_ReadRSS.md b/docs/Guide_technique_ReadRSS.md index 2cdae95..57117d3 100644 --- a/docs/Guide_technique_ReadRSS.md +++ b/docs/Guide_technique_ReadRSS.md @@ -1,135 +1,153 @@ -# ReadRSS — Guide technique complet (≈ 30 minutes) +# ReadRSS — Guide technique détaillé et pédagogique (≈ 30 minutes) -Ce document sert de support d’oral (1 section ≈ 1 minute) et de référence technique exhaustive pour ReadRSS. Chaque section est concise, progressive et illustrée de code du projet. +Ce document est un guide COMPLÈTEMENT autonome pour expliquer ReadRSS à un public mixte (débutants en Rust compris) et servir de script d’oral. Chaque section correspond à ~1 minute. Vous trouverez pour chaque sujet: un objectif clair, les dépendances, le fonctionnement interne, un petit lexique, des schémas mentaux et, quand utile, de courts extraits de code tirés du projet. + +Astuce d’utilisation: lisez linéairement si vous découvrez le projet; pour une présentation, traitez 1 section = 1 diapo/minute. --- -## 01 — Vision et objectifs +## 01 — Vision, promesse utilisateur et contraintes + +Objectif: un lecteur RSS/Atom local, rapide, fiable, sans complexité inutile. -ReadRSS est un lecteur RSS/Atom local, rapide et simple: -- Polling en arrière‑plan avec limites, timeouts, retries. -- UI fluide (egui/wgpu), sans WebView: ouverture des liens dans le navigateur système. -- Données et configuration persistées côté utilisateur, par OS. -- Sécurité: HTTPS obligatoire en production (loopback autorisé en dev/tests). +- Promesse: “j’ajoute des flux, ça se met à jour tout seul, je lis, je classe, j’ouvre dans le navigateur”. +- Contraintes de sûreté: HTTPS obligatoire (sauf loopback en dev/tests), limite 10 MiB par flux, timeouts et retries. +- Contraintes d’UX: démarrage rapide, UI réactive (aucune E/S ou réseau ne bloque le rendu), paramètres persistés immédiatement. -Contrats d’expérience: -- Démarre vite, ne bloque jamais l’UI sur les E/S. -- Lire hors‑ligne ce qui a déjà été récupéré. -- Paramètres sauvegardés immédiatement. +Lexique: +- RSS/Atom: formats XML listant des items d’actualité. +- Poller: tâche périodique qui récupère les flux. +- Déduplication: éviter de ré‑annoncer un article déjà vu. --- -## 02 — Architecture d’ensemble +## 02 — Carte d’architecture (vue macro) Workspace Cargo: -- `rss-core` (bibliothèque): modèles, parsing, polling, persistance, erreurs, seen store. -- `rss-gui` (application): eframe/egui, navigation, thèmes, intégration `rss-core`. +- `rss-core` (lib): modèle, parsing, polling, persistance, erreurs, “seen store”. +- `rss-gui` (app): eframe/egui (wgpu), navigation, thèmes, logique UI. -Flux de données (simplifié): +Flux logique (de gauche à droite): ``` -HTTP (reqwest) → parse (rss/atom) → FeedEntry → SeenStore (dédup) → DataApi (persist) - ↓ - UI (egui) +Réseau (reqwest) → Parsing (rss/atom_syndication) → FeedEntry → SeenStore (dédup) + ↘ DataApi (persist JSON) → UI (egui) ``` +Dépendances clefs: `tokio` (async), `reqwest` (HTTP, rustls), `rss` et `atom_syndication` (parsing), `serde` (JSON), `egui/eframe` (UI), `tracing` (logs). + --- -## 03 — Modules principaux (core) +## 03 — Modules (core) et responsabilités -- `config`: AppConfig, gestion du fichier `config.json` (chargement/écriture). -- `poller`: tâche périodique, timeouts, retries, 10 MiB max, HTTPS only. -- `feed`: modèles `FeedDescriptor`, `FeedEntry` et conversions RSS/Atom. -- `data`: API de données (feeds, read-state, cache d’articles) avec persistance atomique .tmp. +- `config`: charge/sauvegarde `AppConfig` (thème, UI, params de polling). +- `poller`: cadence, timeouts, retries, émet `Event::NewArticles`. +- `feed`: structures `FeedDescriptor`, `FeedEntry` et conversions RSS/Atom. +- `data`: API persistante (feeds, “lus”, cache d’articles) écriture atomique `.tmp`. - `storage`: `SeenStore` (déduplication persistée). -- `error`: `PollError` centralise les erreurs. +- `error`: `PollError` (réseau, parsing, schéma, taille, tâche…). -Exposition publique (`rss-core/src/lib.rs`): +Code d’export (`rss-core/src/lib.rs`) pour tout réutiliser côté app. -```rust -pub mod config; pub mod data; pub mod error; pub mod feed; pub mod poller; pub mod storage; -pub use config::{AppConfig, FeedConfig, ThemeConfig, UiConfig}; -pub use data::DataApi; -pub use error::PollError; -pub use feed::{FeedDescriptor, FeedEntry, SharedFeedList, add_feed, list_feeds, remove_feed, shared_feed_list}; -pub use poller::{poll_once, spawn_poller, Event, PollConfig, PollerHandle}; -pub use storage::SeenStore; -``` +--- + +## 04 — Lexique minimal Rust et async + +- Crate: paquet Rust (lib ou binaire). Workspace: ensemble de crates. +- Trait `Send + Sync`: partagabilité entre threads. +- `Arc`: pointeur partagé thread‑safe; `RwLock`: verrou lecture/écriture. +- `async/await`: écriture asynchrone; `tokio::spawn`: lance une tâche concurrente. +- `mpsc`/`broadcast`: canaux asynchrones (point‑à‑point / un‑à‑N). + +But: comprendre la mécanique sans plonger dans tous les détails bas niveau. --- -## 04 — Configuration: AppConfig +## 05 — Configuration: AppConfig (où, quand, comment) + +Chemin: `rss-core/src/config.rs` -Rôle: centraliser thème, UI et paramètres de polling côté utilisateur. +Rôle: centraliser les préférences utilisateur (couleurs, largeur panneau, pagination) et les paramètres réseau (timeouts, intervalle, retries). Fichier stocké par OS dans le dossier `readrss` de l’utilisateur. +Contrat: +- Entrée: JSON partiel accepté (valeurs par défaut appliquées si clés manquantes). +- Sortie: objet `AppConfig` utilisable partout (UI + runtime). +- Erreurs: en cas d’échec de lecture, on crée un défaut et on le sauvegarde. + +Extrait: ```rust #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AppConfig { pub theme: ThemeConfig, pub feeds: FeedConfig, pub ui: UiConfig } - -impl AppConfig { - pub fn config_file_path() -> Result> { /* … */ } - pub fn load() -> Self { /* défaut + save si absent */ } - pub fn save(&self) -> Result<(), Box> { /* … */ } -} +impl AppConfig { pub fn load() -> Self { /* défaut si lecture échoue + auto-save */ } } ``` -Design: -- Toujours chargeable (fallback défaut + auto‑save en cas d’erreur). -- Neutralité UI: pas de dépendance forte à egui, seulement des couleurs `[u8;3]`. +Dépend: `dirs` (chemin config), `serde`/`serde_json`. +Utilisé par: `rss-gui` (thème et sliders), construction de `PollConfig`. --- -## 05 — Modèle de données: FeedDescriptor et FeedEntry +## 06 — Données: FeedDescriptor et FeedEntry (schémas) -```rust -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub struct FeedDescriptor { pub id: String, pub title: String, pub url: String } +Chemin: `rss-core/src/feed.rs` -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct FeedEntry { - pub feed_id: String, pub title: String, pub summary: Option, pub url: String, - pub published_at: Option>, pub guid: Option, /* … */ -} +Schémas: +- `FeedDescriptor { id, title, url }` décrit un flux suivi. +- `FeedEntry` représente un article normalisé (titre, url, auteur, date, guid…). +Points clés: +- `identity()` fabrique une clé stable (GUID > URL > titre@timestamp) — sert à la déduplication. +- Conversions depuis RSS et Atom remplissent au mieux les champs. + +Extrait: +```rust impl FeedEntry { pub fn identity(&self) -> String { /* GUID > URL > titre@ts */ } } ``` -Points clés: -- `identity()` assure une déduplication robuste. -- Conversions depuis `rss::Item` et `atom::Entry` enrichissent les champs (auteur, catégorie…). +--- + +## 07 — Persistance des données: DataApi (contrats) + +Chemin: `rss-core/src/data.rs` + +Responsabilités: +- Feeds: ajouter/supprimer/lister avec persistance (`feeds.json`). +- Read‑state: marquer “lu” (`read_store.json`). +- Articles: cache par feed (`articles_store.json`), déduplication + tri + truncate. + +Contrats fonctionnels: +- `add_feed(feed)` — Entrée: `FeedDescriptor`; Effet: persiste et met à jour la liste. +- `mark_read(entry)` — Entrée: `FeedEntry`; Effet: persiste la marque “lu”. +- `upsert_articles(feed_id, entries)` — Entrée: liste d’articles; Effet: fusion, tri, limite, persistance atomique. + +Note: écriture atomique via fichier `.tmp` puis `rename()`. --- -## 06 — Parsing RSS puis fallback Atom +## 08 — Déduplication persistée: SeenStore -`fetch_feed` lit en streaming, limite à 10 MiB, parse RSS sinon tente Atom: +Chemin: `rss-core/src/storage.rs` -```rust -let mut cursor_rss = std::io::Cursor::new(bytes.to_vec()); -match rss::Channel::read_from(&mut cursor_rss) { - Ok(channel) => { /* map vers FeedEntry */ } - Err(rss_err) => { - let mut cursor = std::io::Cursor::new(bytes.to_vec()); - match atom_syndication::Feed::read_from(&mut cursor) { - Ok(atom_feed) => { /* map vers FeedEntry */ } - Err(_e2) => Err(PollError::from(rss_err)), - } - } -} -``` +Rôle: empêcher l’UI de recevoir à nouveau un article déjà diffusé. Différent de “lu” (qui relève de l’utilisateur). + +Contrat: +- `is_new_and_mark(entry) -> bool`: retourne true s’il n’a jamais été vu (et le marque immédiatemment), sinon false. + +Structure de données: `HashMap>` sérialisé en JSON. --- -## 07 — Politique réseau et sécurité +## 09 — Réseau et sécurité: fetch_feed (politique) -- Production: HTTPS obligatoire, sauf loopback (tests/dev). -- Timeout configurable, redirections limitées (côté client GUI). -- Limite de taille 10 MiB pour éviter les abus et OOM. +Chemin: `rss-core/src/poller.rs` -Snippet (enforcement): +Garanties: +- HTTPS requis hors tests/dev (exception loopback). +- Taille max 10 MiB; streaming du body pour limiter la mémoire. +- Timeout configurable par `PollConfig`. +Extrait de contrôle de schéma: ```rust #[cfg(not(test))] if url.scheme() != "https" { /* autorise localhost/127.0.0.1/::1 sinon UnsupportedScheme */ } @@ -137,251 +155,244 @@ if url.scheme() != "https" { /* autorise localhost/127.0.0.1/::1 sinon Unsupport --- -## 08 — Poller: cadence, retries, backoff +## 10 — Parsing: d’abord RSS, puis fallback Atom -```rust -#[derive(Debug, Clone)] -pub struct PollConfig { interval: Duration, request_timeout: Duration, max_retries: usize, retry_backoff_ms: u64 } +Stratégie: essayer RSS; si parsing échoue, tenter Atom; si les deux échouent, retourner l’erreur RSS (plus informative). -pub fn spawn_poller(/* … */) -> PollerHandle { /* tokio::spawn + interval + select cancel */ } - -async fn fetch_feed_with_retries(/*…*/) -> Result, PollError> { - let mut attempt = 0; /* backoff exponentiel */ +Extrait simplifié: +```rust +match rss::Channel::read_from(&mut cursor_rss) { + Ok(channel) => map_items(channel.items()), + Err(rss_err) => match atom_syndication::Feed::read_from(&mut cursor_atom) { + Ok(feed) => map_entries(feed.entries()), + Err(_) => Err(PollError::from(rss_err)) + } } ``` -Points d’attention: -- `MissedTickBehavior::Skip` évite l’effet “rattrapage” en cas de blocage. -- Emission d’évènements `Event::NewArticles(feed_id, entries)` via `mpsc`. - --- -## 09 — Déduplication: SeenStore +## 11 — PollConfig et backoff (retry exponentiel) -Objectif: ne pousser vers l’UI que des articles jamais vus. +Paramètres: +- `interval`: cadence du polling. +- `request_timeout`: timeout HTTP par requête. +- `max_retries`: nb max de tentatives. +- `retry_backoff_ms`: base du backoff exponentiel. +Extrait: ```rust -pub async fn is_new_and_mark(&self, entry: &FeedEntry) -> bool { - let key = entry.identity(); /* persist JSON si nouveau */ -} +let backoff = cfg.retry_backoff_ms * (1u64 << (attempt - 1)); +tokio::time::sleep(Duration::from_millis(backoff)).await; ``` -Design: -- Structure HashMap> sérialisée en JSON. -- Mode mémoire ou persistant (chemin injecté à l’initialisation). +--- + +## 12 — Tâche de polling: spawn_poller (concurrence) + +Mécanique: +- `tokio::spawn` crée une tâche qui réveille un `interval`. +- À chaque tick: snapshot des feeds, fetch en séquence (simple et sûr), émission d’évènements. +- Arrêt: canal `broadcast` (envoi `()`), `join.await` dans `stop()`. + +Contrats d’erreur: toute erreur de réseau/parsing est loggée, pas fatale. --- -## 10 — API de données: DataApi +## 13 — Évènements: Event::NewArticles -Fonctions: gestion des feeds, marques “lus”, cache d’articles par feed. +Chemin: `rss-core/src/poller.rs` -```rust -pub async fn add_feed(&self, feed: FeedDescriptor) { /* persist_feeds */ } -pub async fn mark_read(&self, entry: &FeedEntry) { /* persist_read */ } -pub async fn upsert_articles(&self, feed_id: &str, entries: Vec) { /* dédup + tri + truncate + persist */ } -``` +Rôle: isoler l’UI des détails réseau. L’UI ne “scrape” jamais directement: elle consomme des évènements. -Persistance atomique: -- écriture dans `*.json.tmp` puis `rename()` vers le fichier final. +Format: `NewArticles(feed_id, Vec)` + +Dépendances: `mpsc::Sender` passé à `spawn_poller`. --- -## 11 — Entrée GUI: initialisation (main.rs) +## 14 — Point d’entrée GUI (main.rs) + +Chemin: `rss-gui/src/main.rs` + +Étapes: +1. Initialiser tracing (logs filtrables via `RUST_LOG`). +2. Créer un runtime Tokio et les services (DataApi, SeenStore, client HTTP). +3. Dériver `PollConfig` à partir d’`AppConfig` (cohérence UI/runtime). +4. Lancer le poller et démarrer la fenêtre eframe/egui. +Extrait: ```rust -let runtime = Arc::new(tokio::runtime::Runtime::new()?); -let (update_tx, update_rx) = mpsc::channel(64); -let client = reqwest::ClientBuilder::new().redirect(redirect::Policy::limited(5)).build()?; -let poll_config = load_poll_config(); -let poller = spawn_poller(feeds.clone(), poll_config.clone(), client.clone(), update_tx, seen); -eframe::run_native("ReadRSS", NativeOptions { /* viewport */ }, /* app */) +let poller = spawn_poller(feeds.clone(), poll_config.clone(), client, update_tx, seen_store); +eframe::run_native("ReadRSS", NativeOptions { /* … */ }, Box::new(move |_| Box::new(RssApp::new(init)))) ``` -Points clés: -- Runtime Tokio propriété de l’appli, partagé aux services. -- Client HTTP partagé GUI/poller (clone, threadsafe). - --- -## 12 — Cartographie UI (AppView) +## 15 — Architecture UI: vues et navigation -Vues: `ArticleList`, `ArticleDetail`, `DiscoverHome`, `DiscoverCategory`, `Settings`. +Chemin: `rss-gui/src/app.rs` -Principe: `draw_left_panel` pilote la navigation; `draw_main_content` route vers la vue courante. +Vues principales: +- Liste d’articles, Détail d’article, Discover (catégories), Paramètres. + +Navigation: +- Panneau gauche: ajout/recherche, accès Discover/Paramètres, sélection de flux. +- Panneau central: route selon `current_view`. --- -## 13 — Thème et style egui +## 16 — Thème et styles (egui) -Application du thème depuis `AppConfig`: +Règles: +- Couleurs et arrondis issus d’`AppConfig`. +- Objectif lisibilité (contraste, hover, active). +Extrait (simplifié): ```rust -style.visuals.dark_mode = true; style.visuals.panel_fill = panel_color; /* … */ -style.visuals.widgets.active.bg_fill = accent_color; /* … */ +style.visuals.dark_mode = true; +style.visuals.widgets.active.bg_fill = accent_color; ctx.set_style(style); ``` -Objectif: look cohérent, lisible, non flashy, contrôlé par l’utilisateur. - --- -## 14 — Panneau gauche: ajout/recherche/gestion - -Fonctions clés: -- Ajout d’un flux (HTTPS obligatoire, feedback en UI). -- Recherche locale par titre. -- Découverte (catégories recommandées) et Paramètres. +## 17 — Ajout d’un flux: validation et feedback -Validation URL: +UX: titre optionnel, URL obligatoire et en HTTPS (sinon message d’erreur). Après ajout: déclenchement d’un `poll_once` immédiat pour “voir un résultat tout de suite”. +Extrait: ```rust -if parsed.scheme() != "https" { /* feedback UI: refuser HTTP */ } +if parsed.scheme() != "https" { self.add_feedback = Some((false, "Seules les URLs HTTPS…".into())); } ``` --- -## 15 — Agrégateur d’articles +## 18 — Discover: recommandations prêtes à suivre -Tri décroissant par date, pagination via `articles_per_page`, badge Non‑lu/Lu. -Ouverture d’un article: +Principe: listes statiques de flux classées par catégorie (Tech, Dev, Science, Actu FR). Bouton “Suivre” → ajout + rafraîchissement instantané. -```rust -if ui.small_button("🔗 Ouvrir").clicked() { let _ = webbrowser::open(&article.url); } -``` +But: onboarding immédiat sans chercher des URLs. --- -## 16 — Lecture d’un article +## 19 — Liste d’articles: agrégation et filtrage -Rendu texte simplifié via `html2text` (HTML → texte brut). Options: ouvrir dans le navigateur, copier le lien. +Fonctions: +- Vue “Tous” (agrégée) ou par flux. +- Tri par date décroissante, pagination via `articles_per_page`. +- “Non lus” uniquement (en s’appuyant sur `DataApi.is_read`). --- -## 17 — Paramètres (sauvegarde immédiate) +## 20 — Détail d’un article et actions -Sections: Thème, Interface, Flux. +Rendu: `html2text` transforme le HTML en texte brut (lisible, sûr). +Actions: Ouvrir dans le navigateur (mise en page native), Copier le lien. -```rust -if ui.color_edit_button_rgb(&mut bg).changed() { self.config.theme.background_color = /* … */; let _ = self.config.save(); } -``` +Sécurité: l’UI ne rend pas du HTML riche (pas de WebView), donc pas d’exécution de scripts. --- -## 18 — Discover (recommandations) +## 21 — Paramètres: thème, interface et flux -Catégories statiques (tech, dev, science, actu FR), ajout 1‑clic, rafraîchissement immédiat du flux ajouté. +Sauvegarde immédiate: chaque slider/checkbox écrit le JSON. +Impact: thème appliqué à chaud; paramètres des feeds pris en compte à la relance (ou conversion vers `PollConfig` dès l’entrée). --- -## 19 — Concurrency et canaux +## 22 — Concurrence et canaux (modèle mental) + +- Poller: tâche async autonome qui pousse des évènements. +- UI: boucle egui qui consomme les évènements et persiste via `DataApi`. +- Partage: `Arc>>` pour la liste des flux. -- `mpsc` pour pousser `Event::NewArticles` vers l’UI. -- `broadcast` pour l’arrêt propre du poller. -- `RwLock` pour la liste des feeds. +Avantage: découplage réseau/UI, robustesse, simplicité de debug. --- -## 20 — Limites, timeouts et robustesse +## 23 — Robustesse: limites et timeouts -- 10 MiB max par flux. -- Timeout requête configurable. -- Backoff exponentiel (base 500 ms). -- Skip des ticks manqués. +Pourquoi 10 MiB? Éviter les flux anormalement gros (DoS mémoire/temps). +Pourquoi des retries? L’Internet est faillible; on retente avec backoff exponentiel. +Pourquoi `MissedTickBehavior::Skip`? On ne rattrape pas un retard si l’app a été gelée (préserve la réactivité). --- -## 21 — Erreurs et journalisation +## 24 — Gestion des erreurs et logs -`PollError` centralise les échecs (réseau, parsing, taille, schéma, JoinError…). -`tracing` + `RUST_LOG` pour le debug. +`PollError` catégorise les échecs (réseau, parsing, scheme, taille, task join). +`tracing` permet `RUST_LOG=info`/`debug` pour diagnostiquer. +Extrait: ```rust #[derive(Debug, Error)] -pub enum PollError { #[error("network error: {0}")] Network(#[from] reqwest::Error), /* … */ } +pub enum PollError { Network(#[from] reqwest::Error), Parse(#[from] rss::Error), /* … */ } ``` --- -## 22 — Persistance: formats et chemins +## 25 — Formats et chemins de persistance -Fichiers par utilisateur: +Fichiers côté utilisateur: - `config.json`, `feeds.json`, `read_store.json`, `articles_store.json`, `seen_store.json`. -- Linux: `~/.config/readrss/`; macOS: `~/Library/Application Support/readrss/`; Windows: `%APPDATA%/readrss/`. - ---- +- Dossiers: Linux `~/.config/readrss/`, macOS `~/Library/Application Support/readrss/`, Windows `%APPDATA%/readrss/`. -## 23 — Tests et mocks HTTP - -- Mocks via `wiremock` (levier sur reqwest). -- `poll_once` facilite des tests unitaires d’un seul tour de polling. +Lecture/écriture JSON via `serde_json` (lisible et diffable). --- -## 24 — Packaging local et CI - -- Script local `.deb`: `scripts/build_deb.sh` (cargo‑deb en release par défaut). -- Release GitHub Actions: artefacts Linux (.tar.gz + .deb) et Windows (.zip). -- Correctif: `cargo deb --no-build` pour réutiliser le binaire déjà compilé. +## 26 — Tests et “poll_once” ---- +`poll_once` exécute un tour synchrone (utile pour tests ou action “rafraîchir maintenant”). -## 25 — Sécurité: menaces et parades - -- Refus HTTP (downgrade, MITM). -- Taille limitant la surface d’attaque DoS. -- Déduplication empêche l’inflation mémoire sur replays. -- Parsing RSS/Atom sous contrôle, pas d’exécution HTML (texte). +Mocks: `wiremock` côté requêtes HTTP (injectable car on utilise `reqwest`). --- -## 26 — Performance +## 27 — Packaging et Release CI -- Streaming réseau; pas de WebView; rendu UI 2D via wgpu/egui. -- Cache articles par feed + pagination. -- Evite copies coûteuses; usage d’`Arc`, `RwLock`, slices. +Local: `scripts/build_deb.sh` (utilise `cargo-deb`). +CI Release: artefacts Linux (.tar.gz + .deb) et Windows (.zip). +Précaution Linux: `cargo deb --no-build` après la compilation pour éviter le double `--release`. --- -## 27 — UX: principes +## 28 — Sécurité élargie (menaces ↔ contre‑mesures) + +- HTTP refusé: évite downgrade/MITM. +- Taille max: réduit le risque DoS. +- Pas d’HTML riche: surface XSS nulle dans l’UI. +- Déduplication: évite re‑push infini d’articles répétés. -- Minimalisme: 3 gestes clés (ajouter, lire, ouvrir). -- Feedback immédiat pour les erreurs (URL, réseau). -- Paramètres sobres, pertinents. +Limites connues: pas de sandbox réseau avancée; confiance dans `reqwest/rustls`. --- -## 28 — Démonstration: add → fetch → read +## 29 — Performance et mémoire -Pseudo‑séquence: +- Download en streaming; pas de copie inutile (Buffers BytesMut → freeze). +- Structures compactes; tri et truncate pour borner la taille des caches. +- UI: wgpu/egui rapide, pas de DOM. -``` -UI (Ajouter) → DataApi.add_feed → poll_once → SeenStore.is_new_and_mark → DataApi.upsert_articles → UI list -``` +Mesure recommandée: profiler `tracing` + `cargo flamegraph` si besoin. --- -## 29 — Dépannage - -- Aucun article: vérifier HTTPS, connectivité, taille flux, logs `RUST_LOG=info`. -- Emojis manquants (Linux): installer `fonts-noto-color-emoji`. -- Fichiers corrompus: les .tmp servent de fallback lecture. - ---- +## 30 — Dépannage & Roadmap -## 30 — Roadmap +Checklist panne: +- Aucun article: vérifier URL HTTPS, connectivité, logs (`RUST_LOG=info`). +- Emojis cassés sous Linux: installer `fonts-noto-color-emoji`. +- JSON corrompu: les `.tmp` servent de secours (recréer si besoin). -- macOS artefacts, .desktop + icône pour Linux. -- Recherche plein‑texte, dossiers/étiquettes. -- Export/Import OPML. -- Internationalisation (i18n) et thèmes pré‑définis. +Roadmap: macOS artefacts, .desktop+icône, recherche plein‑texte, OPML, i18n, thèmes. --- -## Annexes — extraits clés - -### Poller (extrait) +## Annexes — extraits de code clés (références rapides) +### A1. spawn_poller (boucle) ```rust pub fn spawn_poller(/* … */) -> PollerHandle { let (cancel_tx, mut cancel_rx) = broadcast::channel(1); @@ -394,26 +405,21 @@ pub fn spawn_poller(/* … */) -> PollerHandle { } ``` -### Conversion RSS → FeedEntry (extrait) - -```rust -pub fn from_rss_item(feed_id: &str, item: &rss::Item) -> Self { /* auteur, catégorie, content:encoded, enclosure */ } -``` - -### DataApi.upsert_articles (extrait) - +### A2. DataApi.upsert_articles (tri et borne) ```rust slot.sort_by(|a, b| b.published_at.cmp(&a.published_at)); if slot.len() > MAX_PER_FEED { slot.truncate(MAX_PER_FEED); } ``` -### Entrée main.rs (extrait) +### A3. Validation URL en UI +```rust +if parsed.scheme() != "https" { /* feedback et refus */ } +``` +### A4. Entrée main.rs ```rust -eframe::run_native("ReadRSS", NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([800.0, 800.0]) , ..Default::default() }, +eframe::run_native("ReadRSS", NativeOptions { /* viewport */ }, Box::new(move |cc| { install_emoji_friendly_fonts(&cc.egui_ctx); Box::new(RssApp::new(init)) })) ``` ---- - Fin du guide. \ No newline at end of file From 68639f31dfb913be3b6eb2f487e20aa96c7dfe3f Mon Sep 17 00:00:00 2001 From: Anthony Mittica Date: Wed, 29 Oct 2025 12:27:51 +0100 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20enrichir=20chaque=20section=20du=20?= =?UTF-8?q?guide=20avec=20extraits=20de=20code=20et=20explications=20d?= =?UTF-8?q?=C3=A9taill=C3=A9es=20(core,=20UI,=20r=C3=A9seau,=20persistance?= =?UTF-8?q?,=20s=C3=A9curit=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide_technique_ReadRSS.md | 284 ++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) diff --git a/docs/Guide_technique_ReadRSS.md b/docs/Guide_technique_ReadRSS.md index 57117d3..eae1d81 100644 --- a/docs/Guide_technique_ReadRSS.md +++ b/docs/Guide_technique_ReadRSS.md @@ -19,6 +19,19 @@ Lexique: - Poller: tâche périodique qui récupère les flux. - Déduplication: éviter de ré‑annoncer un article déjà vu. +Extrait (contrainte HTTPS côté core): +```rust +// rss-core/src/poller.rs +#[cfg(not(test))] +if url.scheme() != "https" { + let host_ok = matches!( + url.host_str(), + Some("localhost") | Some("127.0.0.1") | Some("::1") + ); + if !host_ok { return Err(PollError::UnsupportedScheme); } +} +``` + --- ## 02 — Carte d’architecture (vue macro) @@ -36,6 +49,19 @@ Réseau (reqwest) → Parsing (rss/atom_syndication) → FeedEntry → SeenStore Dépendances clefs: `tokio` (async), `reqwest` (HTTP, rustls), `rss` et `atom_syndication` (parsing), `serde` (JSON), `egui/eframe` (UI), `tracing` (logs). +Extrait (exports du coeur): +```rust +// rss-core/src/lib.rs +pub mod config; pub mod data; pub mod error; pub mod feed; pub mod poller; pub mod storage; +pub use config::{AppConfig, FeedConfig, ThemeConfig, UiConfig}; +pub use data::DataApi; +pub use error::PollError; +pub use feed::{FeedDescriptor, FeedEntry, SharedFeedList}; +pub use feed::{add_feed, list_feeds, remove_feed, shared_feed_list}; +pub use poller::{poll_once, spawn_poller, Event, PollConfig, PollerHandle}; +pub use storage::SeenStore; +``` + --- ## 03 — Modules (core) et responsabilités @@ -49,6 +75,21 @@ Dépendances clefs: `tokio` (async), `reqwest` (HTTP, rustls), `rss` et `atom_sy Code d’export (`rss-core/src/lib.rs`) pour tout réutiliser côté app. +Extrait (erreurs centralisées): +```rust +// rss-core/src/error.rs +#[derive(Debug, thiserror::Error)] +pub enum PollError { + #[error("network error: {0}")] Network(#[from] reqwest::Error), + #[error("feed parsing error: {0}")] Parse(#[from] rss::Error), + #[error("poller task failed: {0}")] Task(#[from] tokio::task::JoinError), + #[error("update channel closed unexpectedly")] UpdateChannelClosed, + #[error("unsupported URL scheme (https required)")] UnsupportedScheme, + #[error("invalid feed url: {0}")] InvalidUrl(#[from] url::ParseError), + #[error("feed too large: {0} bytes")] TooLarge(u64), +} +``` + --- ## 04 — Lexique minimal Rust et async @@ -61,6 +102,15 @@ Code d’export (`rss-core/src/lib.rs`) pour tout réutiliser côté app. But: comprendre la mécanique sans plonger dans tous les détails bas niveau. +Extrait (partage thread‑safe): +```rust +// rss-core/src/feed.rs +pub type SharedFeedList = Arc>>; +pub fn shared_feed_list(initial: Vec) -> SharedFeedList { + Arc::new(RwLock::new(initial)) +} +``` + --- ## 05 — Configuration: AppConfig (où, quand, comment) @@ -84,6 +134,23 @@ impl AppConfig { pub fn load() -> Self { /* défaut si lecture échoue + auto-sa Dépend: `dirs` (chemin config), `serde`/`serde_json`. Utilisé par: `rss-gui` (thème et sliders), construction de `PollConfig`. +Extraits supplémentaires: +```rust +// rss-core/src/config.rs +pub fn config_file_path() -> Result> { + let config_dir = dirs::config_dir().ok_or("Impossible de trouver le dossier de configuration")?; + let app_config_dir = config_dir.join("readrss"); + std::fs::create_dir_all(&app_config_dir)?; + Ok(app_config_dir.join("config.json")) +} + +pub fn save(&self) -> Result<(), Box> { + let config_path = Self::config_file_path()?; + let config_json = serde_json::to_string_pretty(self)?; + std::fs::write(config_path, config_json)?; Ok(()) +} +``` + --- ## 06 — Données: FeedDescriptor et FeedEntry (schémas) @@ -105,6 +172,25 @@ impl FeedEntry { } ``` +Conversions concrètes: +```rust +// rss-core/src/feed.rs +pub fn from_rss_item(feed_id: &str, item: &rss::Item) -> Self { + let published_at = item.pub_date() + .and_then(|v| DateTime::parse_from_rfc2822(v).ok()).map(|dt| dt.with_timezone(&Utc)); + let author = item.dublin_core_ext().and_then(|dc| dc.creators().first().cloned()) + .or_else(|| item.author().map(|s| s.to_string())); + let category = item.categories().first().map(|c| c.name().to_string()) + .or_else(|| item.dublin_core_ext().and_then(|dc| dc.subjects().first().cloned())); + let content_html = item.extensions().get("content").and_then(|m| m.get("encoded")) + .and_then(|v| v.first()).and_then(|ext| ext.value.clone()); + let image_url = item.enclosure().map(|e| e.url().to_string()); + Self { /* … champs remplis … */ feed_id: feed_id.to_owned(), title: item.title().unwrap_or_default().to_owned(), + summary: item.description().map(ToOwned::to_owned), url: item.link().unwrap_or_default().to_owned(), + published_at, guid: item.guid().map(|g| g.value().to_owned()), author, category, content_html, image_url } +} +``` + --- ## 07 — Persistance des données: DataApi (contrats) @@ -123,6 +209,32 @@ Contrats fonctionnels: Note: écriture atomique via fichier `.tmp` puis `rename()`. +Extrait (écriture atomique): +```rust +// rss-core/src/data.rs +async fn persist_feeds(&self) { + let feeds = list_feeds(&self.feeds).await; + if let Ok(bytes) = serde_json::to_vec_pretty(&feeds) { + if let Some(parent) = self.feeds_path.parent() { let _ = tokio::fs::create_dir_all(parent).await; } + let tmp = self.feeds_path.with_extension("json.tmp"); + let _ = tokio::fs::write(&tmp, &bytes).await; let _ = tokio::fs::rename(&tmp, &self.feeds_path).await; } +} +``` + +Extrait (fusion d’articles): +```rust +pub async fn upsert_articles(&self, feed_id: &str, entries: Vec) { + const MAX_PER_FEED: usize = 300; + let mut inner = self.articles_inner.write().await; + let slot = inner.entry(feed_id.to_string()).or_default(); + let mut existing: HashSet = slot.iter().map(|e| e.identity()).collect(); + for e in entries { if existing.insert(e.identity()) { slot.push(e); } } + slot.sort_by(|a,b| b.published_at.cmp(&a.published_at)); + if slot.len() > MAX_PER_FEED { slot.truncate(MAX_PER_FEED); } + drop(inner); self.persist_articles().await; +} +``` + --- ## 08 — Déduplication persistée: SeenStore @@ -136,6 +248,20 @@ Contrat: Structure de données: `HashMap>` sérialisé en JSON. +Extrait: +```rust +// rss-core/src/storage.rs +pub async fn is_new_and_mark(&self, entry: &FeedEntry) -> bool { + let key = entry.identity(); + let feed_id = entry.feed_id.clone(); + let mut inner = self.inner.write().await; + let set = inner.seen.entry(feed_id).or_default(); + if set.contains(&key) { false } else { + set.insert(key); drop(inner); let _ = self.persist().await; true + } +} +``` + --- ## 09 — Réseau et sécurité: fetch_feed (politique) @@ -153,6 +279,19 @@ Extrait de contrôle de schéma: if url.scheme() != "https" { /* autorise localhost/127.0.0.1/::1 sinon UnsupportedScheme */ } ``` +Extrait (streaming et limite 10 MiB): +```rust +// rss-core/src/poller.rs +const MAX_FEED_BYTES: usize = 10 * 1024 * 1024; +let response = client.get(url).timeout(timeout).send().await?; +if let Some(len) = response.content_length() { if len > MAX_FEED_BYTES as u64 { return Err(PollError::TooLarge(len)); } } +let mut bytes_buf = bytes::BytesMut::new(); let mut stream = response.bytes_stream(); +while let Some(chunk) = stream.next().await { + let chunk = chunk?; if bytes_buf.len() + chunk.len() > MAX_FEED_BYTES { return Err(PollError::TooLarge((bytes_buf.len()+chunk.len()) as u64)); } + bytes_buf.extend_from_slice(&chunk); +} +``` + --- ## 10 — Parsing: d’abord RSS, puis fallback Atom @@ -170,6 +309,16 @@ match rss::Channel::read_from(&mut cursor_rss) { } ``` +Extrait (normalisation des dates): +```rust +// rss-core/src/poller.rs +let entries = channel.items().iter().map(|item| { + let mut entry = FeedEntry::from_rss_item(&feed.id, item); + if entry.published_at.is_none() { entry.published_at = Some(Utc::now()); } + entry +}).collect(); +``` + --- ## 11 — PollConfig et backoff (retry exponentiel) @@ -186,6 +335,14 @@ let backoff = cfg.retry_backoff_ms * (1u64 << (attempt - 1)); tokio::time::sleep(Duration::from_millis(backoff)).await; ``` +Définition et valeurs par défaut: +```rust +// rss-core/src/poller.rs +#[derive(Debug, Clone)] +pub struct PollConfig { pub interval: Duration, pub request_timeout: Duration, pub max_retries: usize, pub retry_backoff_ms: u64 } +impl Default for PollConfig { fn default() -> Self { Self { interval: Duration::from_secs(300), request_timeout: Duration::from_secs(15), max_retries: 3, retry_backoff_ms: 500 } } } +``` + --- ## 12 — Tâche de polling: spawn_poller (concurrence) @@ -197,6 +354,19 @@ Mécanique: Contrats d’erreur: toute erreur de réseau/parsing est loggée, pas fatale. +Extrait: +```rust +pub fn spawn_poller(/*…*/) -> PollerHandle { + let (cancel_tx, mut cancel_rx) = broadcast::channel(1); + let join = tokio::spawn(async move { + let mut ticker = tokio::time::interval(config.interval); + ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { tokio::select! { _ = cancel_rx.recv() => break, _ = ticker.tick() => { /* boucle feeds + fetch */ } } } + }); + PollerHandle { cancel_tx, join } +} +``` + --- ## 13 — Évènements: Event::NewArticles @@ -227,6 +397,20 @@ let poller = spawn_poller(feeds.clone(), poll_config.clone(), client, update_tx, eframe::run_native("ReadRSS", NativeOptions { /* … */ }, Box::new(move |_| Box::new(RssApp::new(init)))) ``` +Autres extraits utiles: +```rust +// rss-gui/src/main.rs +let client = ClientBuilder::new().redirect(redirect::Policy::limited(5)) + .user_agent("ReadRSS/0.1 (+https://github.com/xAMA0x/ReadRSS)").build()?; + +fn load_poll_config() -> PollConfig { + let app_cfg = AppConfig::load(); + PollConfig { interval: Duration::from_secs(app_cfg.feeds.update_interval_minutes.max(1) * 60), + request_timeout: Duration::from_secs(app_cfg.feeds.request_timeout_seconds.max(1)), + max_retries: app_cfg.feeds.retry_attempts.max(1) as usize, ..PollConfig::default() } +} +``` + --- ## 15 — Architecture UI: vues et navigation @@ -255,6 +439,15 @@ style.visuals.widgets.active.bg_fill = accent_color; ctx.set_style(style); ``` +Extrait complet (sélection): +```rust +// rss-gui/src/app.rs +style.visuals.widgets.hovered.bg_stroke = Stroke::new(1.0, accent_color); +style.visuals.selection.bg_fill = Color32::from_rgba_unmultiplied(0,122,204,60); +style.spacing.item_spacing = egui::vec2(10.0, 8.0); +style.visuals.widgets.noninteractive.rounding = Rounding::same(3.0); +``` + --- ## 17 — Ajout d’un flux: validation et feedback @@ -266,6 +459,15 @@ Extrait: if parsed.scheme() != "https" { self.add_feedback = Some((false, "Seules les URLs HTTPS…".into())); } ``` +Extrait (ajout + rafraîchissement): +```rust +// rss-gui/src/app.rs +let descriptor = FeedDescriptor { id, title: title_owned_or_url, url: url_owned.clone() }; +self.runtime.block_on(self.data_api.add_feed(descriptor.clone())); +let events = self.runtime.block_on(async { poll_once(&[descriptor], &self.poll_config, &self.client, &self.seen_store).await }); +for evt in events { if let Event::NewArticles(feed_id, mut entries) = evt { self.runtime.block_on(self.data_api.upsert_articles(&feed_id, entries.clone())); self.articles.append(&mut entries); } } +``` + --- ## 18 — Discover: recommandations prêtes à suivre @@ -283,6 +485,16 @@ Fonctions: - Tri par date décroissante, pagination via `articles_per_page`. - “Non lus” uniquement (en s’appuyant sur `DataApi.is_read`). +Extraits: +```rust +// Filtre Non lus +if self.show_unread_only && self.runtime.block_on(self.data_api.is_read(&article)) { continue; } + +// Aperçu texte depuis HTML/summary +let preview_text = if let Some(html) = &article.content_html { html2text::from_read(html.as_bytes(), 100) } + else if let Some(summary) = &article.summary { html2text::from_read(summary.as_bytes(), 100) } else { String::new() }; +``` + --- ## 20 — Détail d’un article et actions @@ -292,6 +504,12 @@ Actions: Ouvrir dans le navigateur (mise en page native), Copier le lien. Sécurité: l’UI ne rend pas du HTML riche (pas de WebView), donc pas d’exécution de scripts. +Extrait: +```rust +if ui.button("Ouvrir dans le navigateur").clicked() { let _ = webbrowser::open(&article.url); } +if ui.button("Copier le lien").clicked() { ui.output_mut(|o| o.copied_text = article.url.clone()); } +``` + --- ## 21 — Paramètres: thème, interface et flux @@ -299,6 +517,14 @@ Sécurité: l’UI ne rend pas du HTML riche (pas de WebView), donc pas d’exé Sauvegarde immédiate: chaque slider/checkbox écrit le JSON. Impact: thème appliqué à chaud; paramètres des feeds pris en compte à la relance (ou conversion vers `PollConfig` dès l’entrée). +Extrait (sauvegarde immédiate): +```rust +if ui.color_edit_button_rgb(&mut bg).changed() { + self.config.theme.background_color = [(bg[0]*255.0) as u8, (bg[1]*255.0) as u8, (bg[2]*255.0) as u8]; + let _ = self.config.save(); +} +``` + --- ## 22 — Concurrence et canaux (modèle mental) @@ -309,6 +535,17 @@ Impact: thème appliqué à chaud; paramètres des feeds pris en compte à la re Avantage: découplage réseau/UI, robustesse, simplicité de debug. +Extrait (consommation des évènements): +```rust +while let Ok(evt) = self.updates.try_recv() { + if let Event::NewArticles(feed_id, mut entries) = evt { + self.runtime.block_on(self.data_api.upsert_articles(&feed_id, entries.clone())); + self.articles.append(&mut entries); + self.articles.sort_by(|a,b| b.published_at.cmp(&a.published_at)); + } +} +``` + --- ## 23 — Robustesse: limites et timeouts @@ -317,6 +554,12 @@ Pourquoi 10 MiB? Éviter les flux anormalement gros (DoS mémoire/temps). Pourquoi des retries? L’Internet est faillible; on retente avec backoff exponentiel. Pourquoi `MissedTickBehavior::Skip`? On ne rattrape pas un retard si l’app a été gelée (préserve la réactivité). +Extrait: +```rust +let mut ticker = tokio::time::interval(config.interval); +ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); +``` + --- ## 24 — Gestion des erreurs et logs @@ -330,6 +573,11 @@ Extrait: pub enum PollError { Network(#[from] reqwest::Error), Parse(#[from] rss::Error), /* … */ } ``` +Extrait (logging côté poller): +```rust +warn!(feed = %feed.url, error = %err, "failed to fetch feed"); +``` + --- ## 25 — Formats et chemins de persistance @@ -340,6 +588,11 @@ Fichiers côté utilisateur: Lecture/écriture JSON via `serde_json` (lisible et diffable). +Extrait (chemin config, côté GUI): +```rust +fn config_dir() -> PathBuf { let mut dir = dirs::config_dir().unwrap_or_else(|| std::env::current_dir().unwrap()); dir.push("readrss"); dir } +``` + --- ## 26 — Tests et “poll_once” @@ -348,6 +601,18 @@ Lecture/écriture JSON via `serde_json` (lisible et diffable). Mocks: `wiremock` côté requêtes HTTP (injectable car on utilise `reqwest`). +Extrait (API de test synchronisable): +```rust +// rss-core/src/poller.rs +pub async fn poll_once(feeds: &[FeedDescriptor], cfg: &PollConfig, client: &Client, seen: &SeenStore) -> Vec { + let mut out = Vec::new(); + for feed in feeds { if let Ok(mut entries) = fetch_feed_with_retries(client, feed, cfg).await { + let mut new_entries = Vec::new(); for e in entries { if seen.is_new_and_mark(&e).await { new_entries.push(e); } } + if !new_entries.is_empty() { out.push(Event::NewArticles(feed.id.clone(), new_entries)); } + }} out +} +``` + --- ## 27 — Packaging et Release CI @@ -356,6 +621,13 @@ Local: `scripts/build_deb.sh` (utilise `cargo-deb`). CI Release: artefacts Linux (.tar.gz + .deb) et Windows (.zip). Précaution Linux: `cargo deb --no-build` après la compilation pour éviter le double `--release`. +Extrait (workflow): +```yaml +# .github/workflows/release.yml (extrait) +- name: Build .deb + run: cargo deb -p rss-gui --no-build +``` + --- ## 28 — Sécurité élargie (menaces ↔ contre‑mesures) @@ -367,6 +639,11 @@ Précaution Linux: `cargo deb --no-build` après la compilation pour éviter le Limites connues: pas de sandbox réseau avancée; confiance dans `reqwest/rustls`. +Extrait (refus HTTP côté UI aussi): +```rust +if parsed.scheme() != "https" { self.add_feedback = Some((false, "Seules les URLs HTTPS sont autorisées".to_string())); return; } +``` + --- ## 29 — Performance et mémoire @@ -377,6 +654,13 @@ Limites connues: pas de sandbox réseau avancée; confiance dans `reqwest/rustls Mesure recommandée: profiler `tracing` + `cargo flamegraph` si besoin. +Extrait (BytesMut → freeze): +```rust +let mut bytes_buf = bytes::BytesMut::new(); +// … remplissage … +let bytes = bytes_buf.freeze(); +``` + --- ## 30 — Dépannage & Roadmap From 53e6b4f6a47b28147f181e542cf31b9792f0c0fc Mon Sep 17 00:00:00 2001 From: Anthony Mittica Date: Wed, 29 Oct 2025 12:36:22 +0100 Subject: [PATCH 4/4] =?UTF-8?q?docs(guide):=20ajouter=20d=C3=A9cryptage=20?= =?UTF-8?q?apr=C3=A8s=20chaque=20extrait,=202=20sc=C3=A9narios=20fil=20rou?= =?UTF-8?q?ge=20et=20glossaire=20d=C3=A9butant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Guide_technique_ReadRSS.md | 111 ++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/docs/Guide_technique_ReadRSS.md b/docs/Guide_technique_ReadRSS.md index eae1d81..b95c2ab 100644 --- a/docs/Guide_technique_ReadRSS.md +++ b/docs/Guide_technique_ReadRSS.md @@ -31,6 +31,10 @@ if url.scheme() != "https" { if !host_ok { return Err(PollError::UnsupportedScheme); } } ``` +Décryptage simple: +- `#[cfg(not(test))]` signifie: on applique cette règle partout sauf dans les tests. Utile pour autoriser des URLs locales en tests. +- `url.scheme()` récupère le schéma (http, https…). On exige `https` en production. +- `matches!` vérifie que l’hôte est local. Si ce n’est pas le cas, on renvoie une erreur claire `UnsupportedScheme` qui sera loggée sans faire planter l’appli. --- @@ -61,6 +65,9 @@ pub use feed::{add_feed, list_feeds, remove_feed, shared_feed_list}; pub use poller::{poll_once, spawn_poller, Event, PollConfig, PollerHandle}; pub use storage::SeenStore; ``` +Décryptage simple: +- `pub mod` rend les modules visibles; `pub use` ré-exporte des types/fonctions pour une importation facile côté UI. +- L’app (`rss-gui`) peut ainsi écrire `use rss_core::spawn_poller;` sans connaître l’arborescence interne. --- @@ -89,6 +96,9 @@ pub enum PollError { #[error("feed too large: {0} bytes")] TooLarge(u64), } ``` +Décryptage simple: +- Chaque variante représente une cause d’échec précise. `#[from]` permet la conversion automatique depuis une erreur tierce. +- Les messages entre guillemets seront affichés tels quels dans les logs ou l’UI si besoin. --- @@ -110,6 +120,9 @@ pub fn shared_feed_list(initial: Vec) -> SharedFeedList { Arc::new(RwLock::new(initial)) } ``` +Décryptage simple: +- `Arc>` = partage multi‑threads + plusieurs lecteurs ou un seul écrivain. +- La liste des flux est donc sûre en concurrence (poller/GUI) avec peu de contention. --- @@ -150,6 +163,10 @@ pub fn save(&self) -> Result<(), Box> { std::fs::write(config_path, config_json)?; Ok(()) } ``` +Décryptage simple: +- `dirs::config_dir()` retourne le dossier config de l’OS (ex: `~/.config` sous Linux). +- On crée le sous‑dossier `readrss` au besoin et on y place `config.json` lisible par l’utilisateur. +- `serde_json::to_string_pretty` produit un JSON facile à éditer à la main. --- @@ -190,6 +207,10 @@ pub fn from_rss_item(feed_id: &str, item: &rss::Item) -> Self { published_at, guid: item.guid().map(|g| g.value().to_owned()), author, category, content_html, image_url } } ``` +Décryptage simple: +- On tente d’abord `pub_date` (format RFC 2822) et on convertit la timezone vers UTC. +- Métadonnées supplémentaires via extensions (Dublin Core, content:encoded, enclosure image). +- Les `Option` évitent les `null`/paniques: si une info manque, on ne casse rien. --- @@ -200,6 +221,8 @@ Chemin: `rss-core/src/data.rs` Responsabilités: - Feeds: ajouter/supprimer/lister avec persistance (`feeds.json`). - Read‑state: marquer “lu” (`read_store.json`). +Décryptage simple: +- Écriture atomique: on écrit d’abord un fichier temporaire `.tmp`, puis on le renomme. En cas de coupure, on évite un fichier final corrompu. - Articles: cache par feed (`articles_store.json`), déduplication + tri + truncate. Contrats fonctionnels: @@ -213,6 +236,9 @@ Extrait (écriture atomique): ```rust // rss-core/src/data.rs async fn persist_feeds(&self) { +Décryptage simple: +- `existing` garde les clés déjà présentes pour éviter les doublons. +- Tri décroissant par date; on tronque pour maîtriser la taille disque/mémoire. let feeds = list_feeds(&self.feeds).await; if let Ok(bytes) = serde_json::to_vec_pretty(&feeds) { if let Some(parent) = self.feeds_path.parent() { let _ = tokio::fs::create_dir_all(parent).await; } @@ -261,6 +287,9 @@ pub async fn is_new_and_mark(&self, entry: &FeedEntry) -> bool { } } ``` +Décryptage simple: +- “Vu ?” Si non: on ajoute la clé et on persiste. Si oui: on ne republie pas l’article à l’UI. +- La persistance est asynchrone; en cas d’échec, on logge mais on ne bloque pas l’UI. --- @@ -291,6 +320,9 @@ while let Some(chunk) = stream.next().await { bytes_buf.extend_from_slice(&chunk); } ``` +Décryptage simple: +- On lit par morceaux (streaming) pour ne pas exploser la mémoire. +- Double barrière: longueur annoncée ET vérification cumulée en cours de lecture. --- @@ -318,6 +350,8 @@ let entries = channel.items().iter().map(|item| { entry }).collect(); ``` +Décryptage simple: +- Certains flux n’ont pas de date fiable; on complète raisonnablement par “maintenant” pour garantir un tri cohérent. --- @@ -342,6 +376,8 @@ Définition et valeurs par défaut: pub struct PollConfig { pub interval: Duration, pub request_timeout: Duration, pub max_retries: usize, pub retry_backoff_ms: u64 } impl Default for PollConfig { fn default() -> Self { Self { interval: Duration::from_secs(300), request_timeout: Duration::from_secs(15), max_retries: 3, retry_backoff_ms: 500 } } } ``` +Décryptage simple: +- Des valeurs prudentes par défaut: 5 minutes d’intervalle, 15 s de timeout, 3 tentatives, backoff base 500 ms. --- @@ -366,6 +402,9 @@ pub fn spawn_poller(/*…*/) -> PollerHandle { PollerHandle { cancel_tx, join } } ``` +Décryptage simple: +- `broadcast` sert à signaler l’arrêt à la tâche. `join` attend la fin propre de la tâche (utile à la fermeture). +- `select!` écoute soit l’arrêt, soit l’horloge périodique. --- @@ -410,6 +449,9 @@ fn load_poll_config() -> PollConfig { max_retries: app_cfg.feeds.retry_attempts.max(1) as usize, ..PollConfig::default() } } ``` +Décryptage simple: +- On limite les redirections HTTP (sécurité/perf) et on définit un user‑agent clair. +- `PollConfig` dérive des préférences utilisateur (UI ↔ runtime alignés). --- @@ -447,6 +489,8 @@ style.visuals.selection.bg_fill = Color32::from_rgba_unmultiplied(0,122,204,60); style.spacing.item_spacing = egui::vec2(10.0, 8.0); style.visuals.widgets.noninteractive.rounding = Rounding::same(3.0); ``` +Décryptage simple: +- On améliore la lisibilité: états visuels cohérents (hover/active/sélection), espacements confortables, arrondis discrets. --- @@ -467,6 +511,9 @@ self.runtime.block_on(self.data_api.add_feed(descriptor.clone())); let events = self.runtime.block_on(async { poll_once(&[descriptor], &self.poll_config, &self.client, &self.seen_store).await }); for evt in events { if let Event::NewArticles(feed_id, mut entries) = evt { self.runtime.block_on(self.data_api.upsert_articles(&feed_id, entries.clone())); self.articles.append(&mut entries); } } ``` +Décryptage simple: +- Après ajout, on force un “mini polling” du seul nouveau flux (`poll_once`). +- On persiste immédiatement les articles et on les affiche triés dans l’UI. --- @@ -494,6 +541,9 @@ if self.show_unread_only && self.runtime.block_on(self.data_api.is_read(&article let preview_text = if let Some(html) = &article.content_html { html2text::from_read(html.as_bytes(), 100) } else if let Some(summary) = &article.summary { html2text::from_read(summary.as_bytes(), 100) } else { String::new() }; ``` +Décryptage simple: +- Le filtre “Non lus” interroge l’état persistant (`DataApi.is_read`). +- `html2text` rend les aperçus lisibles et sûrs (texte brut, pas de scripts). --- @@ -509,6 +559,9 @@ Extrait: if ui.button("Ouvrir dans le navigateur").clicked() { let _ = webbrowser::open(&article.url); } if ui.button("Copier le lien").clicked() { ui.output_mut(|o| o.copied_text = article.url.clone()); } ``` +Décryptage simple: +- On délègue l’affichage enrichi au navigateur par défaut; l’UI reste simple et sûre. +- Copier le lien utilise le presse‑papiers géré par egui. --- @@ -524,6 +577,8 @@ if ui.color_edit_button_rgb(&mut bg).changed() { let _ = self.config.save(); } ``` +Décryptage simple: +- Tout changement UI est immédiatement sauvegardé en JSON — pas de bouton “Enregistrer”. --- @@ -545,6 +600,8 @@ while let Ok(evt) = self.updates.try_recv() { } } ``` +Décryptage simple: +- `try_recv` lit sans bloquer la boucle de rendu; l’UI reste fluide même si le poller publie beaucoup. --- @@ -559,6 +616,8 @@ Extrait: let mut ticker = tokio::time::interval(config.interval); ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); ``` +Décryptage simple: +- Si l’app a “raté” un tick (PC occupé/suspendu), on n’enchaîne pas des dizaines de rattrapages. --- @@ -577,6 +636,8 @@ Extrait (logging côté poller): ```rust warn!(feed = %feed.url, error = %err, "failed to fetch feed"); ``` +Décryptage simple: +- `tracing::warn!` enregistre un message structuré: on voit l’URL du flux et l’erreur précise. --- @@ -592,6 +653,8 @@ Extrait (chemin config, côté GUI): ```rust fn config_dir() -> PathBuf { let mut dir = dirs::config_dir().unwrap_or_else(|| std::env::current_dir().unwrap()); dir.push("readrss"); dir } ``` +Décryptage simple: +- Si `dirs::config_dir()` n’est pas dispo (cas rare), on utilise le répertoire courant — l’app reste utilisable. --- @@ -612,6 +675,8 @@ pub async fn poll_once(feeds: &[FeedDescriptor], cfg: &PollConfig, client: &Clie }} out } ``` +Décryptage simple: +- Utile pour un “rafraîchir maintenant” sans lancer le poller complet: un tour, des évènements, terminé. --- @@ -627,6 +692,8 @@ Extrait (workflow): - name: Build .deb run: cargo deb -p rss-gui --no-build ``` +Décryptage simple: +- `--no-build` réutilise le binaire déjà compilé (évite l’erreur de double `--release`). --- @@ -643,6 +710,8 @@ Extrait (refus HTTP côté UI aussi): ```rust if parsed.scheme() != "https" { self.add_feedback = Some((false, "Seules les URLs HTTPS sont autorisées".to_string())); return; } ``` +Décryptage simple: +- Filtre côté UI également: cohérence d’expérience (on bloque tôt, avec message clair). --- @@ -660,6 +729,48 @@ let mut bytes_buf = bytes::BytesMut::new(); // … remplissage … let bytes = bytes_buf.freeze(); ``` +Décryptage simple: +- `freeze()` convertit le buffer mutable en `Bytes` immuable sans recopier les données — performant et sûr. + +--- + +## Scénarios fil rouge (du clic utilisateur au rendu) + +### Scénario 1 — J’ajoute un flux et je vois des articles + +1) UI: l’utilisateur saisit l’URL et clique “Ajouter”. + - Validation: refus des URLs non‑HTTPS; feedback immédiat. +2) Données: `DataApi.add_feed` persiste le flux dans `feeds.json`. +3) Rafraîchissement: l’app appelle `poll_once` pour ce flux uniquement. + - Réseau: `reqwest` télécharge le flux en streaming (≤ 10 MiB, timeout). + - Parsing: RSS, sinon fallback Atom. + - Déduplication: `SeenStore.is_new_and_mark` ne garde que les nouveaux. +4) Persistance: `DataApi.upsert_articles` fusionne, trie et sauve `articles_store.json`. +5) UI: réception `Event::NewArticles` → ajout dans la liste, tri par date, affichage. + +Résultat tangible: l’utilisateur voit des articles quelques secondes après l’ajout. + +### Scénario 2 — Je lance l’application le matin + +1) Démarrage: `AppConfig::load` charge les préférences; `DataApi::load_from_dir` restaure feeds, “lus” et cache d’articles. +2) Services: on crée le client HTTP; `load_poll_config` dérive `PollConfig` depuis `AppConfig`. +3) Poller: `spawn_poller` démarre la tâche périodique (intervalle choisi par l’utilisateur). +4) Première passe: à chaque tick, fetch des flux, déduplication, persistance; l’UI consomme les évènements sans bloquer. +5) Utilisation: l’utilisateur ouvre un article (navigateur), marque “tout lu”, ajuste le thème; tout est sauvegardé immédiatement. + +--- + +## Glossaire (rapide et utile) + +- Arc: compteur de références thread‑safe; partage d’un même objet entre threads. +- RwLock: verrou lecture/écriture (plusieurs lecteurs OU un écrivain). +- async/await: style d’écriture pour code non bloquant; garde l’UI fluide. +- mpsc/broadcast: canaux asynchrones (point‑à‑point / diffusion à plusieurs). +- serde/serde_json: sérialisation/désérialisation Rust ↔ JSON. +- reqwest: client HTTP asynchrone, ici avec rustls. +- egui/eframe: toolkit UI immédiat sur wgpu (rendu GPU), portable. +- backoff exponentiel: on attend de plus en plus longtemps entre les tentatives après un échec. +- écriture atomique: écriture dans un fichier temporaire puis renommage, pour éviter les données corrompues. ---