Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions .github/workflows/dependabot-secure-flow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,42 @@ jobs:
with:
fetch-depth: 0

- name: Ensure security branch exists
- name: Ensure security branch exists and is up to date
run: |
git fetch origin security 2>/dev/null || git switch --create security
git push origin security || true
git fetch origin main
if git fetch origin security 2>/dev/null; then
# Security branch exists, check if it's behind main
git checkout security
BEHIND=$(git rev-list --count HEAD..origin/main)
if [ "$BEHIND" -gt 50 ]; then
echo "Security branch is $BEHIND commits behind main. Resetting to main..."
git reset --hard origin/main
fi
Comment on lines +48 to +56
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dans l’étape "Ensure security branch exists", git checkout security peut échouer si la branche locale security n’existe pas (actions/checkout ne la crée pas forcément). Il faut checkout/mettre à jour explicitement depuis origin/security (ex: créer/forcer la branche locale à pointer sur origin/security) avant de calculer le retard et éventuellement reset.

Copilot uses AI. Check for mistakes.
else
# Security branch doesn't exist, create from main
echo "Creating security branch from main..."
git checkout -b security origin/main
fi
git push origin security --force-with-lease || git push origin security || true

- name: Merge dependabot changes to dependencies branch
- name: Merge dependabot changes to security branch
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

# Fetch the PR branch
git fetch origin ${{ github.head_ref }}:${{ github.head_ref }} || true

# Switch to security and merge
git switch security
git merge origin/${{ github.head_ref }} --no-edit || true
# Switch to security branch
git checkout security || git checkout -b security origin/main

# Try to merge, abort if conflicts
if ! git merge origin/${{ github.head_ref }} --no-edit; then
echo "Merge conflict detected, aborting and retrying with main as base..."
git merge --abort || true
git reset --hard origin/main
git merge origin/${{ github.head_ref }} --no-edit || true
Comment on lines +76 to +80
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

La fusion utilise git merge origin/${{ github.head_ref }}, mais juste avant le fetch est fait via ${head_ref}:${head_ref} (création d’une branche locale), ce qui ne garantit pas l’existence/à-jour de origin/${head_ref}. Résultat possible: merge qui échoue / merge d’une ref obsolète, puis push de security sans les changements Dependabot. Ajuster soit le fetch (pour alimenter refs/remotes/origin/${head_ref}), soit la commande merge pour cibler la branche locale ${{ github.head_ref }} ou FETCH_HEAD.

Suggested change
if ! git merge origin/${{ github.head_ref }} --no-edit; then
echo "Merge conflict detected, aborting and retrying with main as base..."
git merge --abort || true
git reset --hard origin/main
git merge origin/${{ github.head_ref }} --no-edit || true
if ! git merge ${{ github.head_ref }} --no-edit; then
echo "Merge conflict detected, aborting and retrying with main as base..."
git merge --abort || true
git reset --hard origin/main
git merge ${{ github.head_ref }} --no-edit || true

Copilot uses AI. Check for mistakes.
fi

# Push to security
git push origin security
Expand Down
125 changes: 22 additions & 103 deletions reader.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,81 +117,42 @@
}

#view-article .iframe-container {
padding-top: 48px;
padding-top: 0;
}

.article-topbar {
position: absolute;
top: 8px;
left: 10px;
right: 10px;
z-index: 2147483647;
display: flex;
gap: 8px;
align-items: center;
pointer-events: auto;
}

.article-topbar-btn {
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(15, 23, 42, 0.84);
color: #fff;
font-size: 12px;
font-weight: 600;
border-radius: 10px;
padding: 8px 12px;
cursor: pointer;
}

.article-topbar-btn:hover {
background: rgba(15, 23, 42, 0.95);
}

.back-to-previous {
position: absolute;
top: 12px;
left: 12px;
z-index: 40;
background: rgba(15, 23, 42, 0.78);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.18);
padding: 8px 12px;
border-radius: 10px;
font-size: 13px;
cursor: pointer;
backdrop-filter: blur(4px);
}

.back-to-previous:hover {
background: rgba(15, 23, 42, 0.9);
}

.reader-elevator {
position: fixed;
right: 12px;
top: 58px;
bottom: auto;
right: 20px;
bottom: 20px;
z-index: 2147483646;
display: flex;
flex-direction: column;
gap: 8px;
gap: 10px;
}

.reader-elevator-btn {
width: 36px;
height: 36px;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 10px;
background: rgba(15, 23, 42, 0.82);
color: #fff;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(0, 217, 255, 0.2);
border-radius: 50%;
color: #00d9ff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
line-height: 1;
backdrop-filter: blur(4px);
backdrop-filter: blur(5px);
transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
font-size: 1.2rem;
}

