diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..7793a8879 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: AI-Pulse Reader + url: https://thephoenixagency.github.io/AI-Pulse/app.html + about: Accedez au reader en ligne diff --git a/.github/ISSUE_TEMPLATE/new-source.yml b/.github/ISSUE_TEMPLATE/new-source.yml new file mode 100644 index 000000000..e89574b7e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-source.yml @@ -0,0 +1,65 @@ +name: Proposer une nouvelle source / Suggest a new source +description: Proposez une source RSS a ajouter a AI-Pulse +title: "[SOURCE] " +labels: ["new-source"] +body: + - type: input + id: source_name + attributes: + label: Nom de la source / Source Name + placeholder: "Ex: Korben, TechCrunch..." + validations: + required: true + - type: input + id: source_url + attributes: + label: URL du site / Website URL + placeholder: "https://example.com" + validations: + required: true + - type: input + id: rss_url + attributes: + label: URL du flux RSS / RSS Feed URL + placeholder: "https://example.com/feed/" + validations: + required: true + - type: dropdown + id: category + attributes: + label: Categorie / Category + options: + - AI / IA + - Cybersecurity / Cybersecurite + - IoT + - Windows + - Mac / Apple + - Linux + - Tech Generale + - Entrepreneuriat / Entrepreneurship + - Bourse & Finance + - Crypto & Blockchain + - Open Source & GitHub + - Produits & Innovation + validations: + required: true + - type: dropdown + id: language + attributes: + label: Langue / Language + options: + - Francais / French + - Anglais / English + - Autre / Other + validations: + required: true + - type: input + id: tags + attributes: + label: Tags / Mots-cles + placeholder: "Ex: tech, security, AI..." + - type: textarea + id: reason + attributes: + label: Pourquoi cette source? / Why this source? + placeholder: "Contenu original, source fiable..." diff --git a/.github/ISSUE_TEMPLATE/subscribe.yml b/.github/ISSUE_TEMPLATE/subscribe.yml new file mode 100644 index 000000000..2177d7cd6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/subscribe.yml @@ -0,0 +1,48 @@ +name: S'abonner a AI-Pulse / Subscribe to AI-Pulse +description: Recevez votre digest personnalise par email +title: "[SUBSCRIBE] " +labels: ["subscription"] +body: + - type: input + id: email + attributes: + label: Votre email / Your email + placeholder: "you@example.com" + validations: + required: true + - type: checkboxes + id: categories + attributes: + label: Categories souhaitees / Desired categories + options: + - label: AI / IA + - label: Cybersecurity + - label: IoT + - label: Windows + - label: Mac / Apple + - label: Linux + - label: Tech Generale + - label: Entrepreneuriat + - label: Bourse & Finance + - label: Crypto & Blockchain + - label: Open Source & GitHub + - label: Produits & Innovation + - type: dropdown + id: language + attributes: + label: Langue preferee / Preferred language + options: + - Francais + - English + - Les deux / Both + validations: + required: true + - type: dropdown + id: frequency + attributes: + label: Frequence / Frequency + options: + - Chaque mise a jour (toutes les 3h) / Every update + - Quotidien / Daily digest + validations: + required: true diff --git a/.github/workflows/add-source.yml b/.github/workflows/add-source.yml new file mode 100644 index 000000000..01d08173b --- /dev/null +++ b/.github/workflows/add-source.yml @@ -0,0 +1,122 @@ +name: Auto-add approved source + +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + +jobs: + add-source: + if: github.event.label.name == 'source-approved' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse issue and add source + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const body = issue.body; + + // Parse form fields from issue body + function getField(label) { + const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i'); + const match = body.match(regex); + return match ? match[1].trim() : ''; + } + + const sourceName = getField('Nom de la source / Source Name'); + const sourceUrl = getField('URL du site / Website URL'); + const rssUrl = getField('URL du flux RSS / RSS Feed URL'); + const categoryRaw = getField('Categorie / Category'); + const languageRaw = getField('Langue / Language'); + const tags = getField('Tags / Mots-cles'); + + if (!sourceName || !rssUrl) { + console.log('Missing required fields'); + return; + } + + // Map category + const catMap = { + 'AI / IA': 'ai', + 'Cybersecurity / Cybersecurite': 'cybersecurity', + 'IoT': 'iot', + 'Windows': 'windows', + 'Mac / Apple': 'mac', + 'Linux': 'linux', + 'Tech Generale': 'tech', + 'Entrepreneuriat / Entrepreneurship': 'entrepreneurship', + 'Bourse & Finance': 'finance', + 'Crypto & Blockchain': 'crypto', + 'Open Source & GitHub': 'opensource', + 'Produits & Innovation': 'products' + }; + const category = catMap[categoryRaw] || 'tech'; + + // Map language + const lang = languageRaw.includes('Francais') ? 'fr' : 'en'; + + // Parse tags + const tagList = tags ? tags.split(',').map(t => t.trim()).filter(t => t) : [category]; + + // Read and update config.json + const config = JSON.parse(fs.readFileSync('config.json', 'utf-8')); + if (!config.categories[category]) { + console.log(`Category ${category} not found`); + return; + } + + // Check for duplicates + const exists = config.categories[category].feeds.some(f => f.url === rssUrl); + if (exists) { + console.log('Source already exists'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `This source (${rssUrl}) already exists in the ${category} category.` + }); + return; + } + + // Add new feed + config.categories[category].feeds.push({ + name: sourceName, + url: rssUrl, + tags: tagList, + lang: lang + }); + + fs.writeFileSync('config.json', JSON.stringify(config, null, 2)); + + // Comment on issue + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `Source "${sourceName}" added to category "${category}" in config.json. It will be active at the next aggregation cycle.` + }); + + // Close issue + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + - name: Commit changes + run: | + git config --global user.name 'PhoenixProject-AutoSync' + git config --global user.email '${{ secrets.GIT_AUTHOR_EMAIL }}' + git add config.json + if ! git diff --cached --exit-code; then + git commit -m "Add new source from issue #${{ github.event.issue.number }}" + git push + fi diff --git a/.github/workflows/manage-subscriber.yml b/.github/workflows/manage-subscriber.yml new file mode 100644 index 000000000..34ada499b --- /dev/null +++ b/.github/workflows/manage-subscriber.yml @@ -0,0 +1,122 @@ +name: Manage subscriber + +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + +jobs: + add-subscriber: + if: github.event.label.name == 'subscription' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse and add subscriber + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const body = issue.body; + + function getField(label) { + const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i'); + const match = body.match(regex); + return match ? match[1].trim() : ''; + } + + function getCheckboxes(label) { + const regex = new RegExp(`### ${label}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = body.match(regex); + if (!match) return []; + return [...match[1].matchAll(/- \[X\] (.+)/gi)].map(m => m[1].trim()); + } + + const email = getField('Votre email / Your email'); + if (!email || !email.includes('@')) { + console.log('Invalid email'); + return; + } + + const selectedCats = getCheckboxes('Categories souhaitees / Desired categories'); + const langRaw = getField('Langue preferee / Preferred language'); + const freqRaw = getField('Frequence / Frequency'); + + const catMap = { + 'AI / IA': 'ai', + 'Cybersecurity': 'cybersecurity', + 'IoT': 'iot', + 'Windows': 'windows', + 'Mac / Apple': 'mac', + 'Linux': 'linux', + 'Tech Generale': 'tech', + 'Entrepreneuriat': 'entrepreneurship', + 'Bourse & Finance': 'finance', + 'Crypto & Blockchain': 'crypto', + 'Open Source & GitHub': 'opensource', + 'Produits & Innovation': 'products' + }; + + const categories = selectedCats.map(c => catMap[c]).filter(Boolean); + const lang = langRaw.includes('Francais') ? 'fr' : langRaw.includes('English') ? 'en' : 'all'; + const frequency = freqRaw.includes('Quotidien') ? 'daily' : 'each_update'; + + // Read subscribers + const subsPath = 'data/subscribers.json'; + let subscribers = []; + try { + subscribers = JSON.parse(fs.readFileSync(subsPath, 'utf-8')); + } catch (e) { + subscribers = []; + } + + // Check duplicate + const exists = subscribers.some(s => s.email === email); + if (exists) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `This email is already subscribed. To update preferences, please unsubscribe first and resubscribe.` + }); + return; + } + + // Add subscriber + subscribers.push({ + email, + categories: categories.length > 0 ? categories : Object.values(catMap), + lang, + frequency, + subscribed_at: new Date().toISOString().split('T')[0] + }); + + fs.writeFileSync(subsPath, JSON.stringify(subscribers, null, 2)); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `Welcome! You've been subscribed to AI-Pulse digest.\n\nCategories: ${categories.join(', ') || 'All'}\nLanguage: ${lang}\nFrequency: ${frequency}\n\nYou will receive your personalized digest at your next update cycle.` + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + - name: Commit changes + run: | + git config --global user.name 'PhoenixProject-AutoSync' + git config --global user.email '${{ secrets.GIT_AUTHOR_EMAIL }}' + git add data/subscribers.json + if ! git diff --cached --exit-code; then + git commit -m "Add subscriber from issue #${{ github.event.issue.number }}" + git push + fi diff --git a/.github/workflows/update-ai-pulse.yml b/.github/workflows/update-ai-pulse.yml index 6fd4d1596..a51d7d307 100644 --- a/.github/workflows/update-ai-pulse.yml +++ b/.github/workflows/update-ai-pulse.yml @@ -1,101 +1,283 @@ +# ================================================================================ +# AI-PULSE - WORKFLOW DE MISE À JOUR AUTOMATIQUE +# ================================================================================ +# +# DESCRIPTION: +# Ce fichier définit une "GitHub Action" qui s'exécute automatiquement. +# Il récupère les nouveaux articles, met à jour le README et le site. +# +# QU'EST-CE QU'UNE GITHUB ACTION ? +# C'est un "robot" qui exécute des commandes automatiquement sur les +# serveurs de GitHub. Pas besoin de laisser un ordinateur allumé ! +# +# QUAND CE WORKFLOW S'EXÉCUTE-T-IL ? +# 1. Toutes les 2 heures (programmé avec "cron") +# 2. À chaque push sur la branche main +# 3. Manuellement depuis l'interface GitHub (workflow_dispatch) +# +# QUE FAIT CE WORKFLOW ? +# 1. Télécharge le code du projet +# 2. Installe Node.js et les dépendances +# 3. Exécute l'agrégateur (src/aggregator.js) +# 4. Met à jour le README.md avec les nouveaux articles +# 5. Commit et push les changements +# +# SECRETS NÉCESSAIRES (à configurer dans GitHub > Settings > Secrets): +# - GIT_AUTHOR_EMAIL : Email pour les commits +# - LINKEDIN_ACCESS_TOKEN : Token pour poster sur LinkedIn (optionnel) +# - LINKEDIN_USER_ID : ID utilisateur LinkedIn (optionnel) +# - OPENAI_API_KEY : Clé API OpenAI pour générer les posts (optionnel) +# - API_RESEND : Clé API Resend pour les emails (optionnel) +# +# VERSION: 2.0.0 +# DERNIÈRE MISE À JOUR: Février 2026 +# ================================================================================ + + +# ============================================================================= +# NOM DU WORKFLOW +# ============================================================================= +# Ce nom apparaît dans l'onglet "Actions" de GitHub name: AI-Pulse Auto Aggregator + +# ============================================================================= +# DÉCLENCHEURS (Quand le workflow s'exécute) +# ============================================================================= on: + # --------------------------------------------------------------------------- + # PLANIFICATION (Schedule) + # --------------------------------------------------------------------------- + # Exécution automatique selon une expression "cron" + # + # FORMAT CRON : minute heure jour-du-mois mois jour-de-la-semaine + # + # EXPLICATION : '0 */2 * * *' + # - 0 : À la minute 0 (début de l'heure) + # - */2 : Toutes les 2 heures (* = toutes, /2 = divisé par 2) + # - * : Tous les jours du mois + # - * : Tous les mois + # - * : Tous les jours de la semaine + # + # RÉSULTAT : Exécution à 00:00, 02:00, 04:00, 06:00, etc. (UTC) schedule: - - cron: '0 */2 * * *' # Every 2 hours + - cron: '0 */2 * * *' + + # --------------------------------------------------------------------------- + # DÉCLENCHEMENT AU PUSH + # --------------------------------------------------------------------------- + # S'exécute quand du code est poussé sur la branche "main" push: branches: - main - workflow_dispatch: # Manual trigger + # --------------------------------------------------------------------------- + # DÉCLENCHEMENT MANUEL + # --------------------------------------------------------------------------- + # Permet de lancer le workflow manuellement depuis l'interface GitHub + # (onglet Actions > sélectionner le workflow > "Run workflow") + workflow_dispatch: + + +# ============================================================================= +# PERMISSIONS +# ============================================================================= +# Autorise le workflow à modifier le contenu du dépôt (push des commits) permissions: - contents: write + contents: write # Permet de lire ET écrire dans le dépôt + +# ============================================================================= +# JOBS (Tâches à exécuter) +# ============================================================================= +# Un workflow peut contenir plusieurs jobs qui s'exécutent en parallèle. +# Ici, on n'a qu'un seul job : "aggregate-and-post" jobs: + + # --------------------------------------------------------------------------- + # JOB : aggregate-and-post + # --------------------------------------------------------------------------- + # Ce job fait tout le travail : récupérer les articles, mettre à jour le site aggregate-and-post: + + # Sur quel type de machine exécuter ce job ? + # ubuntu-latest = dernière version d'Ubuntu (Linux gratuit de GitHub) runs-on: ubuntu-latest + # ========================================================================= + # ÉTAPES DU JOB (Steps) + # ========================================================================= + # Les étapes s'exécutent dans l'ordre, une par une steps: + + # ------------------------------------------------------------------------- + # ÉTAPE 1 : Récupérer le code source + # ------------------------------------------------------------------------- + # Cette action officielle de GitHub télécharge le code du dépôt + # sur la machine virtuelle où s'exécute le workflow. + # + # Sans cette étape, le workflow n'aurait pas accès aux fichiers ! - name: Check out repository uses: actions/checkout@v4 + # @v4 = version 4 de cette action (la plus récente et stable) + # ------------------------------------------------------------------------- + # ÉTAPE 2 : Installer Node.js + # ------------------------------------------------------------------------- + # Notre agrégateur est écrit en JavaScript (Node.js). + # Cette action installe Node.js sur la machine. + # + # PARAMÈTRES: + # - node-version: '20' : Utilise Node.js version 20 (LTS) + # - cache: 'npm' : Met en cache les dépendances pour accélérer les builds - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - # Installation automatique et résiliente des dépendances - # Tente d'abord npm ci (rapide et reproductible) - # Si échec dû à désynchronisation, bascule sur npm install - # Et commit automatiquement le package-lock.json mis à jour + # ------------------------------------------------------------------------- + # ÉTAPE 3 : Installer les dépendances + # ------------------------------------------------------------------------- + # Installe les bibliothèques listées dans package.json + # + # STRATÉGIE RÉSILIENTE: + # 1. Essaie d'abord "npm ci" (rapide, utilise package-lock.json) + # 2. Si ça échoue (lock file désynchronisé), utilise "npm install" + # 3. Cela régénère un package-lock.json propre - name: Install dependencies with auto-fix run: | # Tentative avec npm ci (installation propre et rapide) + # npm ci est plus rapide que npm install car il ne modifie pas + # le fichier package-lock.json et utilise exactement les versions + # qui y sont spécifiées if npm ci; then echo "✅ Installation réussie avec npm ci" else echo "⚠️ npm ci a échoué, reconstruction du lock file..." - + # Suppression du cache npm pour éviter les conflits + # entre anciennes et nouvelles versions npm cache clean --force - + # Installation complète qui met à jour package-lock.json + # npm install lit package.json et installe les dépendances npm install - + echo "✅ package-lock.json régénéré automatiquement" fi - # Commit automatique du package-lock.json si modifié - # S'exécute avant l'agrégation pour garder le repo propre + # ------------------------------------------------------------------------- + # ÉTAPE 4 : Commit automatique du lock file (si modifié) + # ------------------------------------------------------------------------- + # Si npm install a régénéré package-lock.json, on le commit + # pour garder le dépôt synchronisé. + # + # POURQUOI ? + # Cela évite que le prochain build échoue à cause d'un lock file + # désynchronisé. - name: Auto-commit updated lock file run: | # Configuration Git pour les commits automatiques + # user.name : Nom affiché dans l'historique des commits + # user.email : Email associé au commit git config --global user.name 'PhoenixProject-AutoSync' git config --global user.email '${{ secrets.GIT_AUTHOR_EMAIL }}' - + # Vérifier si package-lock.json a été modifié + # git diff --exit-code retourne 0 si pas de changement, 1 sinon + # Le ! inverse le résultat pour entrer dans le if si modifié if ! git diff --exit-code package-lock.json; then echo "📦 Synchronisation automatique de package-lock.json" - + + # Ajouter le fichier à l'index Git git add package-lock.json + + # Créer un commit avec un message descriptif git commit -m "🔧 Auto-sync: package-lock.json mis à jour automatiquement - + - Synchronisation automatique des dépendances - Généré par le workflow CI/CD - Date: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" - + + # Pousser les changements vers GitHub git push - + echo "✅ package-lock.json synchronisé et committé" else echo "ℹ️ package-lock.json déjà à jour" fi + # ------------------------------------------------------------------------- + # ÉTAPE 5 : Exécuter l'agrégateur + # ------------------------------------------------------------------------- + # C'est l'étape principale ! Elle lance le script src/aggregator.js + # qui récupère tous les articles des flux RSS. + # + # Le résultat (le nouveau README) est redirigé vers NEW-README.md + # avec l'opérateur > (redirection de sortie) + # + # VARIABLES D'ENVIRONNEMENT (env): + # Ces secrets sont passés au script pour les fonctionnalités optionnelles: + # - LINKEDIN_* : Pour poster automatiquement sur LinkedIn + # - OPENAI_API_KEY : Pour générer des résumés avec l'IA + # - API_RESEND : Pour envoyer les emails de newsletter - name: Aggregate AI, iOT and Cybersecurity articles env: LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }} LINKEDIN_USER_ID: ${{ secrets.LINKEDIN_USER_ID }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + API_RESEND: ${{ secrets.API_RESEND }} run: | + # Exécuter l'agrégateur et sauvegarder le résultat + # La sortie standard (console.log) va dans NEW-README.md + # Les erreurs (console.error) s'affichent dans les logs GitHub node src/aggregator.js > NEW-README.md + # ------------------------------------------------------------------------- + # ÉTAPE 6 : Mettre à jour le README + # ------------------------------------------------------------------------- + # Remplace l'ancien README.md par le nouveau + # mv = move (renommer/déplacer un fichier) - name: Update README with new content run: | mv NEW-README.md README.md + # ------------------------------------------------------------------------- + # ÉTAPE 7 : Commit et push des changements + # ------------------------------------------------------------------------- + # Sauvegarde tous les changements dans un nouveau commit + # et les envoie vers GitHub. + # + # SÉCURITÉ: + # On vérifie d'abord s'il y a des changements à committer + # pour éviter les commits vides qui génèrent des erreurs. - name: Commit and push changes env: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} run: | + # Configuration de l'identité Git git config --global user.name 'PhoenixProject' git config --global user.email "${GIT_AUTHOR_EMAIL:-phoenix.project@example.com}" + + # Ajouter tous les fichiers modifiés à l'index + # Le point (.) signifie "tous les fichiers" git add . + # Vérifier s'il y a des changements à committer + # git diff --cached : montre les différences entre l'index et le dernier commit + # --exit-code : retourne 1 s'il y a des différences + # Le ! inverse le résultat if ! git diff --cached --exit-code; then + # Créer la date au format UTC pour le message de commit UTC_DATE=$(date -u +'%a %b %d %H:%M:%S UTC %Y') + + # Créer le commit avec un message incluant la date git commit -m "Updated AI-Pulse: $UTC_DATE" + + # Envoyer le commit vers GitHub git push else + # Pas de changements, rien à faire echo "No changes to commit" fi diff --git a/404.html b/404.html index 10c6f856d..a444ab555 100644 --- a/404.html +++ b/404.html @@ -1,109 +1,355 @@ + + + + + + + + 404 - Page Not Found | AI-Pulse + + + + + + + + + + + + - + - +
+ +
404
+ +

