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 @@ +
+ + + + + + +
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 -