.reader-elevator-btn:hover {
background: rgba(15, 23, 42, 0.95);
background: #00d9ff;
color: white;
transform: scale(1.1);
box-shadow: 0 0 15px rgba(0, 217, 255, 0.5);
}

.article-mobile-footer {
Expand Down Expand Up @@ -332,16 +293,9 @@
padding: 12px 16px;
}

.back-to-previous {
top: 10px;
font-size: 12px;
padding: 7px 10px;
}

.reader-elevator {
right: 10px;
top: 56px;
bottom: auto;
bottom: 10px;
}

.article-mobile-footer {
Expand Down Expand Up @@ -393,11 +347,6 @@
</div>

<div class="iframe-container">
<div class="article-topbar" id="articleTopbar">
<button class="article-topbar-btn" id="articleBackBtn" type="button">Retour</button>
<button class="article-topbar-btn" id="articleHomeBtn" type="button">Accueil</button>
</div>
<button class="back-to-previous" id="backToPreviousBtn" type="button">Page précédente</button>
<div class="loading" id="loading">
<div class="spinner"></div>
<p class="loading-text">Chargement de l'article...</p>
Expand Down Expand Up @@ -875,36 +824,6 @@ <h2>Impossible de charger l'article</h2>
});
}

// Back to previous page in navigation history (fallback to list).
const backToPreviousBtn = document.getElementById('backToPreviousBtn');
const articleBackBtn = document.getElementById('articleBackBtn');
const articleHomeBtn = document.getElementById('articleHomeBtn');

function navigateBackOrHome() {
if (window.history.length > 1) {
window.history.back();
return;
}
const fallbackUrl = new URL(window.location.href);
fallbackUrl.search = '';
fallbackUrl.hash = '';
window.location.assign(fallbackUrl.toString());
}

if (backToPreviousBtn) {
backToPreviousBtn.addEventListener('click', navigateBackOrHome);
}
if (articleBackBtn) {
articleBackBtn.addEventListener('click', navigateBackOrHome);
}
if (articleHomeBtn) {
articleHomeBtn.addEventListener('click', () => {
const fallbackUrl = new URL(window.location.href);
fallbackUrl.search = '';
fallbackUrl.hash = '';
window.location.assign(fallbackUrl.toString());
});
}