Page Not Found

-

The page you're looking for doesn't exist or has been - moved.

+ + +

+ The page you're looking for doesn't exist or has been moved. +

+ + Go to Homepage -
Redirecting automatically in 5 seconds...
+ + +
+ Redirecting automatically in 5 seconds... +
- + + + + - \ No newline at end of file + diff --git a/about.html b/about.html index 0c0ab1df3..e49223fb2 100644 --- a/about.html +++ b/about.html @@ -1,102 +1,295 @@ + + + + + + + + About | AI-Pulse + + + + + + + + + + + + - + - +
+ +

About AI-Pulse

-

The future of AI news aggregation, monitored in - real-time.

+

+ The future of AI news aggregation, monitored in real-time. +

+
+

Our Mission

+ +

AI-Pulse is a specialized aggregator designed to filter the noise and deliver high-signal intelligence on Artificial Intelligence and Cybersecurity.

+ +

Developed by ThePhoenixAgency, this project prioritizes privacy, speed, and cleaner reading experiences without the tracking clutter of modern web media.

+
+

Technology Stack

-

Built with modern, lightweight web technologies: + +

+ Built with modern, lightweight web technologies:

+ +
Vanilla JS CSS Variables @@ -106,36 +299,85 @@

Technology Stack

+
+

Privacy First

+

We believe in transparency. AI-Pulse operates with:

+ +
    -
  • No Third-Party Cookies
  • -
  • No External Tracking Pixels
  • -
  • Local-Only Session Statistics
  • -
  • Open Source Codebase
  • +
  • No Third-Party Cookies
  • +
  • No External Tracking Pixels
  • +
  • Local-Only Session Statistics
  • +
  • Open Source Codebase
