Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion .github/workflows/news-article-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -1483,7 +1483,7 @@ Remember: You are producing world-class political journalism that informs Swedis
**Article Maintenance & Fixing:**
| Script | Usage | Description |
|--------|-------|-------------|
| `scripts/fix-article-navigation.py` | `python3 scripts/fix-article-navigation.py [--dry-run]` | **Fallback only** — adds language switcher + article-top-nav to articles missing them (idempotent) |
| `scripts/fix-article-navigation.ts` | `npx tsx scripts/fix-article-navigation.ts [--dry-run]` | **Fallback only** — adds language switcher + article-top-nav to articles missing them (idempotent) |
| `scripts/fix-language-switchers-and-css.py` | `python3 scripts/fix-language-switchers-and-css.py` | Updates switchers to show only existing languages, removes embedded CSS |
| `scripts/fix-mixed-language-descriptions.py` | `python3 scripts/fix-mixed-language-descriptions.py` | Fixes articles with mixed-language meta descriptions |

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-committee-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate Swedish Content & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-evening-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,7 @@ For deeper analysis, combine MCP tools: `search_voteringar` → `get_voting_grou
| Script | Usage | Description |
|--------|-------|-------------|
| `scripts/generate-news-enhanced.ts` | `npx tsx scripts/generate-news-enhanced.ts --types=evening-analysis --languages=LANGS` | Main article generator |
| `scripts/fix-article-navigation.py` | `python3 scripts/fix-article-navigation.py` | **Fallback only** — fix missing language switcher + article-top-nav (idempotent) |
| `scripts/fix-article-navigation.ts` | `npx tsx scripts/fix-article-navigation.ts` | **Fallback only** — fix missing language switcher + article-top-nav (idempotent) |
| `scripts/validate-news-generation.sh` | `bash scripts/validate-news-generation.sh` | Validate generated article structure |
| `scripts/mcp-setup.sh` | `source scripts/mcp-setup.sh` | Set MCP environment variables |
| `scripts/mcp-query-cli.ts` | `npx tsx scripts/mcp-query-cli.ts <tool> '<json>'` | Query individual MCP tools |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-month-ahead.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-monthly-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-motions.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-propositions.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-week-ahead.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/news-weekly-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ npx tsx scripts/generate-news-enhanced.ts \
These elements are validated by `bash scripts/validate-news-generation.sh` (Checks 8–10). The fix script is a **fallback only** — do not run it by default:
```bash
# FALLBACK ONLY — use if validate-news-generation.sh reports missing navigation elements
python3 scripts/fix-article-navigation.py
npx tsx scripts/fix-article-navigation.ts
```

### Step 4: Translate, Validate & Verify Analysis Quality
Expand Down
241 changes: 241 additions & 0 deletions scripts/fix-article-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
#!/usr/bin/env node
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The file is intended to be executed via npx tsx ..., but it has a #!/usr/bin/env node shebang. If someone runs the script directly (e.g., makes it executable and calls it), Node will try to execute TypeScript and fail. Align the shebang with the documented execution method (e.g., use a tsx-compatible shebang) or remove the shebang to avoid accidental misuse.

Suggested change
#!/usr/bin/env node
#!/usr/bin/env -S npx tsx

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 41bce96 — shebang updated to #!/usr/bin/env -S npx tsx.

/**
* Fix Article Navigation: Language Switcher + Back-to-News Top Nav
*
* This script ensures ALL news articles have:
* 1. A language switcher nav (14 languages) after <body>
* 2. An article-top-nav div with a localized back-to-news link before the article
*
* It auto-discovers all articles in the news/ directory and processes them
* idempotently — safe to run multiple times.
*
* Usage:
* npx tsx scripts/fix-article-navigation.ts
* npx tsx scripts/fix-article-navigation.ts --dry-run
*
* @author Hack23 AB
* @license Apache-2.0
*/

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

// ── Language configuration ────────────────────────────────────────────────

const LANGUAGES = ['en', 'sv', 'da', 'no', 'fi', 'de', 'fr', 'es', 'nl', 'ar', 'he', 'ja', 'ko', 'zh'] as const;
type Lang = typeof LANGUAGES[number];

const LANG_DISPLAY: Readonly<Record<Lang, [string, string]>> = {
en: ['🇬🇧', 'English'],
sv: ['🇸🇪', 'Svenska'],
da: ['🇩🇰', 'Dansk'],
no: ['🇳🇴', 'Norsk'],
fi: ['🇫🇮', 'Suomi'],
de: ['🇩🇪', 'Deutsch'],
fr: ['🇫🇷', 'Français'],
es: ['🇪🇸', 'Español'],
nl: ['🇳🇱', 'Nederlands'],
ar: ['🇸🇦', 'العربية'],
he: ['🇮🇱', 'עברית'],
ja: ['🇯🇵', '日本語'],
ko: ['🇰🇷', '한국어'],
zh: ['🇨🇳', '中文'],
} as const;

const LANG_SWITCHER_ARIA: Readonly<Record<Lang, string>> = {
en: 'Language', sv: 'Språk', da: 'Sprog', no: 'Språk',
fi: 'Kieli', de: 'Sprache', fr: 'Langue', es: 'Idioma',
nl: 'Taal', ar: 'اللغة', he: 'שפה', ja: '言語',
ko: '언어', zh: '语言',
} as const;

const BACK_TO_NEWS: Readonly<Record<Lang, string>> = {
en: 'Back to News', sv: 'Tillbaka till nyheter',
da: 'Tilbage til nyheder', no: 'Tilbake til nyheter',
fi: 'Takaisin uutisiin', de: 'Zurück zu Nachrichten',
fr: 'Retour aux actualités', es: 'Volver a noticias',
nl: 'Terug naar nieuws', ar: 'العودة إلى الأخبار',
he: 'חזרה לחדשות', ja: 'ニュースに戻る',
ko: '뉴스로 돌아가기', zh: '返回新闻',
} as const;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This script redefines language lists, display names, ARIA labels, and back-to-news translations locally. The repo already has centralized language metadata and HTML generators in scripts/article-template/helpers.ts (e.g., ALL_LANG_CODES, LANG_DISPLAY, LANG_SWITCHER_ARIA_LABELS, getNewsIndexFilename). Reusing those shared constants/helpers here would reduce duplication and prevent future drift between the generator and this fixer script.

Suggested change
// ── Language configuration ────────────────────────────────────────────────
const LANGUAGES = ['en', 'sv', 'da', 'no', 'fi', 'de', 'fr', 'es', 'nl', 'ar', 'he', 'ja', 'ko', 'zh'] as const;
type Lang = typeof LANGUAGES[number];
const LANG_DISPLAY: Readonly<Record<Lang, [string, string]>> = {
en: ['🇬🇧', 'English'],
sv: ['🇸🇪', 'Svenska'],
da: ['🇩🇰', 'Dansk'],
no: ['🇳🇴', 'Norsk'],
fi: ['🇫🇮', 'Suomi'],
de: ['🇩🇪', 'Deutsch'],
fr: ['🇫🇷', 'Français'],
es: ['🇪🇸', 'Español'],
nl: ['🇳🇱', 'Nederlands'],
ar: ['🇸🇦', 'العربية'],
he: ['🇮🇱', 'עברית'],
ja: ['🇯🇵', '日本語'],
ko: ['🇰🇷', '한국어'],
zh: ['🇨🇳', '中文'],
} as const;
const LANG_SWITCHER_ARIA: Readonly<Record<Lang, string>> = {
en: 'Language', sv: 'Språk', da: 'Sprog', no: 'Språk',
fi: 'Kieli', de: 'Sprache', fr: 'Langue', es: 'Idioma',
nl: 'Taal', ar: 'اللغة', he: 'שפה', ja: '言語',
ko: '언어', zh: '语言',
} as const;
const BACK_TO_NEWS: Readonly<Record<Lang, string>> = {
en: 'Back to News', sv: 'Tillbaka till nyheter',
da: 'Tilbage til nyheder', no: 'Tilbake til nyheter',
fi: 'Takaisin uutisiin', de: 'Zurück zu Nachrichten',
fr: 'Retour aux actualités', es: 'Volver a noticias',
nl: 'Terug naar nieuws', ar: 'العودة إلى الأخبار',
he: 'חזרה לחדשות', ja: 'ニュースに戻る',
ko: '뉴스로 돌아가기', zh: '返回新闻',
} as const;
import {
ALL_LANG_CODES,
LANG_DISPLAY as TEMPLATE_LANG_DISPLAY,
LANG_SWITCHER_ARIA_LABELS,
BACK_TO_NEWS as TEMPLATE_BACK_TO_NEWS,
} from './article-template/helpers';
// ── Language configuration ────────────────────────────────────────────────
const LANGUAGES = ALL_LANG_CODES;
type Lang = (typeof LANGUAGES)[number];
const LANG_DISPLAY: Readonly<Record<Lang, [string, string]>> = TEMPLATE_LANG_DISPLAY;
const LANG_SWITCHER_ARIA: Readonly<Record<Lang, string>> = LANG_SWITCHER_ARIA_LABELS;
const BACK_TO_NEWS: Readonly<Record<Lang, string>> = TEMPLATE_BACK_TO_NEWS;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 5d66764. Removed all locally-duplicated language constants (LANGUAGES, LANG_DISPLAY, LANG_SWITCHER_ARIA, BACK_TO_NEWS, newsIndexFor, generateLanguageSwitcher) and now imports ALL_LANG_CODES, FOOTER_LABELS from ./article-template/constants.js and getNewsIndexFilename, generateArticleLanguageSwitcher from ./article-template/helpers.js.


// ── Helpers ───────────────────────────────────────────────────────────────

function extractLang(filename: string): Lang | null {
const name = filename.replace(/\.html$/, '');
for (const lang of LANGUAGES) {
if (name.endsWith(`-${lang}`)) return lang;
}
return null;
}

function extractBase(filename: string): string | null {
const name = filename.replace(/\.html$/, '');
for (const lang of LANGUAGES) {
if (name.endsWith(`-${lang}`)) return name.slice(0, -(lang.length + 1));
}
return null;
}

function newsIndexFor(lang: Lang): string {
return lang === 'en' ? 'index.html' : `index_${lang}.html`;
}

function generateLanguageSwitcher(baseSlug: string, currentLang: Lang): string {
const aria = LANG_SWITCHER_ARIA[currentLang];
const lines: string[] = [` <nav class="language-switcher" role="navigation" aria-label="${aria}">`];
for (const lang of LANGUAGES) {
const [flag, name] = LANG_DISPLAY[lang];
const activeClass = lang === currentLang ? ' active' : '';
const ariaCurrent = lang === currentLang ? ' aria-current="page"' : '';
lines.push(
` <a href="${baseSlug}-${lang}.html" class="lang-link${activeClass}" hreflang="${lang}"${ariaCurrent}>${flag} ${name}</a>`,
);
}
lines.push(' </nav>');
return lines.join('\n');
}

function generateTopNav(lang: Lang): string {
const label = BACK_TO_NEWS[lang];
const index = newsIndexFor(lang);
return `\n<div class="article-top-nav">\n <a href="${index}" class="back-to-news">\n ← ${label}\n </a>\n</div>\n`;
}

// ── Processing ────────────────────────────────────────────────────────────

interface ProcessResult {
addedSwitcher: boolean;
addedTopnav: boolean;
}

function processArticle(filepath: string, baseSlug: string, lang: Lang, dryRun: boolean): ProcessResult {
const original = fs.readFileSync(filepath, 'utf-8');
let content = original;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

scripts/fix-article-navigation.ts introduces non-trivial HTML rewrite logic (switcher insertion/rewrite, top-nav insertion patterns, and --dry-run behavior) but has no direct unit tests. Given the repo has extensive Vitest coverage for other scripts, add tests that validate the transformation is idempotent and that both insertion patterns work on representative HTML inputs (including cases where switcher exists but top-nav/back-to-news is missing).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 5d66764 as tests/fix-article-navigation.test.ts (15 tests). The string-transformation logic was extracted into an exported pure transformContent() function, making it testable without file I/O. Tests cover: switcher insertion position, 14-language generation, active-lang marking, Pattern A/B top-nav insertion, missing back-to-news repair, and idempotency across 4 fixture variants.

let addedSwitcher = false;
let addedTopnav = false;

// ── 1. Language switcher ──────────────────────────────────────────
const hasSwitcher = content.includes('language-switcher');
if (!hasSwitcher) {
const switcherHtml = generateLanguageSwitcher(baseSlug, lang);
content = content.replace(/(<body>)/, `$1\n${switcherHtml}`);
addedSwitcher = true;
} else {
// Update existing switcher to have all 14 languages
const newSwitcher = generateLanguageSwitcher(baseSlug, lang);
content = content.replace(/<nav class="language-switcher"[^>]*>[\s\S]*?<\/nav>/, newSwitcher);
}

// ── 2. article-top-nav ────────────────────────────────────────────
const hasTopnav = content.includes('article-top-nav');
if (!hasTopnav) {
Comment on lines +98 to +114
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

processArticle() only adds article-top-nav when it's completely missing (hasTopnav). If an article already contains article-top-nav but is missing the required .back-to-news link (or has a wrong/unstyled link), this script will not fix it even though the header comment says it ensures a localized back-to-news link. Consider checking for back-to-news separately and either inserting the missing anchor or regenerating/replacing the existing top-nav block when the link is absent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5d66764. transformContent() now checks when article-top-nav is present but has no back-to-news link, replaces the entire div with a freshly generated localized one, and reports the count as Top nav fixed (missing back-to-news link) in the summary.

const topNavHtml = generateTopNav(lang);
let inserted = false;

// Pattern A: insert after closing </nav> of language-switcher, before article/div.news-article
if (content.includes('</nav>')) {
// Capture groups: (1)=full outer match, (2)=</nav>, (3)=whitespace, (4)=article/div opening tag
const navPattern = /((<\/nav>)([\s]*)(<(?:article|div)\s+class="(?:news-article|container)"))/s;
const match = navPattern.exec(content);
if (match) {
const endOfNav = match.index + match[2].length; // position right after </nav>
const whitespace = match[3];
const articleTag = match[4];
const afterFull = content.slice(match.index + match[0].length);
content = content.slice(0, endOfNav) + topNavHtml + whitespace + articleTag + afterFull;
inserted = true;
}
}

// Pattern B: insert directly before <article class="news-article"> or <article class="container">
if (!inserted) {
const articlePattern = /(<(?:article|div)\s+class="(?:news-article|container)")/;
const match = articlePattern.exec(content);
if (match) {
content = content.slice(0, match.index) + topNavHtml + '\n' + content.slice(match.index);
inserted = true;
}
}

if (inserted) addedTopnav = true;
}

// ── Write if changed ──────────────────────────────────────────────
if (content !== original && !dryRun) {
fs.writeFileSync(filepath, content, 'utf-8');
}

return { addedSwitcher, addedTopnav };
}

interface ArticleMap {
[baseSlug: string]: Partial<Record<Lang, string>>;
}

function discoverArticles(newsDir: string): ArticleMap {
const articles: ArticleMap = {};
const files = fs.readdirSync(newsDir).sort();
for (const name of files) {
if (!name.endsWith('.html') || name.startsWith('index')) continue;
const lang = extractLang(name);
const base = extractBase(name);
if (lang && base) {
if (!articles[base]) articles[base] = {};
articles[base][lang] = path.join(newsDir, name);
}
}
return articles;
}

function main(): void {
const dryRun = process.argv.includes('--dry-run');
if (dryRun) {
console.log('=== DRY RUN — no files will be modified ===\n');
}

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const newsDir = path.resolve(scriptDir, '..', 'news');

if (!fs.existsSync(newsDir)) {
console.error(`ERROR: news directory not found at ${newsDir}`);
process.exit(1);
}

console.log('=== Fix Article Navigation ===');
console.log(`News directory: ${newsDir}\n`);

const articles = discoverArticles(newsDir);
const slugs = Object.keys(articles).sort();
console.log(`Discovered ${slugs.length} unique article slugs\n`);

let total = 0;
let switchersAdded = 0;
let topnavsAdded = 0;

for (const baseSlug of slugs) {
const langFiles = articles[baseSlug];
for (const lang of LANGUAGES) {
const filepath = langFiles[lang];
if (!filepath) continue;
total++;
const { addedSwitcher, addedTopnav } = processArticle(filepath, baseSlug, lang, dryRun);
if (addedSwitcher) switchersAdded++;
if (addedTopnav) topnavsAdded++;
}
}

console.log('=== Summary ===');
console.log(`Total files processed: ${total}`);
console.log(`Language switchers added/updated: ${switchersAdded}`);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The summary prints “Language switchers added/updated”, but the counter is incremented only when a switcher is newly added (not when it’s updated/replaced). Either adjust the counter to include updates or change the summary label so the reported numbers are accurate.

Suggested change
console.log(`Language switchers added/updated: ${switchersAdded}`);
console.log(`Language switchers added: ${switchersAdded}`);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 41bce96 — label changed to Language switchers added: to match what the counter actually tracks.

console.log(`Top nav (article-top-nav) added: ${topnavsAdded}`);
if (dryRun) {
console.log('\n(Dry run — no files were modified)');
}
console.log('\n✓ Done!');
}

if (import.meta.url === `file://${process.argv[1]}`) {
main();
}
19 changes: 12 additions & 7 deletions vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,19 @@ export default defineConfig({
reporter: ['text', 'html', 'lcov', 'json'],
reportsDirectory: './builds/coverage',

// Disabled: tests don't import dashboard modules (by design)
all: false,
// Enabled: include all source files so zero-coverage modules are visible
all: true,

// Coverage thresholds
lines: 70,
functions: 70,
branches: 60,
statements: 70,
// Coverage thresholds — intentionally low during transition while all:true
// exposes previously-hidden zero-coverage files. Raise incrementally as
// tests are added for the dashboard and browser modules.
// Target thresholds (raise as coverage improves): lines:70, functions:70, branches:60, statements:70
thresholds: {
lines: 25,
functions: 20,
branches: 25,
statements: 25,
Comment on lines +32 to +40
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This comment block updates the coverage thresholds, but the rationale references “all:true” transition; please keep the comment and the numeric thresholds in sync if the baseline changes, otherwise it’s easy for thresholds to drift from reality.

Copilot uses AI. Check for mistakes.
},

// Include patterns
include: [
Expand Down
Loading