function scrollArticleFrame(direction) {
const iframe = document.getElementById('articleFrame');
Expand Down
68 changes: 62 additions & 6 deletions src/aggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ function addUTMParams(url, category = 'general') {
if (mediumHosts.includes(hostname)) {
url = `https://freedium.cloud/${url}`;
}

// Liste des domaines avec paywalls stricts
const paywalledHosts = ['ft.com', 'wsj.com', 'economist.com', 'bloomberg.com', 'investing.com'];

// Ajouter Archive.ph en query parameter pour fallback
if (paywalledHosts.some(host => hostname.includes(host))) {
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le test hostname.includes(host) pour détecter des domaines paywallés est fragile (ex: notft.com matcherait ft.com). Le fichier a déjà un helper hostnameMatches(host, baseDomain) basé sur un match suffixe (host === domain || host.endsWith('.'+domain)); utilisez-le ici pour éviter des contournements et garder une logique cohérente.

Suggested change
if (paywalledHosts.some(host => hostname.includes(host))) {
if (paywalledHosts.some(host => hostnameMatches(hostname, host))) {

Copilot uses AI. Check for mistakes.
// On ne change pas l'URL ici, on l'utilisera comme fallback
}
Comment on lines +263 to +270
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le bloc paywalledHosts est actuellement un no-op (aucune action dans le if) et ajoute du code mort / un commentaire trompeur ("Ajouter Archive.ph"), ce qui rend addUTMParams() plus difficile à maintenir. Soit implémenter réellement le fallback (ex: retourner une URL archive, ou exposer l’URL de fallback via un champ séparé), soit supprimer entièrement ce bloc.

Suggested change
// Liste des domaines avec paywalls stricts
const paywalledHosts = ['ft.com', 'wsj.com', 'economist.com', 'bloomberg.com', 'investing.com'];
// Ajouter Archive.ph en query parameter pour fallback
if (paywalledHosts.some(host => hostname.includes(host))) {
// On ne change pas l'URL ici, on l'utilisera comme fallback
}

Copilot uses AI. Check for mistakes.
} catch (e) {
// Erreur de parsing URL, on continue sans modification
}
Expand Down Expand Up @@ -1408,9 +1416,42 @@ async function processArticle(article, sourceName, tags, category, feedLang) {
}
const lang = detectedLang || feedLang || 'en';

// Essayer d'utiliser Archive.ph ou autres services de bypass
const tryPaywallBypass = async (url) => {
const bypassServices = [
{
name: 'archive.ph',
transform: (u) => `https://archive.ph/?url=${encodeURIComponent(u)}`
},
{
name: 'scribe.rip',
transform: (u) => u.includes('medium.com') ? u.replace('medium.com', 'scribe.rip') : null
},
{
name: 'web.archive.org',
transform: (u) => `https://web.archive.org/web/*/${u}`
}
];

for (const service of bypassServices) {
const bypassUrl = service.transform(url);
if (!bypassUrl) continue;
try {
const response = await axios.get(bypassUrl, {
timeout: 5000,
headers: { 'User-Agent': 'AI-Pulse/3.0' }
});
return { success: true, html: response.data, service: service.name };
} catch (e) {
// Continuer vers le service suivant
}
}
return { success: false };
};

const writeFallbackLocalArticle = () => {
const safeTitle = sanitizeText(article.title) || 'Untitled';
const safeSummary = smartTruncate(cleanupNoiseText(rawSummary || ''), 1200) || 'Summary unavailable for this article.';
const safeSummary = smartTruncate(cleanupNoiseText(rawSummary || ''), 1200) || (rawSummary ? sanitizeText(rawSummary) : 'Summary unavailable for this article.');
const fallbackHtml = `<!DOCTYPE html>
<html lang="${lang}">
<head>
Expand Down Expand Up @@ -1518,11 +1559,26 @@ async function processArticle(article, sourceName, tags, category, feedLang) {

if (articleContent && articleContent.textContent) {
if (isPaywallText(articleContent.textContent)) {
writeFallbackLocalArticle();
} else {
if (isLikelyBoilerplateExtraction(articleContent.textContent)) {
writeFallbackLocalArticle();
// Essayer les services de bypass avant de renoncer
const bypassResult = await tryPaywallBypass(resolvedArticleUrl);
if (bypassResult.success) {
const bypassDom = createSafeDom(bypassResult.html, resolvedArticleUrl);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tryPaywallBypass() ne retourne pas l’URL réellement utilisée (bypassUrl), et plus bas createSafeDom(bypassResult.html, resolvedArticleUrl) utilise l’URL d’origine comme base. Pour des HTML provenant de archive.ph / web.archive.org, cela peut casser la résolution des liens relatifs et fausser les heuristiques Readability. Retourner bypassUrl (ou finalUrl) et l’utiliser comme second paramètre de createSafeDom afin que document.URL/baseURI reflète la page parsée.

Suggested change
const bypassDom = createSafeDom(bypassResult.html, resolvedArticleUrl);
// Utiliser l'URL réellement utilisée par le service de bypass (si fournie)
const bypassBaseUrl = bypassResult.finalUrl || bypassResult.bypassUrl || resolvedArticleUrl;
const bypassDom = createSafeDom(bypassResult.html, bypassBaseUrl);

Copilot uses AI. Check for mistakes.
const bypassReader = new Readability(bypassDom.window.document);
const bypassContent = bypassReader.parse();
if (bypassContent && bypassContent.textContent && !isPaywallText(bypassContent.textContent) && bypassContent.textContent.length > 200) {
articleContent = bypassContent;
// Continuer le traitement normal avec le contenu bypassed
} else {
writeFallbackLocalArticle();
}
} else {
writeFallbackLocalArticle();
}
} else {
// Contenu normal sans paywall
if (isLikelyBoilerplateExtraction(articleContent.textContent)) {
writeFallbackLocalArticle();
} else {
if (!computedSummary || computedSummary.trim().length < 20) {
computedSummary = trimPromotionalTailText(cleanupNoiseText(sanitizeText(articleContent.textContent.slice(0, 1400))));
}
Comment on lines 1577 to 1584
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

La logique imbriquée autour du cas "contenu normal" (paywall/boilerplate) a une indentation incohérente et des blocs else difficiles à lire (ex: if (!computedSummary...) n’est pas indenté sous le else). Merci de réindenter/reformater ce bloc pour éviter des erreurs futures lors de modifications (et faciliter la vérification des accolades/branches).

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -1633,10 +1689,10 @@ async function processArticle(article, sourceName, tags, category, feedLang) {

// Sauvegarder le fichier HTML localement
fs.writeFileSync(localPath, cleanHtml);
}
}
}
}
}
} catch (e) {
if (!shouldSuppressExtractionLog(resolvedArticleUrl, e)) {
console.error(` Could not extract content for: ${articleUrl}`);
Expand Down
Loading