+
View Developer Portfolio
+
+ + +
- + + - \ No newline at end of file + diff --git a/app.html b/app.html index 6a9046d6b..17625d863 100644 --- a/app.html +++ b/app.html @@ -1,49 +1,170 @@ + + + + + + + + AI-Pulse - Reader + + + + + + + + + + + + - + - + - + + + + - \ No newline at end of file + diff --git a/config.json b/config.json new file mode 100644 index 000000000..7193b2b68 --- /dev/null +++ b/config.json @@ -0,0 +1,263 @@ +{ + "settings": { + "articlesPerFeed": 15, + "maxArticlesPerCategory": 30, + "summaryMaxLength": 600, + "deduplication": { + "enabled": true, + "similarityThreshold": 0.7, + "contentThreshold": 0.5 + } + }, + "categories": { + "ai": { + "labels": { "en": "AI - Artificial Intelligence", "fr": "IA - Intelligence Artificielle" }, + "icon": "brain", + "priority": 1, + "feeds": [ + { "name": "TechCrunch AI", "url": "https://techcrunch.com/category/artificial-intelligence/feed/", "tags": ["AI", "Startups", "Tech"], "lang": "en" }, + { "name": "VentureBeat AI", "url": "https://venturebeat.com/category/ai/feed/", "tags": ["AI", "Enterprise", "Business"], "lang": "en" }, + { "name": "AI News", "url": "https://www.artificialintelligence-news.com/feed/", "tags": ["AI", "News", "Industry"], "lang": "en" }, + { "name": "Google AI Blog", "url": "https://ai.googleblog.com/feeds/posts/default", "tags": ["AI", "Research", "Google"], "lang": "en" }, + { "name": "MIT Technology Review", "url": "https://www.technologyreview.com/feed/", "tags": ["AI", "Research", "Innovation"], "lang": "en" }, + { "name": "OpenAI Blog", "url": "https://openai.com/blog/rss.xml", "tags": ["AI", "LLM", "GPT"], "lang": "en" }, + { "name": "Hugging Face Blog", "url": "https://huggingface.co/blog/feed.xml", "tags": ["AI", "ML", "Open Source"], "lang": "en" }, + { "name": "Medium AI", "url": "https://medium.com/tag/artificial-intelligence/feed", "tags": ["AI", "ML", "Deep Learning"], "lang": "en" }, + { "name": "Towards Data Science", "url": "https://towardsdatascience.com/feed", "tags": ["AI", "Data Science", "Analytics"], "lang": "en" }, + { "name": "ActuIA", "url": "https://actuia.com/feed/", "tags": ["IA", "Actualites", "France"], "lang": "fr" }, + { "name": "LeMagIT - IA", "url": "https://www.lemagit.fr/rss/IA.xml", "tags": ["IA", "Entreprise", "Tech"], "lang": "fr" }, + { "name": "Siecle Digital", "url": "https://siecledigital.fr/feed/", "tags": ["IA", "Digital", "Tech"], "lang": "fr" }, + { "name": "Numerama Tech", "url": "https://www.numerama.com/feed/", "tags": ["IA", "Tech", "Science"], "lang": "fr" } + ] + }, + "cybersecurity": { + "labels": { "en": "Cybersecurity", "fr": "Cybersecurite" }, + "icon": "shield", + "priority": 2, + "feeds": [ + { "name": "The Hacker News", "url": "https://feeds.feedburner.com/TheHackersNews", "tags": ["Security", "Vulnerabilities", "Threats"], "lang": "en" }, + { "name": "Bleeping Computer", "url": "https://www.bleepingcomputer.com/feed/", "tags": ["Security", "Malware", "CVE"], "lang": "en" }, + { "name": "Krebs on Security", "url": "https://krebsonsecurity.com/feed/", "tags": ["Security", "Fraud", "Privacy"], "lang": "en" }, + { "name": "Dark Reading", "url": "https://www.darkreading.com/rss_simple.asp", "tags": ["Security", "CVE", "Enterprise"], "lang": "en" }, + { "name": "SecurityWeek", "url": "https://www.securityweek.com/feed/", "tags": ["Security", "CVE", "News"], "lang": "en" }, + { "name": "Threatpost", "url": "https://threatpost.com/feed/", "tags": ["Security", "Threats", "CVE"], "lang": "en" }, + { "name": "Penta Security", "url": "https://www.pentasecurity.com/blog/feed/", "tags": ["Security", "WAF", "Encryption"], "lang": "en" }, + { "name": "Google Project Zero", "url": "https://googleprojectzero.blogspot.com/feeds/posts/default", "tags": ["Security", "0day", "Research"], "lang": "en" }, + { "name": "PortSwigger Research", "url": "https://portswigger.net/research/rss", "tags": ["Security", "WebSec", "Research"], "lang": "en" }, + { "name": "HackerOne Blog", "url": "https://www.hackerone.com/blog.rss", "tags": ["Bug Bounty", "Security", "Hacking"], "lang": "en" }, + { "name": "Bugcrowd Blog", "url": "https://www.bugcrowd.com/blog/feed", "tags": ["Bug Bounty", "Security", "Pentesting"], "lang": "en" }, + { "name": "Packet Storm Security", "url": "https://packetstormsecurity.com/feeds/", "tags": ["Exploits", "Advisories", "Security"], "lang": "en" }, + { "name": "Exploit-DB", "url": "https://www.exploit-db.com/rss.xml", "tags": ["Exploits", "CVE", "Security"], "lang": "en" }, + { "name": "CTFtime", "url": "https://ctftime.org/event/list/upcoming/rss/", "tags": ["CTF", "Hacking", "Competition"], "lang": "en" }, + { "name": "CERT-FR", "url": "https://www.cert.ssi.gouv.fr/feed/", "tags": ["Securite", "Alertes", "France"], "lang": "fr" }, + { "name": "Zataz", "url": "https://www.zataz.com/feed/", "tags": ["Securite", "Cybercrime", "France"], "lang": "fr" }, + { "name": "UnderNews", "url": "https://www.undernews.fr/feed", "tags": ["Securite", "Malware", "France"], "lang": "fr" }, + { "name": "NoLimitSecu", "url": "https://www.nolimitsecu.fr/feed/", "tags": ["Securite", "Podcast", "France"], "lang": "fr" }, + { "name": "LeMagIT - Securite", "url": "https://www.lemagit.fr/rss/Securite.xml", "tags": ["Securite", "Entreprise", "France"], "lang": "fr" } + ] + }, + "iot": { + "labels": { "en": "IoT - Internet of Things", "fr": "IdO - Internet des Objets" }, + "icon": "cpu", + "priority": 3, + "feeds": [ + { "name": "Raspberry Pi", "url": "https://www.raspberrypi.com/news/feed/", "tags": ["IoT", "RaspberryPi", "Hardware"], "lang": "en" }, + { "name": "Arduino Blog", "url": "https://blog.arduino.cc/feed/", "tags": ["IoT", "Arduino", "Hardware"], "lang": "en" }, + { "name": "Hackster.io", "url": "https://www.hackster.io/news/feed", "tags": ["IoT", "Hardware", "Projects"], "lang": "en" }, + { "name": "IoT For All", "url": "https://www.iotforall.com/feed", "tags": ["IoT", "News"], "lang": "en" }, + { "name": "IoT Business News", "url": "https://iotbusinessnews.com/feed/", "tags": ["IoT", "Business", "News"], "lang": "en" }, + { "name": "IoT World Today", "url": "https://www.iotworldtoday.com/feed", "tags": ["IoT", "News"], "lang": "en" }, + { "name": "Adafruit Blog", "url": "https://blog.adafruit.com/feed/", "tags": ["IoT", "Maker", "Hardware"], "lang": "en" }, + { "name": "SparkFun Blog", "url": "https://www.sparkfun.com/news/feed", "tags": ["IoT", "Electronics", "Hardware"], "lang": "en" }, + { "name": "Embedded.com", "url": "https://www.embedded.com/feed", "tags": ["Embedded", "IoT", "Hardware"], "lang": "en" }, + { "name": "Domotique News", "url": "http://www.domotique-news.com/feed", "tags": ["Domotique", "IoT", "Maison"], "lang": "fr" }, + { "name": "Framboise 314", "url": "https://www.framboise314.fr/feed/", "tags": ["RaspberryPi", "IoT", "DIY"], "lang": "fr" }, + { "name": "Maison et Domotique", "url": "https://www.maison-et-domotique.com/feed/", "tags": ["Domotique", "SmartHome", "IoT"], "lang": "fr" } + ] + }, + "windows": { + "labels": { "en": "Windows", "fr": "Windows" }, + "icon": "monitor", + "priority": 4, + "feeds": [ + { "name": "Windows Central", "url": "https://www.windowscentral.com/feed", "tags": ["Windows", "Microsoft", "PC"], "lang": "en" }, + { "name": "Neowin", "url": "https://www.neowin.net/news/rss/", "tags": ["Windows", "Microsoft", "Tech"], "lang": "en" }, + { "name": "Windows Latest", "url": "https://www.windowslatest.com/feed/", "tags": ["Windows", "Updates", "Microsoft"], "lang": "en" }, + { "name": "BleepingComputer Windows", "url": "https://www.bleepingcomputer.com/feed/", "tags": ["Windows", "Security", "Updates"], "lang": "en" }, + { "name": "Next INpact", "url": "https://next.ink/feed/", "tags": ["Windows", "Tech", "Numerique"], "lang": "fr" }, + { "name": "GNT", "url": "https://www.generation-nt.com/export/rss.xml", "tags": ["Windows", "Tech", "Logiciels"], "lang": "fr" } + ] + }, + "mac": { + "labels": { "en": "Mac / Apple", "fr": "Mac / Apple" }, + "icon": "apple", + "priority": 5, + "feeds": [ + { "name": "9to5Mac", "url": "https://9to5mac.com/feed/", "tags": ["Apple", "Mac", "iOS"], "lang": "en" }, + { "name": "MacRumors", "url": "https://feeds.macrumors.com/MacRumors-All", "tags": ["Apple", "Mac", "Rumors"], "lang": "en" }, + { "name": "AppleInsider", "url": "https://appleinsider.com/rss/news/", "tags": ["Apple", "Mac", "News"], "lang": "en" }, + { "name": "The Mac Observer", "url": "https://www.macobserver.com/feed/", "tags": ["Apple", "Mac", "Reviews"], "lang": "en" }, + { "name": "MacGeneration", "url": "https://www.macgeneration.com/rss", "tags": ["Apple", "Mac", "France"], "lang": "fr" }, + { "name": "iGeneration", "url": "https://www.igen.fr/rss", "tags": ["Apple", "iOS", "France"], "lang": "fr" } + ] + }, + "linux": { + "labels": { "en": "Linux", "fr": "Linux" }, + "icon": "terminal", + "priority": 6, + "feeds": [ + { "name": "OMG! Ubuntu", "url": "https://www.omgubuntu.co.uk/feed", "tags": ["Linux", "Ubuntu", "Open Source"], "lang": "en" }, + { "name": "Its FOSS", "url": "https://itsfoss.com/feed/", "tags": ["Linux", "Open Source", "Tutorials"], "lang": "en" }, + { "name": "Phoronix", "url": "https://www.phoronix.com/rss.php", "tags": ["Linux", "Benchmarks", "Hardware"], "lang": "en" }, + { "name": "LWN.net", "url": "https://lwn.net/headlines/rss", "tags": ["Linux", "Kernel", "Development"], "lang": "en" }, + { "name": "GamingOnLinux", "url": "https://www.gamingonlinux.com/article_rss.php", "tags": ["Linux", "Gaming", "Proton"], "lang": "en" }, + { "name": "LinuxFr", "url": "https://linuxfr.org/news.atom", "tags": ["Linux", "Logiciel Libre", "France"], "lang": "fr" }, + { "name": "Journal du Hacker", "url": "https://www.journalduhacker.net/rss", "tags": ["Linux", "Hacking", "Open Source"], "lang": "fr" }, + { "name": "Toolinux", "url": "https://www.toolinux.com/feed", "tags": ["Linux", "Logiciel Libre", "France"], "lang": "fr" } + ] + }, + "tech": { + "labels": { "en": "General Tech", "fr": "Tech Generale" }, + "icon": "zap", + "priority": 7, + "feeds": [ + { "name": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "tags": ["Tech", "News", "Reviews"], "lang": "en" }, + { "name": "Ars Technica", "url": "https://feeds.arstechnica.com/arstechnica/index", "tags": ["Tech", "Science", "Analysis"], "lang": "en" }, + { "name": "Wired", "url": "https://www.wired.com/feed/rss", "tags": ["Tech", "Culture", "Science"], "lang": "en" }, + { "name": "TechRadar", "url": "https://www.techradar.com/rss", "tags": ["Tech", "Reviews", "Gadgets"], "lang": "en" }, + { "name": "Korben", "url": "https://korben.info/feed", "tags": ["Tech", "Outils", "Astuces"], "lang": "fr" }, + { "name": "Frandroid", "url": "https://www.frandroid.com/feed", "tags": ["Tech", "Mobile", "Android"], "lang": "fr" }, + { "name": "Numerama", "url": "https://www.numerama.com/feed/", "tags": ["Tech", "Numerique", "Societe"], "lang": "fr" }, + { "name": "Le Monde Informatique", "url": "https://www.lemondeinformatique.fr/flux-rss/thematique/toutes-les-actualites/rss/", "tags": ["Tech", "Entreprise", "IT"], "lang": "fr" } + ] + }, + "entrepreneurship": { + "labels": { "en": "Entrepreneurship", "fr": "Entrepreneuriat" }, + "icon": "rocket", + "priority": 8, + "feeds": [ + { "name": "TechCrunch Startups", "url": "https://techcrunch.com/category/startups/feed/", "tags": ["Startups", "Fundraising", "Tech"], "lang": "en" }, + { "name": "Entrepreneur.com", "url": "https://www.entrepreneur.com/latest/feed", "tags": ["Entrepreneurship", "Business", "Startups"], "lang": "en" }, + { "name": "Inc.com", "url": "https://www.inc.com/rss", "tags": ["Business", "Startups", "Leadership"], "lang": "en" }, + { "name": "Maddyness", "url": "https://www.maddyness.com/feed/", "tags": ["Startups", "Entrepreneuriat", "France"], "lang": "fr" }, + { "name": "FrenchWeb", "url": "https://www.frenchweb.fr/feed", "tags": ["Startups", "Tech", "France"], "lang": "fr" }, + { "name": "BPI France Le Hub", "url": "https://lehub.bpifrance.fr/feed/", "tags": ["Entrepreneuriat", "Innovation", "France"], "lang": "fr" }, + { "name": "Les Echos Entrepreneurs", "url": "https://business.lesechos.fr/feed/", "tags": ["Business", "Entrepreneurs", "France"], "lang": "fr" }, + { "name": "Journal du Net", "url": "https://www.journaldunet.com/rss/", "tags": ["Business", "Tech", "Economie"], "lang": "fr" }, + { "name": "Capital", "url": "https://www.capital.fr/feed", "tags": ["Business", "Economie", "France"], "lang": "fr" } + ] + }, + "finance": { + "labels": { "en": "Stock Market & Finance", "fr": "Bourse & Finance" }, + "icon": "trending-up", + "priority": 9, + "feeds": [ + { "name": "MarketWatch", "url": "https://feeds.marketwatch.com/marketwatch/topstories/", "tags": ["Finance", "Markets", "Stocks"], "lang": "en" }, + { "name": "Yahoo Finance", "url": "https://finance.yahoo.com/rss/", "tags": ["Finance", "Markets", "Economy"], "lang": "en" }, + { "name": "Reuters Business", "url": "https://www.reuters.com/business/finance/rss", "tags": ["Finance", "Business", "Global"], "lang": "en" }, + { "name": "Investing.com", "url": "https://www.investing.com/rss/news.rss", "tags": ["Finance", "Trading", "Markets"], "lang": "en" }, + { "name": "Zonebourse", "url": "https://www.zonebourse.com/rss/", "tags": ["Bourse", "Actions", "Finance"], "lang": "fr" }, + { "name": "Boursorama", "url": "https://www.boursorama.com/rss/actualites", "tags": ["Bourse", "Marches", "Finance"], "lang": "fr" }, + { "name": "ABC Bourse", "url": "https://www.abcbourse.com/rss/", "tags": ["Bourse", "Analyse", "Finance"], "lang": "fr" }, + { "name": "Investir Les Echos", "url": "https://investir.lesechos.fr/feed/", "tags": ["Investissement", "Bourse", "Finance"], "lang": "fr" } + ] + }, + "crypto": { + "labels": { "en": "Crypto & Blockchain", "fr": "Crypto & Blockchain" }, + "icon": "link", + "priority": 10, + "feeds": [ + { "name": "CoinDesk", "url": "https://www.coindesk.com/arc/outboundfeeds/rss/", "tags": ["Crypto", "Bitcoin", "Blockchain"], "lang": "en" }, + { "name": "CoinTelegraph", "url": "https://cointelegraph.com/rss", "tags": ["Crypto", "Blockchain", "DeFi"], "lang": "en" }, + { "name": "The Block", "url": "https://www.theblock.co/rss.xml", "tags": ["Crypto", "Blockchain", "Analysis"], "lang": "en" }, + { "name": "Decrypt", "url": "https://decrypt.co/feed", "tags": ["Crypto", "Web3", "NFT"], "lang": "en" }, + { "name": "Journal du Coin", "url": "https://journalducoin.com/feed/", "tags": ["Crypto", "Bitcoin", "France"], "lang": "fr" }, + { "name": "Cryptoast", "url": "https://cryptoast.fr/feed/", "tags": ["Crypto", "Blockchain", "France"], "lang": "fr" }, + { "name": "CoinTribune", "url": "https://www.cointribune.com/feed/", "tags": ["Crypto", "Blockchain", "France"], "lang": "fr" } + ] + }, + "opensource": { + "labels": { "en": "Open Source & GitHub", "fr": "Open Source & GitHub" }, + "icon": "git-branch", + "priority": 11, + "feeds": [ + { "name": "GitHub Blog", "url": "https://github.blog/feed/", "tags": ["GitHub", "Open Source", "DevTools"], "lang": "en" }, + { "name": "GitHub Changelog", "url": "https://github.blog/changelog/feed/", "tags": ["GitHub", "Updates", "Features"], "lang": "en" }, + { "name": "GitHub Trending", "url": "https://mshibanami.github.io/GitHubTrendingRSS/daily/all.xml", "tags": ["GitHub", "Trending", "Repos"], "lang": "en" }, + { "name": "GitHub Trending Python", "url": "https://mshibanami.github.io/GitHubTrendingRSS/daily/python.xml", "tags": ["GitHub", "Python", "Trending"], "lang": "en" }, + { "name": "The Changelog", "url": "https://changelog.com/feed", "tags": ["Open Source", "Podcast", "DevTools"], "lang": "en" }, + { "name": "Console.dev", "url": "https://console.dev/rss.xml", "tags": ["DevTools", "Open Source", "Weekly"], "lang": "en" }, + { "name": "Dev.to Open Source", "url": "https://dev.to/feed/tag/opensource", "tags": ["Open Source", "Community", "Dev"], "lang": "en" }, + { "name": "LibHunt Selfhosted", "url": "https://selfhosted.libhunt.com/feed", "tags": ["Self-hosted", "Open Source", "Tools"], "lang": "en" } + ] + }, + "products": { + "labels": { "en": "Products & Innovation", "fr": "Produits & Innovation" }, + "icon": "package", + "priority": 12, + "feeds": [ + { "name": "Product Hunt", "url": "https://www.producthunt.com/feed", "tags": ["Products", "Launch", "Startups"], "lang": "en" }, + { "name": "Hacker News Show", "url": "https://hnrss.org/show", "tags": ["Products", "Launch", "HackerNews"], "lang": "en" }, + { "name": "BetaList", "url": "https://betalist.com/feed", "tags": ["Beta", "Startups", "Products"], "lang": "en" }, + { "name": "Indie Hackers", "url": "https://www.indiehackers.com/feed", "tags": ["Indie", "Products", "SaaS"], "lang": "en" }, + { "name": "Les Numeriques", "url": "https://www.lesnumeriques.com/rss.xml", "tags": ["Produits", "Tests", "Tech"], "lang": "fr" }, + { "name": "Journal du Geek", "url": "https://www.journaldugeek.com/feed/", "tags": ["Produits", "Gadgets", "Tech"], "lang": "fr" }, + { "name": "Futura Tech", "url": "https://www.futura-sciences.com/tech/rss", "tags": ["Innovation", "Science", "Tech"], "lang": "fr" }, + { "name": "01net", "url": "https://www.01net.com/rss/info/flux-rss.xml", "tags": ["Produits", "Tests", "Tech"], "lang": "fr" } + ] + } + }, + "keywords": { + "universal": [ + "LLM", "GPT", "ChatGPT", "Copilot", "Gemini", "Claude", "Mistral", "Llama", + "Stable Diffusion", "Midjourney", "Docker", "Kubernetes", "DevOps", + "GitHub", "GitLab", "Python", "Rust", "JavaScript", "TypeScript", + "Raspberry Pi", "Arduino", "ESP32", "Ubuntu", "Debian", "Fedora", "Arch", + "macOS", "iOS", "Android", "Windows 11", + "API", "Open Source", "Cloud", "AWS", "Azure", "GCP", + "Ransomware", "Phishing", "Zero-day", "CVE", "CERT", + "5G", "Wi-Fi", "Bluetooth", "Zigbee", "Matter", + "Tesla", "NVIDIA", "AMD", "Intel", "ARM", "RISC-V", + "OpenClaw", "ClowdBot", "MoltBot", + "Blockchain", "Bitcoin", "Ethereum", "DeFi", "NFT", "Web3", + "Startup", "Fintech", "SaaS", "Scaleup", + "CAC 40", "S&P 500", "NASDAQ", "Dow Jones", + "IPO", "Trading", + "Product Hunt", "Kickstarter", "Gadget", "Wearable", + "Self-hosted", "CLI", "Framework", "SDK" + ], + "localized": { + "artificial_intelligence": { "en": "Artificial Intelligence", "fr": "Intelligence Artificielle" }, + "machine_learning": { "en": "Machine Learning", "fr": "Apprentissage Automatique" }, + "deep_learning": { "en": "Deep Learning", "fr": "Apprentissage Profond" }, + "cybersecurity": { "en": "Cybersecurity", "fr": "Cybersecurite" }, + "data_breach": { "en": "Data Breach", "fr": "Fuite de Donnees" }, + "privacy": { "en": "Privacy", "fr": "Vie Privee" }, + "home_automation": { "en": "Home Automation", "fr": "Domotique" }, + "connected_objects": { "en": "Connected Objects", "fr": "Objets Connectes" }, + "open_source": { "en": "Open Source", "fr": "Logiciel Libre" }, + "update": { "en": "Update", "fr": "Mise a jour" }, + "vulnerability": { "en": "Vulnerability", "fr": "Vulnerabilite" }, + "neural_network": { "en": "Neural Network", "fr": "Reseau de Neurones" }, + "computer_vision": { "en": "Computer Vision", "fr": "Vision par Ordinateur" }, + "robotics": { "en": "Robotics", "fr": "Robotique" }, + "quantum_computing": { "en": "Quantum Computing", "fr": "Informatique Quantique" }, + "stock_market": { "en": "Stock Market", "fr": "Bourse" }, + "cryptocurrency": { "en": "Cryptocurrency", "fr": "Cryptomonnaie" }, + "fundraising": { "en": "Fundraising", "fr": "Levee de Fonds" }, + "entrepreneurship": { "en": "Entrepreneurship", "fr": "Entrepreneuriat" }, + "startup": { "en": "Startup", "fr": "Startup" }, + "investment": { "en": "Investment", "fr": "Investissement" }, + "bug_bounty": { "en": "Bug Bounty", "fr": "Bug Bounty" }, + "exploit": { "en": "Exploit", "fr": "Exploit" }, + "regulation": { "en": "Regulation", "fr": "Regulation" }, + "new_product": { "en": "New Product", "fr": "Nouveau Produit" }, + "launch": { "en": "Launch", "fr": "Lancement" }, + "crowdfunding": { "en": "Crowdfunding", "fr": "Financement Participatif" }, + "repository": { "en": "Repository", "fr": "Depot" }, + "self_hosted": { "en": "Self-hosted", "fr": "Auto-heberge" }, + "library": { "en": "Library", "fr": "Bibliotheque" }, + "tool": { "en": "Tool", "fr": "Outil" } + } + } +} diff --git a/css/style.css b/css/style.css index 6b31c7369..b603daddf 100644 --- a/css/style.css +++ b/css/style.css @@ -1,211 +1,522 @@ -/* Global Vars */ +/** + * ================================================================================ + * AI-PULSE - FEUILLE DE STYLES PRINCIPALE (style.css) + * ================================================================================ + * + * DESCRIPTION: + * Ce fichier contient tous les styles visuels du site AI-Pulse. + * Il définit les couleurs, les polices, les mises en page et les animations. + * + * STRUCTURE DU FICHIER: + * 1. Variables CSS (couleurs, polices) + * 2. Reset (remise à zéro des styles par défaut) + * 3. Typographie (titres, liens) + * 4. En-tête (header) + * 5. Pied de page (footer) + * 6. Classes utilitaires + * 7. Animations + * 8. Boutons de navigation rapide (elevator) + * 9. Styles responsive (mobile) + * + * PALETTE DE COULEURS: + * - Cyan (#00d9ff) : Couleur principale, liens, accents + * - Violet (#825ee4) : Couleur secondaire, dégradés + * - Bleu foncé (#0a0e27) : Fond principal + * - Blanc (#ffffff) : Texte principal + * - Gris (#94a3b8) : Texte secondaire + * + * VERSION: 1.0.0 + * DERNIÈRE MISE À JOUR: Février 2026 + * ================================================================================ + */ + + +/* ============================================================================= + 1. VARIABLES CSS (Custom Properties) + ============================================================================= + + Les variables CSS permettent de définir des valeurs réutilisables. + Elles sont définies dans :root (la racine du document) pour être + accessibles partout dans le fichier. + + UTILISATION: + Pour utiliser une variable : var(--nom-de-la-variable) + Exemple : color: var(--primary); + + AVANTAGES: + - Changer une couleur à un seul endroit la change partout + - Code plus lisible et maintenable + - Possibilité de créer des thèmes (mode sombre/clair) +============================================================================= */ + :root { + /* ------------------------------------------------------------------------- + COULEURS PRINCIPALES + ------------------------------------------------------------------------- */ + + /* Cyan - Couleur d'accent principale + Utilisée pour : liens, boutons, titres, effets de brillance */ --primary: #00d9ff; + + /* Cyan avec transparence - Pour les effets de lueur (glow) */ --primary-glow: rgba(0, 217, 255, 0.6); + + /* Violet - Couleur secondaire + Utilisée pour : dégradés, accents secondaires */ --secondary: #825ee4; + + /* ------------------------------------------------------------------------- + COULEURS DE FOND + ------------------------------------------------------------------------- */ + + /* Bleu très foncé - Fond principal du site */ --bg-dark: #0a0e27; + + /* Blanc très transparent - Fond des cartes et éléments */ --bg-card: rgba(255, 255, 255, 0.05); + + /* ------------------------------------------------------------------------- + COULEURS DE TEXTE + ------------------------------------------------------------------------- */ + + /* Blanc - Texte principal (titres, contenu important) */ --text-light: #ffffff; + + /* Gris - Texte secondaire (descriptions, métadonnées) */ --text-dim: #94a3b8; + + /* ------------------------------------------------------------------------- + TYPOGRAPHIE + ------------------------------------------------------------------------- */ + + /* Police principale avec fallbacks + 1. Inter : Police Google Fonts moderne + 2. -apple-system : Police système Apple + 3. BlinkMacSystemFont : Chrome sur Mac + 4. Segoe UI : Windows + 5. Roboto : Android + 6. sans-serif : Fallback générique */ --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + + /* ------------------------------------------------------------------------- + AUTRES + ------------------------------------------------------------------------- */ + + /* Fond de l'en-tête avec transparence pour l'effet de flou */ --header-bg: rgba(10, 14, 39, 0.95); } -/* Reset */ -/* Reset */ + +/* ============================================================================= + 2. RESET CSS + ============================================================================= + + Le reset supprime les styles par défaut des navigateurs pour avoir + une base cohérente. Chaque navigateur a ses propres marges et espacements + par défaut, ce qui peut causer des différences d'affichage. +============================================================================= */ + +/* Appliquer à tous les éléments (*) */ * { - margin: 0; - padding: 0; - box-sizing: border-box; - cursor: auto; - /* Default cursor restored */ + margin: 0; /* Supprime les marges par défaut */ + padding: 0; /* Supprime les espacements internes par défaut */ + box-sizing: border-box; /* La largeur inclut le padding et la bordure */ + cursor: auto; /* Curseur par défaut (flèche normale) */ } +/* ------------------------------------------------------------------------- + CORPS DE LA PAGE (body) + ------------------------------------------------------------------------- */ body { + /* Fond dégradé : du bleu foncé vers le bleu-violet + 135deg = direction diagonale (haut-gauche vers bas-droite) */ background: linear-gradient(135deg, #0a0e27 0%, #1a1e47 100%); + + /* Police définie dans les variables */ font-family: var(--font-main); + + /* Couleur du texte par défaut */ color: var(--text-light); + + /* Hauteur minimum = toute la fenêtre */ min-height: 100vh; + + /* Flexbox pour coller le footer en bas */ display: flex; flex-direction: column; + + /* Pas de défilement horizontal */ overflow-x: hidden; } -/* Typography */ + +/* ============================================================================= + 3. TYPOGRAPHIE + ============================================================================= + + Styles pour les titres et les liens. +============================================================================= */ + +/* Tous les niveaux de titres (h1 à h6) */ h1, h2, h3, h4, h5, h6 { - font-weight: 700; - line-height: 1.2; + font-weight: 700; /* Gras */ + line-height: 1.2; /* Hauteur de ligne serrée pour les titres */ } +/* Tous les liens */ a { - text-decoration: none; - color: inherit; - transition: color 0.3s ease; + text-decoration: none; /* Pas de soulignement */ + color: inherit; /* Hérite la couleur du parent */ + transition: color 0.3s ease; /* Animation douce au survol */ } -/* Custom Cursor Hidden */ -#cursor, -#cursor-arrow { - display: none; -} + +/* ============================================================================= + 4. CURSEUR PERSONNALISÉ (désactivé) + ============================================================================= + + Ces éléments étaient utilisés pour un curseur personnalisé. + Ils sont maintenant cachés car le curseur par défaut est utilisé. +============================================================================= */ #cursor, #cursor-arrow { - display: none; + display: none; /* Caché */ } -/* Header */ + +/* ============================================================================= + 5. EN-TÊTE DU SITE (Header) + ============================================================================= + + Barre de navigation en haut de chaque page. + + CARACTÉRISTIQUES: + - Position sticky : reste visible lors du défilement + - Fond semi-transparent avec effet de flou + - Flexbox pour aligner le logo et la navigation +============================================================================= */ + .site-header { + /* Fond défini dans les variables (semi-transparent) */ background: var(--header-bg); + + /* Espacement interne */ padding: 1rem 2rem; + + /* Bordure fine en bas */ border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + /* Flexbox pour disposer les éléments */ display: flex; - justify-content: space-between; - align-items: center; + justify-content: space-between; /* Logo à gauche, nav à droite */ + align-items: center; /* Centré verticalement */ + + /* Position sticky : reste en haut lors du défilement */ position: sticky; top: 0; + + /* Au-dessus des autres éléments */ z-index: 1000; + + /* Effet de flou sur le fond (effet de verre) */ backdrop-filter: blur(10px); } +/* ------------------------------------------------------------------------- + LOGO + ------------------------------------------------------------------------- */ .brand-logo { font-size: 1.5rem; font-weight: 700; + + /* Dégradé de texte (cyan vers violet) */ background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + + /* Flexbox pour aligner le logo et le texte */ display: flex; align-items: center; gap: 10px; } +/* ------------------------------------------------------------------------- + NAVIGATION + ------------------------------------------------------------------------- */ .nav-links { display: flex; - gap: 20px; + gap: 20px; /* Espacement entre les liens */ } .nav-links a { - color: var(--text-dim); + color: var(--text-dim); /* Gris par défaut */ font-weight: 500; font-size: 0.95rem; } +/* Lien au survol ou lien actif */ .nav-links a:hover, .nav-links a.active { - color: var(--primary); + color: var(--primary); /* Cyan */ + + /* Effet de lueur autour du texte */ text-shadow: 0 0 10px rgba(0, 217, 255, 0.4); } -/* Footer */ + +/* ============================================================================= + 6. PIED DE PAGE (Footer) + ============================================================================= + + Zone en bas de chaque page avec les liens et le copyright. +============================================================================= */ + .site-footer { padding: 20px; text-align: center; color: var(--text-dim); font-size: 0.85rem; + + /* Bordure fine en haut */ border-top: 1px solid rgba(255, 255, 255, 0.05); + + /* Fond légèrement transparent */ background: rgba(10, 14, 39, 0.3); + + /* Pousse le footer en bas (avec flex sur body) */ margin-top: auto; } +/* ------------------------------------------------------------------------- + LIENS DU FOOTER + ------------------------------------------------------------------------- */ .footer-links { margin-bottom: 10px; + + /* Flexbox pour aligner les liens horizontalement */ display: flex; justify-content: center; - gap: 15px; + gap: 10px 15px; /* Espacement vertical et horizontal */ align-items: center; + flex-wrap: wrap; /* Retour à la ligne si nécessaire */ + + /* Limites de largeur */ + max-width: 100%; + padding: 0 10px; } .footer-links a { - color: var(--primary); + color: var(--primary); /* Liens en cyan */ font-weight: 500; } +/* Séparateurs entre les liens (•) */ .footer-links span { - color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.2); /* Très transparent */ } -/* Utility Classes */ + +/* ============================================================================= + 7. CLASSES UTILITAIRES + ============================================================================= + + Classes réutilisables pour la mise en page. +============================================================================= */ + +/* ------------------------------------------------------------------------- + CONTAINER + Limite la largeur du contenu et le centre + ------------------------------------------------------------------------- */ .container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; - width: 100%; + max-width: 1200px; /* Largeur maximale */ + margin: 0 auto; /* Centré horizontalement */ + padding: 2rem; /* Espacement interne */ + width: 100%; /* Prend toute la largeur disponible */ } +/* ------------------------------------------------------------------------- + BOUTON PRINCIPAL + ------------------------------------------------------------------------- */ .btn { display: inline-block; padding: 12px 30px; + + /* Dégradé cyan vers violet */ background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); + color: white; - border-radius: 8px; + border-radius: 8px; /* Coins arrondis */ font-weight: 600; + + /* Ombre portée cyan */ box-shadow: 0 4px 15px rgba(0, 217, 255, 0.3); + border: none; + + /* Animation au survol */ transition: transform 0.2s, box-shadow 0.2s; } .btn:hover { + /* Légère élévation au survol */ transform: translateY(-2px); + + /* Ombre plus prononcée */ box-shadow: 0 6px 20px rgba(0, 217, 255, 0.5); } -/* Animations */ + +/* ============================================================================= + 8. ANIMATIONS + ============================================================================= + + Définitions des animations CSS. +============================================================================= */ + +/* ------------------------------------------------------------------------- + ANIMATION: fadeIn (apparition progressive) + + Fait apparaître un élément en le faisant : + 1. Passer de transparent à opaque + 2. Glisser de bas en haut + ------------------------------------------------------------------------- */ @keyframes fadeIn { + /* État initial */ from { - opacity: 0; - transform: translateY(20px); + opacity: 0; /* Invisible */ + transform: translateY(20px); /* Décalé vers le bas */ } + /* État final */ to { - opacity: 1; - transform: translateY(0); + opacity: 1; /* Visible */ + transform: translateY(0); /* Position normale */ } } +/* Classe pour appliquer l'animation */ .fade-in { animation: fadeIn 0.8s ease-out forwards; + /* fadeIn : nom de l'animation + 0.8s : durée + ease-out : ralentit vers la fin + forwards : garde l'état final */ } -/* Elevator Buttons */ + +/* ============================================================================= + 9. BOUTONS DE NAVIGATION RAPIDE (Elevator) + ============================================================================= + + Deux boutons flottants en bas à droite : + - ▲ : Remonter en haut de la page + - ▼ : Descendre en bas de la page + + Ces boutons sont visibles sur toutes les pages et permettent + de naviguer rapidement dans les pages longues. +============================================================================= */ + +/* Conteneur des boutons */ .elevator { - position: fixed; - right: 20px; - bottom: 40px; + position: fixed; /* Position fixe (ne bouge pas au défilement) */ + right: 20px; /* 20px du bord droit */ + bottom: 40px; /* 40px du bas */ + + /* Colonne verticale */ display: flex; flex-direction: column; - gap: 10px; - z-index: 1000; + gap: 10px; /* Espacement entre les boutons */ + + z-index: 1000; /* Au-dessus des autres éléments */ } +/* Style d'un bouton */ .elevator-btn { width: 40px; height: 40px; + + /* Fond semi-transparent */ background: var(--bg-card); + + /* Bordure cyan transparente */ border: 1px solid rgba(0, 217, 255, 0.2); - border-radius: 50%; - color: var(--primary); + + border-radius: 50%; /* Rond */ + color: var(--primary); /* Icône en cyan */ + + /* Centrer l'icône */ display: flex; align-items: center; justify-content: center; + cursor: pointer; + + /* Effet de flou sur le fond */ backdrop-filter: blur(5px); + + /* Animation au survol */ transition: all 0.3s ease; + + /* Ombre portée */ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); } +/* Bouton au survol */ .elevator-btn:hover { - background: var(--primary); - color: white; + background: var(--primary); /* Fond cyan */ + color: white; /* Icône blanche */ + + /* Légère augmentation de taille */ transform: scale(1.1); + + /* Lueur cyan */ box-shadow: 0 0 15px var(--primary-glow); } -.elevator-btn.bottom { - bottom: 20px; -} \ No newline at end of file + +/* ============================================================================= + 10. STYLES RESPONSIVE (Mobile) + ============================================================================= + + Ces styles s'appliquent uniquement sur les petits écrans. + + @media (max-width: 480px) signifie : + "Appliquer ces styles si la largeur de l'écran est de 480px ou moins" + + 480px est une taille typique pour les smartphones en mode portrait. +============================================================================= */ + +@media (max-width: 480px) { + /* ------------------------------------------------------------------------- + FOOTER MOBILE + Sur mobile, les liens du footer sont organisés en grille 3x2 + au lieu d'être en ligne + ------------------------------------------------------------------------- */ + .footer-links { + display: grid; + + /* 3 colonnes de taille égale */ + grid-template-columns: repeat(3, 1fr); + + gap: 10px; + text-align: center; + } + + /* Cacher les séparateurs (•) sur mobile */ + .footer-links span { + display: none; + } + + /* Les liens prennent toute la largeur de leur cellule */ + .footer-links a { + display: block; + padding: 5px; + } +} diff --git a/data/subscribers.json b/data/subscribers.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/data/subscribers.json @@ -0,0 +1 @@ +[] diff --git a/docs/CONFIG_GUIDE.md b/docs/CONFIG_GUIDE.md new file mode 100644 index 000000000..f6231c45d --- /dev/null +++ b/docs/CONFIG_GUIDE.md @@ -0,0 +1,376 @@ +# Guide de Configuration - config.json + +**Version:** 1.0.0 +**Dernière mise à jour:** Février 2026 +**Fichier concerné:** `/config.json` + +--- + +## Table des matières + +1. [Présentation générale](#présentation-générale) +2. [Structure du fichier](#structure-du-fichier) +3. [Section "settings"](#section-settings) +4. [Section "categories"](#section-categories) +5. [Section "keywords"](#section-keywords) +6. [Comment ajouter une source](#comment-ajouter-une-source) +7. [Comment créer une catégorie](#comment-créer-une-catégorie) +8. [Comment modifier l'ordre des catégories](#comment-modifier-lordre-des-catégories) +9. [Comment ajouter des mots-clés](#comment-ajouter-des-mots-clés) + +--- + +## Présentation générale + +Le fichier `config.json` est le **cerveau** de AI-Pulse. Il contrôle : +- Quelles sources d'actualités sont récupérées (flux RSS) +- Comment les articles sont organisés par catégorie +- Les mots-clés utilisés pour filtrer et classer les articles +- Les paramètres de déduplication (éviter les doublons) + +**Emplacement:** `/config.json` (à la racine du projet) + +--- + +## Structure du fichier + +Le fichier est divisé en **3 sections principales** : + +```json +{ + "settings": { ... }, // Paramètres généraux + "categories": { ... }, // Catégories et sources RSS + "keywords": { ... } // Mots-clés pour le filtrage +} +``` + +--- + +## Section "settings" + +Cette section contient les **paramètres généraux** de l'agrégateur. + +```json +"settings": { + "articlesPerFeed": 15, + "maxArticlesPerCategory": 30, + "summaryMaxLength": 600, + "deduplication": { + "enabled": true, + "similarityThreshold": 0.7, + "contentThreshold": 0.5 + } +} +``` + +### Explication de chaque paramètre : + +| Paramètre | Valeur par défaut | Description | +|-----------|-------------------|-------------| +| `articlesPerFeed` | 15 | Nombre maximum d'articles récupérés **par source RSS**. Si une source a 50 articles, seuls les 15 plus récents sont gardés. | +| `maxArticlesPerCategory` | 30 | Nombre maximum d'articles affichés **par catégorie** sur le site. Même si on a 100 articles IA, on n'en montre que 30. | +| `summaryMaxLength` | 600 | Longueur maximale du résumé d'un article (en caractères). Les résumés trop longs sont coupés. | + +### Sous-section "deduplication" : + +La déduplication empêche d'afficher le même article plusieurs fois (quand plusieurs sources parlent de la même news). + +| Paramètre | Valeur | Description | +|-----------|--------|-------------| +| `enabled` | true/false | Active ou désactive la détection des doublons. Mettre `true` pour éviter les répétitions. | +| `similarityThreshold` | 0.7 | Seuil de similarité des titres (0 à 1). À 0.7, deux titres similaires à 70% sont considérés comme doublons. | +| `contentThreshold` | 0.5 | Seuil de similarité du contenu (0 à 1). Vérifie si le contenu des articles est identique. | + +**Conseil :** Plus le seuil est élevé (proche de 1), plus il faut que les articles soient identiques pour être considérés comme doublons. + +--- + +## Section "categories" + +Cette section définit les **catégories d'actualités** et leurs **sources RSS**. + +### Structure d'une catégorie : + +```json +"ai": { // Identifiant unique (en minuscules, sans espaces) + "labels": { + "en": "AI - Artificial Intelligence", // Nom affiché en anglais + "fr": "IA - Intelligence Artificielle" // Nom affiché en français + }, + "icon": "brain", // Icône (nom Feather Icons) + "priority": 1, // Ordre d'affichage (1 = premier) + "feeds": [ // Liste des sources RSS + { + "name": "TechCrunch AI", // Nom de la source + "url": "https://techcrunch.com/.../feed/", // URL du flux RSS + "tags": ["AI", "Startups", "Tech"], // Tags pour le filtrage + "lang": "en" // Langue (en = anglais, fr = français) + } + ] +} +``` + +### Liste des catégories actuelles : + +| ID | Nom français | Priorité | Description | +|----|--------------|----------|-------------| +| `ai` | IA - Intelligence Artificielle | 1 | Actualités sur l'IA, ChatGPT, LLM, etc. | +| `cybersecurity` | Cybersécurité | 2 | Sécurité informatique, failles, alertes | +| `iot` | Internet des Objets | 3 | Raspberry Pi, Arduino, domotique | +| `windows` | Windows | 4 | Actualités Microsoft Windows | +| `mac` | Mac / Apple | 5 | Actualités Apple, macOS, iOS | +| `linux` | Linux | 6 | Distributions Linux, open source | +| `tech` | Tech Générale | 7 | Actualités tech générales | +| `entrepreneurship` | Entrepreneuriat | 8 | Startups, levées de fonds | +| `finance` | Bourse & Finance | 9 | Marchés financiers, actions | +| `crypto` | Crypto & Blockchain | 10 | Bitcoin, Ethereum, DeFi | +| `opensource` | Open Source & GitHub | 11 | Projets GitHub, outils open source | +| `products` | Produits & Innovation | 12 | Nouveaux produits, Product Hunt | + +### Icônes disponibles : + +Les icônes utilisent la bibliothèque [Feather Icons](https://feathericons.com/). Voici les plus courantes : + +- `brain` : cerveau (IA) +- `shield` : bouclier (sécurité) +- `cpu` : processeur (IoT) +- `monitor` : écran (Windows) +- `apple` : pomme (Mac) +- `terminal` : terminal (Linux) +- `zap` : éclair (tech) +- `rocket` : fusée (startups) +- `trending-up` : courbe montante (finance) +- `link` : chaîne (crypto) +- `git-branch` : branche git (open source) +- `package` : paquet (produits) + +--- + +## Section "keywords" + +Les mots-clés servent à **identifier** et **classer** les articles. + +### Structure : + +```json +"keywords": { + "universal": [...], // Mots-clés utilisés dans toutes les langues + "localized": {...} // Mots-clés traduits (anglais/français) +} +``` + +### Mots-clés universels : + +Ce sont des mots qui s'écrivent pareil en anglais et français : + +```json +"universal": [ + "LLM", "GPT", "ChatGPT", "Docker", "Python", "GitHub", ... +] +``` + +### Mots-clés localisés : + +Ce sont des mots traduits selon la langue : + +```json +"localized": { + "artificial_intelligence": { + "en": "Artificial Intelligence", + "fr": "Intelligence Artificielle" + } +} +``` + +--- + +## Comment ajouter une source + +### Étape 1 : Trouver le flux RSS de la source + +La plupart des sites ont un flux RSS. L'URL se termine souvent par `/feed/`, `/rss/`, ou `/rss.xml`. + +Exemples : +- `https://example.com/feed/` +- `https://example.com/rss.xml` +- `https://example.com/blog/feed` + +### Étape 2 : Ajouter la source dans la bonne catégorie + +Ouvrir `config.json` et trouver la catégorie appropriée. Ajouter un nouvel objet dans le tableau `feeds` : + +```json +{ + "name": "Nom de la source", + "url": "https://example.com/feed/", + "tags": ["Tag1", "Tag2", "Tag3"], + "lang": "fr" +} +``` + +### Champs obligatoires : + +| Champ | Description | Exemple | +|-------|-------------|---------| +| `name` | Nom affiché de la source | "Le Monde Tech" | +| `url` | URL du flux RSS | "https://lemonde.fr/tech/rss.xml" | +| `tags` | Liste de 3 tags maximum | ["Tech", "France", "News"] | +| `lang` | Code langue | "fr" ou "en" | + +### Exemple complet : + +Pour ajouter "Le Monde Tech" dans la catégorie "tech" : + +```json +"tech": { + "feeds": [ + // ... sources existantes ... + { + "name": "Le Monde Tech", + "url": "https://www.lemonde.fr/pixels/rss_full.xml", + "tags": ["Tech", "France", "Actualités"], + "lang": "fr" + } + ] +} +``` + +--- + +## Comment créer une catégorie + +### Étape 1 : Choisir un identifiant unique + +L'identifiant doit être : +- En minuscules +- Sans espaces (utiliser des tirets si besoin) +- Court et descriptif + +Exemples : `gaming`, `science`, `mobile`, `cloud` + +### Étape 2 : Ajouter la catégorie dans config.json + +```json +"categories": { + // ... catégories existantes ... + + "gaming": { + "labels": { + "en": "Gaming & Esports", + "fr": "Jeux Vidéo & Esport" + }, + "icon": "play", + "priority": 13, + "feeds": [ + { + "name": "Jeuxvideo.com", + "url": "https://www.jeuxvideo.com/rss/rss.xml", + "tags": ["Gaming", "Actualités", "France"], + "lang": "fr" + } + ] + } +} +``` + +--- + +## Comment modifier l'ordre des catégories + +L'ordre d'affichage est contrôlé par le champ `priority`. + +- `priority: 1` = affiché en premier +- `priority: 2` = affiché en deuxième +- etc. + +### Pour changer l'ordre : + +1. Ouvrir `config.json` +2. Trouver la catégorie à déplacer +3. Modifier sa valeur `priority` +4. Ajuster les autres priorités si nécessaire + +### Exemple : + +Pour mettre "Cybersécurité" en premier et "IA" en deuxième : + +```json +"ai": { + "priority": 2, // Était 1, devient 2 + ... +}, +"cybersecurity": { + "priority": 1, // Était 2, devient 1 + ... +} +``` + +--- + +## Comment ajouter des mots-clés + +### Ajouter un mot-clé universel : + +Si le mot s'écrit pareil en anglais et français, l'ajouter dans `universal` : + +```json +"universal": [ + "LLM", "GPT", "ChatGPT", ..., + "NouveauMotClé" // Ajouter ici +] +``` + +### Ajouter un mot-clé traduit : + +Si le mot a des traductions différentes : + +```json +"localized": { + // ... mots existants ... + + "augmented_reality": { + "en": "Augmented Reality", + "fr": "Réalité Augmentée" + } +} +``` + +--- + +## Conseils et bonnes pratiques + +1. **Toujours tester les flux RSS** avant de les ajouter. Utiliser un lecteur RSS ou ouvrir l'URL dans un navigateur. + +2. **Ne pas surcharger les catégories** : 10-15 sources par catégorie est un bon maximum. + +3. **Équilibrer les langues** : essayer d'avoir autant de sources françaises qu'anglaises. + +4. **Sauvegarder avant de modifier** : faire une copie de `config.json` avant toute modification importante. + +5. **Valider le JSON** : utiliser un outil comme [JSONLint](https://jsonlint.com/) pour vérifier que le fichier est valide après modification. + +--- + +## Résolution de problèmes + +### "Le flux RSS ne fonctionne pas" + +- Vérifier que l'URL est correcte +- Tester l'URL dans un navigateur +- Certains sites bloquent les requêtes automatiques + +### "Les articles ne s'affichent pas" + +- Vérifier que la catégorie a bien une `priority` +- Vérifier que le flux RSS contient des articles récents +- Regarder les logs de l'agrégateur pour les erreurs + +### "Erreur de syntaxe JSON" + +- Vérifier les virgules (pas de virgule après le dernier élément d'une liste) +- Vérifier les guillemets (utiliser des guillemets droits " et non "") +- Utiliser un validateur JSON en ligne + +--- + +*Documentation générée pour AI-Pulse - Février 2026* diff --git a/feed-ai.xml b/feed-ai.xml new file mode 100644 index 000000000..60f951e0b --- /dev/null +++ b/feed-ai.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - AI - Artificial Intelligence + https://thephoenixagency.github.io/AI-Pulse + AI - Artificial Intelligence news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-crypto.xml b/feed-crypto.xml new file mode 100644 index 000000000..275a4f273 --- /dev/null +++ b/feed-crypto.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Crypto & Blockchain + https://thephoenixagency.github.io/AI-Pulse + Crypto & Blockchain news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-cybersecurity.xml b/feed-cybersecurity.xml new file mode 100644 index 000000000..dc89684fe --- /dev/null +++ b/feed-cybersecurity.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Cybersecurity + https://thephoenixagency.github.io/AI-Pulse + Cybersecurity news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-entrepreneurship.xml b/feed-entrepreneurship.xml new file mode 100644 index 000000000..3055a1010 --- /dev/null +++ b/feed-entrepreneurship.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Entrepreneurship + https://thephoenixagency.github.io/AI-Pulse + Entrepreneurship news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-finance.xml b/feed-finance.xml new file mode 100644 index 000000000..c842caddf --- /dev/null +++ b/feed-finance.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Stock Market & Finance + https://thephoenixagency.github.io/AI-Pulse + Stock Market & Finance news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-iot.xml b/feed-iot.xml new file mode 100644 index 000000000..14d42caba --- /dev/null +++ b/feed-iot.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - IoT - Internet of Things + https://thephoenixagency.github.io/AI-Pulse + IoT - Internet of Things news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-linux.xml b/feed-linux.xml new file mode 100644 index 000000000..2fb2bc7b8 --- /dev/null +++ b/feed-linux.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Linux + https://thephoenixagency.github.io/AI-Pulse + Linux news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-mac.xml b/feed-mac.xml new file mode 100644 index 000000000..fbdba55d9 --- /dev/null +++ b/feed-mac.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Mac / Apple + https://thephoenixagency.github.io/AI-Pulse + Mac / Apple news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-opensource.xml b/feed-opensource.xml new file mode 100644 index 000000000..8ee9dd2e1 --- /dev/null +++ b/feed-opensource.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Open Source & GitHub + https://thephoenixagency.github.io/AI-Pulse + Open Source & GitHub news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-products.xml b/feed-products.xml new file mode 100644 index 000000000..e304620f4 --- /dev/null +++ b/feed-products.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Products & Innovation + https://thephoenixagency.github.io/AI-Pulse + Products & Innovation news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-tech.xml b/feed-tech.xml new file mode 100644 index 000000000..a74df623c --- /dev/null +++ b/feed-tech.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - General Tech + https://thephoenixagency.github.io/AI-Pulse + General Tech news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed-windows.xml b/feed-windows.xml new file mode 100644 index 000000000..fd79f8498 --- /dev/null +++ b/feed-windows.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - Windows + https://thephoenixagency.github.io/AI-Pulse + Windows news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/feed.xml b/feed.xml new file mode 100644 index 000000000..abd130c09 --- /dev/null +++ b/feed.xml @@ -0,0 +1,12 @@ + + + + AI-Pulse - All News + https://thephoenixagency.github.io/AI-Pulse + Curated tech news from AI-Pulse + en + Thu, 05 Feb 2026 17:30:38 GMT + + + + \ No newline at end of file diff --git a/index.html b/index.html index da6172ab3..1c9171578 100644 --- a/index.html +++ b/index.html @@ -1,44 +1,186 @@ + + + + + + + + + + AI-Pulse + + + + + + + + + + - + +
+ +
+ AI-PULSE + +

Your trusted AI & Cybersecurity news aggregator. Stay informed about the latest innovations and threats in real time.

+
+ +
📰
@@ -46,6 +188,11 @@

Premium Sources

Content aggregated from quality sources.

+
@@ -53,6 +200,11 @@

Auto Updates

Automatic refresh via webhook to never miss anything.

+
🔒
@@ -61,34 +213,92 @@

Cybersecurity

+
+

Ready to explore AI & cybersecurity news?

+ + Access the reader +

+ + Open in GitHub App +

- + + +
+
+ + +
- + + diff --git a/js/tracker.js b/js/tracker.js index 88bf29833..a731be7c7 100644 --- a/js/tracker.js +++ b/js/tracker.js @@ -1,93 +1,265 @@ /** - * AI-Pulse Statistics Tracker - * Handles local visitor tracking using localStorage. + * ================================================================================ + * AI-PULSE - GESTIONNAIRE DE STATISTIQUES ET PRÉFÉRENCES (tracker.js) + * ================================================================================ + * + * DESCRIPTION: + * Ce fichier gère toutes les données stockées localement dans le navigateur + * de l'utilisateur. Aucune donnée n'est envoyée à un serveur externe. + * + * FONCTIONNALITÉS: + * 1. Tracker : Statistiques de visite (pages vues, sessions) + * 2. PrefsManager : Préférences utilisateur (langue, filtres) + * 3. ReadHistory : Historique des articles lus + * 4. Bookmarks : Articles sauvegardés (favoris) + * + * STOCKAGE: + * Toutes les données sont stockées dans localStorage, qui est : + * - Local : Les données restent sur l'ordinateur de l'utilisateur + * - Persistant : Les données sont conservées même après fermeture du navigateur + * - Limité : Maximum ~5MB par site + * + * CONFIDENTIALITÉ: + * - Aucun cookie de tracking externe + * - Aucune donnée envoyée à des tiers + * - L'utilisateur peut effacer ses données à tout moment + * + * VERSION: 1.0.0 + * DERNIÈRE MISE À JOUR: Février 2026 + * ================================================================================ */ + +/** + * ============================================================================= + * OBJET : Tracker + * ============================================================================= + * + * DESCRIPTION: + * Gère les statistiques de visite du site. + * Permet de compter les pages vues, les sessions, etc. + * + * DONNÉES STOCKÉES (localStorage: 'ai_pulse_stats'): + * - visitorId : Identifiant unique (généré localement, pas traçable) + * - sessions : Nombre de visites (une session = une visite après 30min d'inactivité) + * - pageViews : Nombre total de pages vues + * - lastVisit : Date de la dernière visite + * - firstVisit : Date de la première visite + * - locations : Villes/pays d'où l'utilisateur a visité + * - articleClicks : Nombre d'articles cliqués + * + * CONFIDENTIALITÉ: + * Les données restent sur l'ordinateur de l'utilisateur. + * Le visitorId est généré localement et ne permet pas d'identifier la personne. + */ const Tracker = { + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : init() + * ------------------------------------------------------------------------- + * Initialise le tracker au chargement de la page. + * Appelée automatiquement à la fin de ce fichier. + */ init: function () { + // Enregistrer cette visite this.trackVisit(); + + // Afficher des infos de débogage dans la console this.logDebugInfo(); }, + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : generateUUID() + * ------------------------------------------------------------------------- + * Génère un identifiant unique universel (UUID v4). + * + * EXEMPLE DE RÉSULTAT: + * "a1b2c3d4-e5f6-4789-a012-b34567890abc" + * + * FORMAT: + * xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + * - Le 4 indique la version 4 (basée sur des nombres aléatoires) + * - x = chiffre hexadécimal (0-9, a-f) + * - y = 8, 9, a, ou b (variante DCE) + * + * UTILISATION: + * Cet ID est stocké localement et sert à distinguer les visiteurs + * dans les statistiques locales. Il ne permet PAS d'identifier + * la personne car il n'est pas partagé avec des serveurs. + */ generateUUID: function () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + // Générer un nombre aléatoire entre 0 et 15 + var r = Math.random() * 16 | 0; + + // Pour 'x': utiliser le nombre aléatoire directement + // Pour 'y': appliquer un masque pour obtenir 8, 9, a, ou b + var v = c == 'x' ? r : (r & 0x3 | 0x8); + + // Convertir en hexadécimal (base 16) return v.toString(16); }); }, + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : trackVisit() + * ------------------------------------------------------------------------- + * Enregistre une visite de page. + * + * LOGIQUE: + * 1. Récupère les stats existantes ou crée un nouvel objet + * 2. Vérifie si c'est une nouvelle session (>30min depuis la dernière visite) + * 3. Incrémente les compteurs + * 4. Récupère la localisation (optionnel, max 1x par semaine) + * 5. Sauvegarde dans localStorage + */ trackVisit: function () { + // Récupérer les stats existantes ou créer un nouvel objet let stats = JSON.parse(localStorage.getItem('ai_pulse_stats')) || { - visitorId: this.generateUUID(), - sessions: 0, - pageViews: 0, - lastVisit: 0, - firstVisit: Date.now(), - locations: [], - articleClicks: 0 + visitorId: this.generateUUID(), // ID unique pour ce navigateur + sessions: 0, // Nombre de sessions/visites + pageViews: 0, // Nombre de pages vues + lastVisit: 0, // Timestamp de la dernière visite + firstVisit: Date.now(), // Timestamp de la première visite + locations: [], // Liste des localisations + articleClicks: 0 // Nombre d'articles cliqués }; const now = Date.now(); - const SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes - // Check if new session + // Durée d'inactivité pour considérer une nouvelle session + // 30 minutes = 30 * 60 * 1000 millisecondes + const SESSION_TIMEOUT = 30 * 60 * 1000; + + // Si plus de 30 minutes depuis la dernière visite = nouvelle session if (now - stats.lastVisit > SESSION_TIMEOUT) { stats.sessions++; } + // Incrémenter le compteur de pages vues stats.pageViews++; + + // Mettre à jour le timestamp de dernière visite stats.lastVisit = now; - // Fetch location if not present or every 7 days (to avoid API spam) + // Récupérer la localisation si nécessaire + // (première visite ou plus de 7 jours depuis la dernière mise à jour) if (stats.locations.length === 0 || (now - (stats.lastLocUpdate || 0) > 7 * 24 * 60 * 60 * 1000)) { this.fetchLocation(stats); } else { localStorage.setItem('ai_pulse_stats', JSON.stringify(stats)); } - // Ensure stats are saved even if location fetch takes time + // Sauvegarder immédiatement (au cas où fetchLocation prend du temps) localStorage.setItem('ai_pulse_stats', JSON.stringify(stats)); - // Expose for other scripts + // Rendre les stats accessibles depuis d'autres scripts window.aiPulseStats = stats; window.aiPulseTracker = this; }, + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : fetchLocation(stats) + * ------------------------------------------------------------------------- + * Récupère la localisation approximative de l'utilisateur. + * + * FONCTIONNEMENT: + * Utilise l'API ipapi.co pour obtenir la ville et le pays + * basés sur l'adresse IP publique. + * + * CONFIDENTIALITÉ: + * - La requête est faite à un service tiers (ipapi.co) + * - Seules la ville et le pays sont stockés (pas l'IP) + * - Limité à 1 requête par semaine pour éviter le spam + * + * @param {Object} stats - Objet de statistiques à mettre à jour + */ fetchLocation: function (stats) { + // Appel à l'API de géolocalisation par IP fetch('https://ipapi.co/json/') .then(res => res.json()) .then(data => { + // Si erreur dans la réponse, ne rien faire if (data.error) return; - const locationStr = `${data.city}, ${data.country_name}`; + // Créer un objet avec les infos de localisation const locationObj = { - city: data.city, - country: data.country_name, - timestamp: Date.now() + city: data.city, // Ville + country: data.country_name, // Pays + timestamp: Date.now() // Quand cette info a été récupérée }; - // Add if not already in list (simple check) - const exists = stats.locations.some(l => l.city === data.city && l.country === data.country_name); + // Vérifier si cette localisation existe déjà + const exists = stats.locations.some(l => + l.city === data.city && l.country === data.country_name + ); + + // Ajouter seulement si nouvelle if (!exists) { - // Store full object now - if (typeof stats.locations[0] === 'string') stats.locations = []; // Reset old string format + // Réinitialiser si l'ancien format était une chaîne simple + if (typeof stats.locations[0] === 'string') { + stats.locations = []; + } stats.locations.push(locationObj); } + // Noter quand on a fait la dernière mise à jour stats.lastLocUpdate = Date.now(); + + // Sauvegarder localStorage.setItem('ai_pulse_stats', JSON.stringify(stats)); + console.log("Location updated:", locationObj); }) .catch(err => console.error("Location fetch failed:", err)); }, + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : trackArticleClick(articleData) + * ------------------------------------------------------------------------- + * Enregistre un clic sur un article. + * + * @param {Object} articleData - Données de l'article + * - articleData.url : URL de l'article + * - articleData.title : Titre de l'article + * + * ACTIONS: + * 1. Incrémente le compteur de clics + * 2. Ajoute l'article à l'historique de lecture + */ trackArticleClick: function (articleData) { let stats = this.getStats(); + + // Incrémenter le compteur de clics stats.articleClicks = (stats.articleClicks || 0) + 1; localStorage.setItem('ai_pulse_stats', JSON.stringify(stats)); + + // Ajouter à l'historique de lecture + if (articleData.url) { + ReadHistory.markRead(articleData.url, articleData.title || 'Unknown'); + } + console.log("Article tracked:", articleData.title); }, + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : getStats() + * ------------------------------------------------------------------------- + * Récupère les statistiques actuelles. + * + * @returns {Object} Objet contenant toutes les statistiques + */ getStats: function () { return JSON.parse(localStorage.getItem('ai_pulse_stats')) || { visitorId: 'Unknown', @@ -98,9 +270,436 @@ const Tracker = { }; }, + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : logDebugInfo() + * ------------------------------------------------------------------------- + * Affiche des informations de débogage dans la console du navigateur. + * Utile pour vérifier que le tracker fonctionne. + */ logDebugInfo: function () { console.log("AI-Pulse Tracker Active. Visitor ID:", this.getStats().visitorId); } }; + +/** + * ============================================================================= + * OBJET : PrefsManager + * ============================================================================= + * + * DESCRIPTION: + * Gère les préférences de l'utilisateur. + * Ces préférences sont utilisées pour filtrer les articles affichés. + * + * DONNÉES STOCKÉES (localStorage: 'ai_pulse_preferences'): + * - lang : Langue préférée ('all', 'en', 'fr') + * - categories : Catégories activées/désactivées + * - keywords : Mots-clés pour filtrer + * - maxArticles : Nombre maximum d'articles à afficher + */ +const PrefsManager = { + + // Clé utilisée pour stocker les préférences dans localStorage + STORAGE_KEY: 'ai_pulse_preferences', + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : getDefaults() + * ------------------------------------------------------------------------- + * Retourne les valeurs par défaut des préférences. + */ + getDefaults: function () { + return { + lang: 'all', // Toutes les langues + categories: {}, // Toutes les catégories (objet vide = toutes) + keywords: '', // Pas de filtrage par mots-clés + maxArticles: 30 // 30 articles maximum par catégorie + }; + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : load() + * ------------------------------------------------------------------------- + * Charge les préférences depuis localStorage. + * Si aucune préférence n'existe, retourne les valeurs par défaut. + * + * @returns {Object} Préférences de l'utilisateur + */ + load: function () { + try { + var stored = localStorage.getItem(this.STORAGE_KEY); + + if (stored) { + var parsed = JSON.parse(stored); + + // S'assurer que toutes les clés par défaut existent + var defaults = this.getDefaults(); + for (var key in defaults) { + if (!(key in parsed)) { + parsed[key] = defaults[key]; + } + } + return parsed; + } + } catch (e) { + // Erreur de parsing, ignorer et retourner les défauts + } + + return this.getDefaults(); + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : save(prefs) + * ------------------------------------------------------------------------- + * Sauvegarde les préférences dans localStorage. + * + * @param {Object} prefs - Préférences à sauvegarder + */ + save: function (prefs) { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(prefs)); + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : reset() + * ------------------------------------------------------------------------- + * Supprime les préférences sauvegardées. + * La prochaine lecture retournera les valeurs par défaut. + */ + reset: function () { + localStorage.removeItem(this.STORAGE_KEY); + } +}; + + +/** + * ============================================================================= + * OBJET : ReadHistory + * ============================================================================= + * + * DESCRIPTION: + * Suit les articles que l'utilisateur a déjà lus. + * Permet d'afficher une indication visuelle (ex: titre grisé). + * + * DONNÉES STOCKÉES (localStorage: 'ai_pulse_read_articles'): + * Tableau d'objets avec : + * - url : URL de l'article + * - title : Titre de l'article + * - readAt : Timestamp de lecture + * + * LIMITE: + * Maximum 500 articles pour éviter de surcharger localStorage. + */ +const ReadHistory = { + + // Clé utilisée pour stocker l'historique dans localStorage + STORAGE_KEY: 'ai_pulse_read_articles', + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : normalizeUrl(url) + * ------------------------------------------------------------------------- + * Normalise une URL pour la comparaison. + * Supprime les paramètres de requête et les slashs finaux. + * + * EXEMPLES: + * "https://site.com/article?id=1" → "https://site.com/article" + * "data/articles/abc123.html" → "article:abc123" + * + * @param {string} url - URL à normaliser + * @returns {string} URL normalisée + */ + normalizeUrl: function (url) { + if (!url) return ''; + + try { + // Pour les articles locaux, extraire le hash du nom de fichier + if (url.includes('data/articles/')) { + var match = url.match(/data\/articles\/([a-f0-9]+)\.html/); + if (match) return 'article:' + match[1]; + } + + // Pour les URLs externes, garder seulement le chemin sans paramètres + var parsed = new URL(url, window.location.origin); + return parsed.origin + parsed.pathname.replace(/\/$/, ''); + } catch (e) { + // Fallback simple : couper au ? et supprimer le slash final + return url.split('?')[0].replace(/\/$/, ''); + } + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : normalizeTitle(title) + * ------------------------------------------------------------------------- + * Normalise un titre pour la comparaison. + * Convertit en minuscules et supprime les caractères spéciaux. + * + * @param {string} title - Titre à normaliser + * @returns {string} Titre normalisé + */ + normalizeTitle: function (title) { + if (!title) return ''; + + return title.toLowerCase() + .replace(/[^\w\s]/g, ' ') // Remplacer les caractères spéciaux par des espaces + .replace(/\s+/g, ' ') // Regrouper les espaces multiples + .trim(); // Supprimer les espaces aux extrémités + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : getAll() + * ------------------------------------------------------------------------- + * Récupère tous les articles lus. + * + * @returns {Array} Liste des articles lus + */ + getAll: function () { + try { + return JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []; + } catch (e) { + return []; + } + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : isRead(url, title) + * ------------------------------------------------------------------------- + * Vérifie si un article a déjà été lu. + * Compare par URL ET par titre (pour détecter les doublons). + * + * @param {string} url - URL de l'article + * @param {string} title - Titre de l'article + * @returns {boolean} true si l'article a été lu + */ + isRead: function (url, title) { + var normalizedUrl = this.normalizeUrl(url); + var normalizedTitle = this.normalizeTitle(title); + var self = this; + + return this.getAll().some(function (a) { + // Correspondance par URL OU par titre similaire + var urlMatch = self.normalizeUrl(a.url) === normalizedUrl; + var titleMatch = normalizedTitle && self.normalizeTitle(a.title) === normalizedTitle; + return urlMatch || titleMatch; + }); + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : markRead(url, title) + * ------------------------------------------------------------------------- + * Marque un article comme lu. + * + * @param {string} url - URL de l'article + * @param {string} title - Titre de l'article + */ + markRead: function (url, title) { + var articles = this.getAll(); + + // Vérifier si pas déjà lu + if (!this.isRead(url, title)) { + // Ajouter l'article + articles.push({ + url: url, + title: title, + readAt: Date.now() + }); + + // Garder maximum 500 entrées (supprimer les plus anciennes) + if (articles.length > 500) { + articles = articles.slice(-500); + } + + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(articles)); + } + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : getCount() + * ------------------------------------------------------------------------- + * Retourne le nombre d'articles lus. + */ + getCount: function () { + return this.getAll().length; + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : clear() + * ------------------------------------------------------------------------- + * Efface tout l'historique de lecture. + */ + clear: function () { + localStorage.removeItem(this.STORAGE_KEY); + } +}; + + +/** + * ============================================================================= + * OBJET : Bookmarks + * ============================================================================= + * + * DESCRIPTION: + * Gère les articles sauvegardés (favoris) par l'utilisateur. + * + * DONNÉES STOCKÉES (localStorage: 'ai_pulse_bookmarks'): + * Tableau d'objets avec : + * - url : URL de l'article + * - title : Titre de l'article + * - source : Nom de la source + * - savedAt : Timestamp de sauvegarde + */ +const Bookmarks = { + + // Clé utilisée pour stocker les favoris dans localStorage + STORAGE_KEY: 'ai_pulse_bookmarks', + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : getAll() + * ------------------------------------------------------------------------- + * Récupère tous les articles favoris. + * + * @returns {Array} Liste des favoris + */ + getAll: function () { + try { + return JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []; + } catch (e) { + return []; + } + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : isBookmarked(url) + * ------------------------------------------------------------------------- + * Vérifie si un article est dans les favoris. + * + * @param {string} url - URL de l'article + * @returns {boolean} true si l'article est favori + */ + isBookmarked: function (url) { + return this.getAll().some(function (b) { + return b.url === url; + }); + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : toggle(url, title, source) + * ------------------------------------------------------------------------- + * Ajoute ou retire un article des favoris. + * + * @param {string} url - URL de l'article + * @param {string} title - Titre de l'article + * @param {string} source - Nom de la source + * @returns {boolean} true si ajouté, false si retiré + */ + toggle: function (url, title, source) { + var bookmarks = this.getAll(); + + // Chercher si l'article existe déjà + var index = bookmarks.findIndex(function (b) { + return b.url === url; + }); + + if (index >= 0) { + // L'article existe, le supprimer + bookmarks.splice(index, 1); + } else { + // L'article n'existe pas, l'ajouter + bookmarks.push({ + url: url, + title: title || '', + source: source || '', + savedAt: Date.now() + }); + } + + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(bookmarks)); + + // Retourner true si ajouté, false si supprimé + return index < 0; + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : remove(url) + * ------------------------------------------------------------------------- + * Supprime un article des favoris. + * + * @param {string} url - URL de l'article à supprimer + */ + remove: function (url) { + var bookmarks = this.getAll().filter(function (b) { + return b.url !== url; + }); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(bookmarks)); + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : getCount() + * ------------------------------------------------------------------------- + * Retourne le nombre de favoris. + */ + getCount: function () { + return this.getAll().length; + }, + + + /** + * ------------------------------------------------------------------------- + * MÉTHODE : clear() + * ------------------------------------------------------------------------- + * Efface tous les favoris. + */ + clear: function () { + localStorage.removeItem(this.STORAGE_KEY); + } +}; + + +// ============================================================================= +// EXPOSITION GLOBALE +// ============================================================================= +// Rendre ces objets accessibles depuis d'autres scripts +// via window.PrefsManager, window.ReadHistory, window.Bookmarks + +window.PrefsManager = PrefsManager; +window.ReadHistory = ReadHistory; +window.Bookmarks = Bookmarks; + + +// ============================================================================= +// INITIALISATION +// ============================================================================= +// Démarrer le tracker automatiquement au chargement du script + Tracker.init(); diff --git a/js/ui.js b/js/ui.js index 79c975a3e..4313a249e 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1,24 +1,148 @@ /** - * AI-Pulse UI Manager - * Handles Custom Cursor and Common UI interactions. + * ================================================================================ + * AI-PULSE - GESTIONNAIRE D'INTERFACE UTILISATEUR (ui.js) + * ================================================================================ + * + * DESCRIPTION: + * Ce fichier gère les interactions de l'interface utilisateur communes + * à toutes les pages du site. + * + * FONCTIONNALITÉS: + * 1. Mise en surbrillance du lien de navigation actif + * 2. Curseur personnalisé (désactivé actuellement) + * + * UTILISATION: + * Ce script est chargé dans toutes les pages HTML via : + * + * + * Il s'exécute automatiquement quand la page est chargée. + * + * VERSION: 1.0.0 + * DERNIÈRE MISE À JOUR: Février 2026 + * ================================================================================ */ + +/** + * ============================================================================= + * POINT D'ENTRÉE PRINCIPAL + * ============================================================================= + * + * Ce bloc s'exécute quand le DOM (la structure HTML) est complètement chargé. + * + * POURQUOI ATTENDRE ? + * Si on exécute le code avant que le HTML soit chargé, les éléments + * comme .nav-links n'existeront pas encore et le code échouera. + * + * "DOMContentLoaded" garantit que tout le HTML est prêt. + */ document.addEventListener('DOMContentLoaded', () => { + // Initialiser le curseur personnalisé (désactivé) initCursor(); + + // Mettre en surbrillance le lien de la page actuelle highlightActiveLink(); }); + +/** + * ============================================================================= + * FONCTION : initCursor() + * ============================================================================= + * + * DESCRIPTION: + * Cette fonction était utilisée pour afficher un curseur personnalisé + * (un cercle animé qui suit la souris au lieu de la flèche standard). + * + * Elle est actuellement DÉSACTIVÉE car le curseur par défaut + * est plus adapté à l'accessibilité. + * + * POURQUOI DÉSACTIVÉ ? + * - Accessibilité : certains utilisateurs ont besoin du curseur standard + * - Performance : le curseur personnalisé consomme plus de ressources + * - Compatibilité : ne fonctionne pas sur tous les appareils + * + * POUR RÉACTIVER: + * Supprimer le "return;" et ajouter le code du curseur personnalisé. + */ function initCursor() { - // Custom cursor disabled by user request + // Curseur personnalisé désactivé sur demande de l'utilisateur return; + + /* CODE DU CURSEUR PERSONNALISÉ (commenté) + + // Créer un élément div pour le curseur + const cursor = document.createElement('div'); + cursor.id = 'cursor'; + document.body.appendChild(cursor); + + // Suivre la position de la souris + document.addEventListener('mousemove', (e) => { + cursor.style.left = e.clientX + 'px'; + cursor.style.top = e.clientY + 'px'; + }); + + */ } + +/** + * ============================================================================= + * FONCTION : highlightActiveLink() + * ============================================================================= + * + * DESCRIPTION: + * Met en surbrillance le lien de navigation correspondant à la page actuelle. + * Par exemple, si on est sur "about.html", le lien "About" sera en cyan. + * + * FONCTIONNEMENT: + * 1. Récupère le nom du fichier actuel depuis l'URL + * (ex: "https://site.com/about.html" → "about.html") + * 2. Parcourt tous les liens de navigation + * 3. Ajoute la classe "active" au lien correspondant + * + * CLASSE CSS "active": + * Définie dans style.css, elle change la couleur du lien en cyan + * et ajoute un effet de lueur. + * + * EXEMPLE: + * URL actuelle : https://site.com/about.html + * Lien trouvé : About + * Résultat : About + */ function highlightActiveLink() { + // ------------------------------------------------------------------------- + // ÉTAPE 1 : Récupérer le nom du fichier actuel + // ------------------------------------------------------------------------- + // window.location.pathname donne le chemin de l'URL + // Exemple : "/AI-Pulse/about.html" + // + // .split('/') divise par "/" : ["", "AI-Pulse", "about.html"] + // .pop() prend le dernier élément : "about.html" + // + // || 'index.html' : si le résultat est vide (page d'accueil sans nom), + // on utilise "index.html" par défaut const currentPath = window.location.pathname.split('/').pop() || 'index.html'; + + // ------------------------------------------------------------------------- + // ÉTAPE 2 : Sélectionner tous les liens de navigation + // ------------------------------------------------------------------------- + // document.querySelectorAll() trouve tous les éléments qui correspondent + // au sélecteur CSS donné. + // + // '.nav-links a' = tous les à l'intérieur d'un élément .nav-links const links = document.querySelectorAll('.nav-links a'); + // ------------------------------------------------------------------------- + // ÉTAPE 3 : Parcourir les liens et marquer celui qui correspond + // ------------------------------------------------------------------------- + // .forEach() exécute une fonction pour chaque élément de la liste links.forEach(link => { + // Comparer l'attribut href du lien avec le chemin actuel + // link.getAttribute('href') retourne la valeur de l'attribut href + // Exemple : "about.html" if (link.getAttribute('href') === currentPath) { + // Ajouter la classe "active" si ça correspond link.classList.add('active'); } }); diff --git a/package-lock.json b/package-lock.json index 88389f1b5..c5cd42973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "cors": "^2.8.6", "dompurify": "^3.3.1", "express": "^5.2.1", + "franc-min": "^6.2.0", "isomorphic-dompurify": "^2.35.0", "jimp": "^1.6.0", "jsdom": "^28.0.0", @@ -1059,6 +1060,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1632,6 +1643,19 @@ "node": ">= 0.6" } }, + "node_modules/franc-min": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/franc-min/-/franc-min-6.2.0.tgz", + "integrity": "sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==", + "license": "MIT", + "dependencies": { + "trigram-utils": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -2229,6 +2253,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/n-gram": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/n-gram/-/n-gram-2.0.2.tgz", + "integrity": "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3020,6 +3054,20 @@ "node": ">=20" } }, + "node_modules/trigram-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/trigram-utils/-/trigram-utils-2.0.1.tgz", + "integrity": "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==", + "license": "MIT", + "dependencies": { + "collapse-white-space": "^2.0.0", + "n-gram": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index 4754ad610..307445f10 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "cors": "^2.8.6", "dompurify": "^3.3.1", "express": "^5.2.1", + "franc-min": "^6.2.0", "isomorphic-dompurify": "^2.35.0", "jimp": "^1.6.0", "jsdom": "^28.0.0", @@ -38,4 +39,4 @@ "engines": { "node": ">=20.0.0" } -} \ No newline at end of file +} diff --git a/portfolio.html b/portfolio.html index ebbd00a51..80185827c 100644 --- a/portfolio.html +++ b/portfolio.html @@ -1,297 +1,787 @@ - + + + + + + + + + Portfolio - ThePhoenixAgency - - - + + + + + + + + + + + + - - - - -
- - -
- Profile + +
+
+ + Profile + +

ThePhoenixAgency

-

AI & Cybersecurity Specialist | Full Stack Developer | Open Source

+ +

🚀 AI & Cybersecurity Specialist | Full Stack Developer | Open Source Contributor

+ +
+
- + +
+ +
-

GitHub Statistics

+

+ 📊 + GitHub Statistics +

+

Real-time metrics from my GitHub profile

+
-
-
Repositories
+
Public Repositories
+ +
-
Followers
+ +
-
Following
+ +
-
-
Stars
+
Total Stars
- +
-

Featured Projects

+

+ 🌟 + Featured Projects +

+

My most impactful open-source contributions

-
-

Loading projects...

+ + +
+
+ 🤖 +

AI-Pulse

+
+

+ Real-time AI & Cybersecurity news aggregator with anonymous analytics. + Features iframe-based article reading, privacy-first tracking, and automated updates. +

+
+ JavaScript + Node.js + RSS + Analytics +
+ + View Project + + +
+ + +
+

Loading more projects from GitHub...

- +
-

About

-
-

- ThePhoenixAgency creates innovative solutions at the intersection of +

+ 🌐 + Official Website +

+

Explore ThePhoenixAgency.github.io

+ + + Visiter ThePhoenixAgency.github.io → + +
+ + +
+

+ 🏢 + Organization +

+

ThePhoenixAgency - Building the future of AI & Security

+ +
+

+ ThePhoenixAgency is dedicated to creating innovative solutions at the intersection + of artificial intelligence and cybersecurity. Our open-source projects focus on privacy-first - approaches, security by design, and accessible technology. + approaches, + security by design, and accessible technology for everyone.

- Visit GitHub Organization → + Visit Organization +
+ + +
+
- + - - + diff --git a/privacy.html b/privacy.html index 7bfd4377a..b1db92d0c 100644 --- a/privacy.html +++ b/privacy.html @@ -1,65 +1,206 @@ + + + + + + + + Privacy Policy | AI-Pulse + + + + + + + + + + + + - + - +
+ +

Privacy Policy

Last updated: January 15, 2026

+

At AI-Pulse, we take your privacy seriously. This Privacy Policy explains how we collect, use, and protect your information when you visit our website.

+

1. Information We Collect

We collect minimal, anonymized analytics data to improve our service:

    +
  • Anonymous visitor statistics: We track page views, session duration, and navigation patterns
  • + +
  • Location data: We collect anonymized city and country information (via basic IP lookup, not precise GPS)
  • + +
  • Technical data: Browser type, device type, and screen resolution
  • + +
  • Cookies: We use local storage for analytics purposes only
+

2. How We Use Your Information

We use collected data exclusively for:

    @@ -96,64 +288,156 @@

    2. How We Use Your Information

  • Improving user experience and website performance
  • Generating aggregate statistics
+ +

We do NOT:

    -
  • Sell or share your data with third parties
  • -
  • Track you across other websites
  • -
  • Collect personally identifiable information (PII)
  • -
  • Use advertising or marketing trackers
  • +
  • Sell or share your data with third parties
  • +
  • Track you across other websites
  • +
  • Collect personally identifiable information (PII)
  • +
  • Use advertising or marketing trackers
+

3. GDPR Compliance

AI-Pulse is fully compliant with the General Data Protection Regulation (GDPR):

    +
  • Data Minimization: We only collect essential analytics data
  • + +
  • Anonymization: All IP addresses are anonymized
  • + +
  • Consent: By using this static site, you consent to local storage usage for functional purposes
  • + +
  • Right to Access: You can see your data in the Statistics page
  • + +
  • Right to Deletion: Clearing your browser cache/storage deletes all data
+

4. Storage

We use LocalStorage to enhance your experience:

    +
  • Session Data: Temporary identifiers to prevent duplicate visitor counts in a single session
  • + +
  • Analytics Data: Stored locally on your device to show you your viewing history
+ +

You can clear this in your browser settings at any time.

+

5. Data Security

We implement industry-standard security measures:

    +
  • HTTPS encryption for all data transmission
  • + +
  • No backend database for this static deployment means no central data breach risk for your personal data
+

6. Third-Party Services

AI-Pulse does not use any third-party analytics (like Google Analytics). All analytics are processed locally.

+

7. Contact Us

If you have questions about this Privacy Policy or your data, please contact us via our GitHub repository.

+
+ + +
- + + - \ No newline at end of file + diff --git a/reader.html b/reader.html index b626dfc8a..43efc4952 100644 --- a/reader.html +++ b/reader.html @@ -1,159 +1,331 @@ + - + + + + + + + + + AI-Pulse Reader + + + + + + + + + + - + - + +
+
- +
+ +