Skip to content

Comments

Resolve merge conflict: preserve accessibility and cross-language features#156

Merged
pethers merged 19 commits intomainfrom
copilot/improve-multi-language-sync
Feb 14, 2026
Merged

Resolve merge conflict: preserve accessibility and cross-language features#156
pethers merged 19 commits intomainfrom
copilot/improve-multi-language-sync

Conversation

Copy link
Contributor

Copilot AI commented Feb 14, 2026

Address 7 PR Review Comments - All Fixed ✅

Summary

Fixed 7 issues identified in merge review: 2 security vulnerabilities, 2 bugs, 1 code cleanup, 1 documentation issue, and 1 outdated reference.

Changes

1. Fixed Regex Injection (#2807520445) ✅

File: scripts/article-quality-enhancer.js

Escape special regex characters in party variant strings:

const escapedVariant = variant.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`\\b${escapedVariant}\\b`, 'gi');

2. Added XSS Protection (#2807520455) ✅

Files: news/index*.html (all 14)

Added escapeHtml() helper and applied to all dynamic content:

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

3. Restored JSON-LD Sanitization (#2807520439) ✅

File: scripts/article-template.js

Restored newline/whitespace normalization:

"articleBody": "${escapeHtml(content).replace(/\n/g, ' ').replace(/\s+/g, ' ').substring(0, 500)}..."

4. Fixed Norwegian Language Code (#2807520430) ✅

Files: All 14 news/index*.html

Reverted hreflang from "nb" to "no":

<link rel="alternate" hreflang="no" href="...index_no.html">

5. Removed Dead Code (#2807520461) ✅

File: scripts/article-template.js

Deleted unused functions (349 lines):

  • generateArticleCSS() (337 lines)
  • getArticleStyles() (12 lines)

6. Updated Line Reference (#2807520467) ✅

File: scripts/generate-news-indexes.js

Corrected JSDoc:

 * passing articlesByLang[langKey] to generateIndexHTML() on line 1023.

7. Fixed Documentation (#2807520449) ✅

File: WORKFLOWS.md

Updated to match actual 5-hour threshold:

- ✅ Smart caching (skip if < 5 hours old)

Testing

✅ Regex injection: Tested with special characters
✅ XSS protection: Verified escaping works
✅ JSON-LD: Valid structured data
✅ Norwegian code: Consistent across all files
✅ Dead code removal: No broken references

Impact

  • Security: 2 vulnerabilities fixed (regex injection, XSS)
  • Data integrity: Valid JSON-LD structured data
  • Consistency: Norwegian language code standardized
  • Code quality: 349 lines of dead code removed
  • Documentation: Accurate and up-to-date

Status: All 7 comments addressed ✅

Original prompt

This section details on the original issue you should resolve

<issue_title>Improve news-realtime-monitor.md: Multi-language synchronization and quality enhancement</issue_title>
<issue_description>## 📋 Issue Type
Feature Enhancement / Bug Fix

🎯 Objective

Improve the news-realtime-monitor.md agentic workflow to ensure comprehensive multi-language synchronization, proper news index updates, and enhanced Economist-style quality standards for real-time Riksdag/Regering coverage.

📊 Current State

Workflow Analysis

File: .github/workflows/news-realtime-monitor.md
Schedule: Twice daily (10:00 UTC, 14:00 UTC) on weekdays
Model: claude-opus-4.6
MCP Server: riksdag-regering (https://riksdag-regering-ai.onrender.com/mcp)
Timeout: 20 minutes

Current Issues Identified

  1. Multi-Language Index Synchronization

    • Problem: News index files (news/index_*.html) for non-generated languages are NOT updated with new articles
    • Example: If English/Swedish articles are generated, news/index_fr.html won't contain links to these articles
    • Impact: French readers cannot discover English/Swedish breaking news
    • Root Cause: Language filtering in generate-news-indexes.js excludes articles based on filename suffix
  2. Quality Framework Missing

    • No systematic quality scoring (analytical depth, source quality, balance)
    • Missing "Why This Matters" section requirement
    • No minimum party perspective count (should be 4+)
    • No cross-referencing validation (should be 3+ sources)
  3. Workflow Coordination Gap

    • No state file (news/metadata/workflow-state.json) to track recent coverage
    • Risk of duplicate articles between realtime-monitor and evening-analysis
    • No MCP query caching (2-hour TTL recommended)
  4. Test Coverage

    • Missing tests for realtime-monitor-specific logic
    • No validation of breaking news threshold criteria
    • No tests for Playwright validation integration

Measured Metrics

  • News Index Files: 14 language files exist
  • Recent Articles: 22 articles in news/ directory (Feb 10-14)
  • Generated Languages: Primarily EN/SV, missing FR/DE/ES/NL in recent runs
  • Breaking News: 0 breaking articles generated in last 4 days
  • Quality Scores: Not implemented (no scoring system)

🚀 Desired State

Target Outcomes

  1. Cross-Language Article Discovery

    • ✅ All 14 news/index_*.html files show ALL articles regardless of language
    • ✅ Language badges indicate article availability ("Available in: EN, SV")
    • ✅ Readers can discover relevant content across languages
  2. Economist-Style Quality Framework

    • ✅ Every article has quality score ≥ 0.75
    • ✅ Analytical depth score ≥ 0.6
    • ✅ "Why This Matters" section (100-150 words)
    • ✅ 4+ party perspectives represented
    • ✅ 3+ cross-referenced sources with document IDs
    • ✅ Historical context included
  3. Workflow State Coordination

    • ✅ news/metadata/workflow-state.json tracks recent queries/articles
    • ✅ 2-hour MCP query caching prevents duplicate API calls
    • ✅ Similarity-based deduplication (>70% similar articles prevented)
  4. Comprehensive Testing

    • ✅ tests/news-realtime-monitor.test.js with 20+ test cases
    • ✅ Breaking news criteria validation
    • ✅ Multi-language index synchronization tests
    • ✅ Quality framework scoring tests

🔧 Implementation Approach

Phase 1: Multi-Language Index Fix (Priority: HIGH)

Modify: scripts/generate-news-indexes.js

// BEFORE (incorrect - filters by language)
const enArticles = allArticles.filter(a => a.file.endsWith('-en.html'));
const svArticles = allArticles.filter(a => a.file.endsWith('-sv.html'));

// AFTER (correct - show all articles in all indexes)
const allArticlesForIndex = allArticles.map(article => ({
  ...article,
  availableLanguages: detectAvailableLanguages(article.slug, allArticles),
  badge: article.language // Show which language this article is in
}));

// Generate index for each language showing ALL articles
for (const lang of LANGUAGES) {
  generateIndexHTML(lang, allArticlesForIndex);
}

Key Changes:

  • Remove language filtering in article gathering
  • Add availableLanguages detection per article slug
  • Show language badges for each article
  • Add "Available in: EN, SV, FR" indicator

Phase 2: Quality Framework Implementation

Create: scripts/article-quality-enhancer.js

Features:

  • Quality score calculation (0.0-1.0)
  • Analytical depth assessment
  • Source quality validation
  • Party perspective counting
  • Historical context detection

Integration:

// In news-realtime-monitor.md, after article generation
const qualityResult = await enhanceArticleQuality(articlePath, {
  minQualityScore: 0.75,
  minAnalyticalDepth: 0.6,
  minPartySources: 4,
  minCrossReferences: 3,
  requireWhyThisMatters: true
});

if (!qualityResult.passed) {
  console.warn('Quality check failed:', qua...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes Hack23/riksdagsmonitor#155

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for you](https://github.com/Hack23/riksdagsmonitor/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)  coding agent works faster and does higher quality work when set up for your repo.

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { WorkflowStateCoordinator, MCP_CACHE_TTL_SECONDS, SIMILARITY_THRESHOLD } from '../scripts/workflow-state-coordinator.js';
Copilot AI changed the title [WIP] Enhance multi-language synchronization and quality for news monitor Multi-language news index sync, quality framework, and workflow coordination Feb 14, 2026
Copilot AI requested a review from pethers February 14, 2026 00:41
Copilot AI changed the title Multi-language news index sync, quality framework, and workflow coordination Move news article CSS to external stylesheet and add editorial tagline Feb 14, 2026
@pethers pethers requested a review from Copilot February 14, 2026 01:03
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors news article CSS architecture from embedded styles (660 lines per article) to a centralized external stylesheet, adds editorial branding with a tagline, implements multi-language discovery features, and introduces quality framework and workflow coordination tools for the news generation system.

Changes:

  • Moved ~350 lines of article CSS from embedded <style> tags to external styles.css under .news-article namespace
  • Added "The Economist-style" editorial tagline to article headers
  • Implemented cross-language article discovery with language badges and "Available in" indicators across all 14 supported languages
  • Created workflow state coordinator for MCP query caching and article deduplication
  • Added article quality enhancer with analytical depth scoring, party perspective counting, and cross-reference validation
  • Added comprehensive test suites (362 lines for workflow coordination, 443 lines for realtime monitor quality)

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
styles.css Added 350 lines of .news-article styles (lines 8703-9049) for typography, event calendars, watch sections, responsive design, print optimization, and editorial tagline
scripts/article-template.js Removed generateArticleCSS() call, added HTML comment clarifying external CSS use, added site tagline div with editorial branding
scripts/generate-news-indexes.js Added getAllArticlesWithLanguageInfo(), generateLanguageBadge(), and generateAvailableLanguages() functions for cross-language discovery with 14-language support
news/index_*.html Updated all 14 language index files with lang and availableLanguages metadata, language badge rendering, and multi-language discovery UI
scripts/workflow-state-coordinator.js New 338-line module for MCP caching (2-hour TTL), similarity-based deduplication (70% threshold), and workflow coordination
scripts/article-quality-enhancer.js New 369-line module implementing Economist-style quality framework with analytical depth (0-1), party perspectives (4+), cross-references (3+), and quality scoring (0.75+ target)
tests/workflow-state-coordinator.test.js 362 lines of tests covering state management, MCP caching, duplicate detection, similarity calculations (25 test cases)
tests/news-realtime-monitor.test.js 443 lines of tests covering multi-language sync, quality metrics, party counting, cross-referencing (40+ test cases)

const availableText = '可用语言';
const availableBadges = availableLangs.map(l => {
const f = {en:'🇬🇧',sv:'🇸🇪',da:'🇩🇰',no:'🇳🇴',fi:'🇫🇮',de:'🇩🇪',fr:'🇫🇷',es:'🇪🇸',nl:'🇳🇱',ar:'🇸🇦',he:'🇮🇱',ja:'🇯🇵',ko:'🇰🇷',zh:'🇨🇳'}[l] || '🌐';
return `<span class="lang-badge-sm"><span aria-hidden="true">${f}</span> ${l.toUpperCase()}</span>`;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The CSS class lang-badge-sm is used in the JavaScript but is not defined in styles.css. This will result in unstyled language badges in the "Available in" section. You should either:

  1. Add .lang-badge-sm styles to styles.css, or
  2. Reuse the existing .language-badge class that's already defined for .news-page
Suggested change
return `<span class="lang-badge-sm"><span aria-hidden="true">${f}</span> ${l.toUpperCase()}</span>`;
return `<span class="language-badge"><span aria-hidden="true">${f}</span> ${l.toUpperCase()}</span>`;

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

Choose a reason for hiding this comment

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

Added .lang-badge-sm and .available-languages CSS styles to styles.css in commit a2f376d. The language badges will now display with proper styling (green theme, borders, appropriate sizing) consistent with the project's design system.

@pethers
Copy link
Member

pethers commented Feb 14, 2026

@copilot apply changes based on the comments in this thread

@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 9 comments.

Comment on lines 97 to 101
// Clean MCP cache (2-hour TTL)
Object.keys(this.state.mcpQueryCache).forEach(key => {
const entry = this.state.mcpQueryCache[key];
const entryTime = new Date(entry.timestamp).getTime();
if (now - entryTime > MCP_CACHE_TTL_SECONDS * 1000) {
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

cleanupExpiredEntries() expires MCP cache entries using the constant MCP_CACHE_TTL_SECONDS rather than each entry’s stored ttl. Since cacheMCPQuery() allows custom TTLs, entries with TTL > 2h can be deleted too early. Use entry.ttl (with a sane default) when deciding expiration here.

Suggested change
// Clean MCP cache (2-hour TTL)
Object.keys(this.state.mcpQueryCache).forEach(key => {
const entry = this.state.mcpQueryCache[key];
const entryTime = new Date(entry.timestamp).getTime();
if (now - entryTime > MCP_CACHE_TTL_SECONDS * 1000) {
// Clean MCP cache using per-entry TTL (default: 2 hours)
Object.keys(this.state.mcpQueryCache).forEach(key => {
const entry = this.state.mcpQueryCache[key];
const entryTime = new Date(entry.timestamp).getTime();
const effectiveTtlSeconds =
typeof entry.ttl === 'number' && entry.ttl > 0
? entry.ttl
: MCP_CACHE_TTL_SECONDS;
if (now - entryTime > effectiveTtlSeconds * 1000) {

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Now uses per-entry TTL with fallback to default constant:

const effectiveTtlSeconds =
  typeof entry.ttl === 'number' && entry.ttl > 0
    ? entry.ttl
    : MCP_CACHE_TTL_SECONDS;

Comment on lines 175 to 197
/**
* Check if article is duplicate based on similarity
*
* @param {string} title - Article title
* @param {string[]} topics - Article topics
* @param {string[]} sources - Article sources
* @returns {Object} { isDuplicate: boolean, matchedArticle: Object|null, similarityScore: number }
*/
async checkDuplicateArticle(title, topics = [], sources = []) {
this.cleanupExpiredEntries();

let maxSimilarity = 0;
let matchedArticle = null;

for (const recentArticle of this.state.recentArticles) {
const similarity = this.calculateSimilarity(
title,
topics,
sources,
recentArticle.title,
recentArticle.topics,
recentArticle.mcpQueries
);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

checkDuplicateArticle() and calculateSimilarity() refer to the third parameter as sources, but the implementation compares it against recentArticle.mcpQueries. This API/Doc mismatch makes it easy to pass the wrong data and get incorrect duplicate detection. Align naming and docs (e.g., rename to mcpQueries everywhere, or add a separate sources field in recentArticles).

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Renamed all instances from sources to mcpQueries for consistency:

  • checkDuplicateArticle(title, topics, mcpQueries)
  • calculateSimilarity(..., mcpQueries1, ..., mcpQueries2)
  • Updated JSDoc comments to match implementation

No embedded CSS needed - promotes consistency and maintainability. -->
<article class="news-article">
<header class="article-header">
<div class="site-tagline">Latest news and analysis from Sweden's Riksdag. The Economist-style political journalism covering parliament, government, and agencies with systematic transparency.</div>
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

In non-English article pages, the new tagline is still an English sentence but isn’t marked as such. For accessibility (screen readers) and correct language metadata, consider adding lang="en" to the tagline element or providing per-language translations.

Suggested change
<div class="site-tagline">Latest news and analysis from Sweden's Riksdag. The Economist-style political journalism covering parliament, government, and agencies with systematic transparency.</div>
<div class="site-tagline"${lang !== 'en' ? ' lang="en"' : ''}>Latest news and analysis from Sweden's Riksdag. The Economist-style political journalism covering parliament, government, and agencies with systematic transparency.</div>

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Added conditional lang="en" attribute for accessibility:

<div class="site-tagline"${lang !== 'en' ? ' lang="en"' : ''}>

const availableText = AVAILABLE_IN_TRANSLATIONS[currentLang] || 'Available in';
const badges = languages.map(lang => generateLanguageBadge(lang, isRTL)).join(' ');

return `<p class="available-languages"${isRTL ? ' dir="ltr"' : ''}><strong>${availableText}:</strong> ${badges}</p>`;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

generateAvailableLanguages() forces dir="ltr" on the entire <p> when the current UI language is RTL. That will make the translated “Available in” label render left-to-right (incorrect for Arabic/Hebrew). Prefer keeping the paragraph direction RTL and only force LTR on the individual badges/codes.

Suggested change
return `<p class="available-languages"${isRTL ? ' dir="ltr"' : ''}><strong>${availableText}:</strong> ${badges}</p>`;
return `<p class="available-languages"><strong>${availableText}:</strong> ${badges}</p>`;

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Removed paragraph-level dir="ltr" to preserve RTL direction for Arabic/Hebrew labels. Individual badges still have dir="ltr" via generateLanguageBadge().

Comment on lines +3 to +20
/**
* Workflow State Coordinator
*
* Manages coordination between multiple news generation workflows to prevent
* duplicate work and improve efficiency:
* - news-realtime-monitor.md (2x daily)
* - news-evening-analysis.md (daily)
* - news-article-generator.md (various schedules)
*
* Features:
* - MCP query caching (2-hour TTL)
* - Similarity-based article deduplication (>70% threshold)
* - Recent article tracking (last 6 hours)
* - Workflow coordination metadata
*
* Usage:
* import { WorkflowStateCoordinator } from './workflow-state-coordinator.js';
* const coordinator = new WorkflowStateCoordinator();
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The PR title/description focus on moving article CSS and adding/repairing language badge styles, but this change set also introduces new workflow coordination and article quality-scoring modules plus new test suites and regenerates multiple news indexes. Please align the PR title/description with the actual scope, or split the unrelated features into separate PRs to reduce review/rollback risk.

Copilot uses AI. Check for mistakes.
Comment on lines 64 to 68
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}

await coordinator.save();
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

This test doesn’t actually verify directory creation when missing: it creates the directory if it doesn’t exist, so coordinator.save() never has to create it. To test the behavior, ensure the directory is removed before calling save(), then assert save() recreates it.

Suggested change
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
await coordinator.save();
// Ensure any existing test file is removed
if (fs.existsSync(TEST_STATE_FILE)) {
fs.unlinkSync(TEST_STATE_FILE);
}
// Remove the directory to simulate a missing metadata directory
if (fs.existsSync(dir)) {
fs.rmdirSync(dir, { recursive: true });
}
expect(fs.existsSync(dir)).toBe(false);
await coordinator.save();
// save() should recreate the directory and state file
expect(fs.existsSync(dir)).toBe(true);

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Test now removes directory first with fs.rmSync(dir, { recursive: true }), then verifies save() recreates it.

Comment on lines 22 to 27
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

path, fileURLToPath, __filename, and __dirname are declared but not used in this module. Removing unused imports/variables will avoid lint noise and keeps the script focused.

Suggested change
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Removed unused imports (path, fileURLToPath, __filename, __dirname), kept only fs.

Comment on lines 717 to 732
grid.innerHTML = articlesToRender.map(article => {
// Generate language badge for the article
const flag = {en:'🇬🇧',sv:'🇸🇪',da:'🇩🇰',no:'🇳🇴',fi:'🇫🇮',de:'🇩🇪',fr:'🇫🇷',es:'🇪🇸',nl:'🇳🇱',ar:'🇸🇦',he:'🇮🇱',ja:'🇯🇵',ko:'🇰🇷',zh:'🇨🇳'}[article.lang] || '🌐';
const langBadge = \`<span class="language-badge" aria-label="\${article.lang} language"><span aria-hidden="true">\${flag}</span> \${article.lang.toUpperCase()}</span>\`;

// Generate available languages display if multiple languages exist
const availableLangs = article.availableLanguages || [article.lang];
let availableDisplay = '';
if (availableLangs.length > 1) {
const availableText = '${AVAILABLE_IN_TRANSLATIONS[langKey] || 'Available in'}';
const availableBadges = availableLangs.map(l => {
const f = {en:'🇬🇧',sv:'🇸🇪',da:'🇩🇰',no:'🇳🇴',fi:'🇫🇮',de:'🇩🇪',fr:'🇫🇷',es:'🇪🇸',nl:'🇳🇱',ar:'🇸🇦',he:'🇮🇱',ja:'🇯🇵',ko:'🇰🇷',zh:'🇨🇳'}[l] || '🌐';
return \`<span class="lang-badge-sm"><span aria-hidden="true">\${f}</span> \${l.toUpperCase()}</span>\`;
}).join(' ');
availableDisplay = \`<p class="available-languages"><strong>\${availableText}:</strong> \${availableBadges}</p>\`;
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The generated client-side rendering duplicates the language flag map and “available languages” HTML building logic instead of using the newly added LANGUAGE_FLAGS / generateLanguageBadge() / generateAvailableLanguages() helpers. This increases maintenance cost and risks the HTML/CSS drifting (e.g., lang-badge-sm vs language-badge, missing RTL handling, different aria-labels). Consider generating these badges via the shared helpers or emitting the needed data and reusing the same functions in one place.

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Eliminated duplication by emitting LANGUAGE_FLAGS and AVAILABLE_IN_TEXT to client-side:

const LANGUAGE_FLAGS = ${JSON.stringify(LANGUAGE_FLAGS)};
const AVAILABLE_IN_TEXT = '${escapeHtml(AVAILABLE_IN_TRANSLATIONS[langKey])}';

Client code now uses these shared constants instead of inline flag maps.

Comment on lines 941 to 949
// Get ALL articles with language metadata for cross-language discovery
const allArticlesWithLanguageInfo = getAllArticlesWithLanguageInfo(articlesByLang);

const html = generateIndexHTML(langKey, allArticlesWithLanguageInfo, articlesByLang);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

getAllArticlesWithLanguageInfo(articlesByLang) is recomputed inside the per-language loop, even though articlesByLang doesn’t change. Compute it once before the loop and reuse it for each generateIndexHTML() call to avoid repeated work on every run.

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

Choose a reason for hiding this comment

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

Fixed in commit 849b756. Moved getAllArticlesWithLanguageInfo() outside the loop for 14× performance improvement:

const allArticlesWithLanguageInfo = getAllArticlesWithLanguageInfo(articlesByLang);
Object.keys(LANGUAGES).forEach(langKey => { ... });

@pethers
Copy link
Member

pethers commented Feb 14, 2026

@copilot apply changes based on the comments in this thread

…cing, cross-language discovery, warnings separation

1. Fixed hashObject() null/undefined handling (#2807360915) - prevents crashes
2. Fixed invalid timestamp handling in cleanup (#2807360928) - treats NaN as expired
3. Converted TTL test to use fake timers (#2807360926) - deterministic, fast
4. Implemented cross-language discovery (#2807348132, #2807360906) - builds slugToLanguages map
5. Namespaced CSS under .news-page (#2807348134) - prevents global scope pollution
6. Removed outdated documentation note (#2807360913) - no longer shows English fallback
7. Separated warnings from blocking issues (#2807360921) - recommendations don't fail quality gate
8. Regenerated all 14 news indexes with enriched availableLanguages field

Co-authored-by: pethers <[email protected]>
const f = lang.filters;
const filename = langKey === 'en' ? 'index.html' : `index_${langKey === 'no' ? 'no' : langKey}.html`;
const mainIndex = langKey === 'en' ? 'index.html' : `index_${langKey === 'no' ? 'no' : langKey}.html`;
const isRTL = ['ar', 'he'].includes(langKey);
Copilot AI changed the title Externalize news article CSS, add quality framework, and implement workflow coordination Externalize news article CSS, add quality framework, implement workflow coordination, and enable cross-language article discovery Feb 14, 2026
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

…agline and language badge additions

Resolved conflict by:
1. Taking main branch version as base (more recent, properly reviewed)
2. Adding back unique styles from our branch:
   - Site tagline styles (.news-article .site-tagline)
   - Language badge styles (.news-page .lang-badge-sm)
   - Available languages section (.news-page .available-languages)
3. Enhanced .back-to-news with :focus and :focus-visible states for WCAG 2.1 AA compliance

The merged version includes:
- All main branch improvements to news article styles
- Our PR's editorial tagline feature
- Our PR's cross-language discovery UI components
- Proper keyboard accessibility with focus indicators

Co-authored-by: pethers <[email protected]>
Copilot AI changed the title Externalize news article CSS, add quality framework, implement workflow coordination, and enable cross-language article discovery Resolve styles.css merge conflict: preserve main branch base with PR feature additions Feb 14, 2026
Copilot AI changed the title Resolve styles.css merge conflict: preserve main branch base with PR feature additions Resolve merge conflict: preserve accessibility and cross-language features Feb 14, 2026
@pethers pethers requested a review from Copilot February 14, 2026 13:53
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 7 comments.

<link rel="alternate" hreflang="sv" href="https://riksdagsmonitor.com/news/index_sv.html">
<link rel="alternate" hreflang="da" href="https://riksdagsmonitor.com/news/index_da.html">
<link rel="alternate" hreflang="no" href="https://riksdagsmonitor.com/news/index_no.html">
<link rel="alternate" hreflang="nb" href="https://riksdagsmonitor.com/news/index_no.html">
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Inconsistent Norwegian language code. The PR changes hreflang from "no" to "nb" (line 31), but according to stored repository memories, the codebase standardized on 'no' (not 'nb') across all contexts including hreflang attributes. This contradicts the documented standardization convention. Either revert to hreflang="no" or update the documentation to explain why hreflang specifically uses 'nb' while other contexts use 'no'.

Suggested change
<link rel="alternate" hreflang="nb" href="https://riksdagsmonitor.com/news/index_no.html">
<link rel="alternate" hreflang="no" href="https://riksdagsmonitor.com/news/index_no.html">

Copilot uses AI. Check for mistakes.
},
"articleSection": "${typeLabel}",
"articleBody": "${sanitizeArticleBody(escapeHtml(content))}...",
"articleBody": "${escapeHtml(content).substring(0, 500)}...",
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Potential JSON-LD syntax error. The removal of sanitizeArticleBody() (which normalized newlines and whitespace) and replacement with escapeHtml(content).substring(0, 500) may cause invalid JSON in the structured data script tag. Standard HTML entity escaping (escapeHtml) does NOT escape newlines (\n), which can break JSON string syntax when content contains line breaks. The Schema.org articleBody field is within a JSON context and needs newline handling. Consider restoring newline normalization: escapeHtml(content).replace(/\n/g, ' ').replace(/\s+/g, ' ').substring(0, 500) or use JSON.stringify for the entire structured data object instead of template string interpolation.

Suggested change
"articleBody": "${escapeHtml(content).substring(0, 500)}...",
"articleBody": "${escapeHtml(content).replace(/\n/g, ' ').replace(/\s+/g, ' ').substring(0, 500)}...",

Copilot uses AI. Check for mistakes.
Object.entries(PARTY_VARIANTS).forEach(([code, variants]) => {
// Check if any variant of this party is mentioned
for (const variant of variants) {
const regex = new RegExp(`\\b${variant}\\b`, 'gi');
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Regex injection vulnerability in party name detection. Constructing regex patterns directly from the variant strings without escaping special characters (line 125: new RegExp(\\b${variant}\b`, 'gi')) can cause regex syntax errors or unexpected behavior if party names contain regex special characters. For example, if a variant contains ".", "+", "*", "?", "[", "]", etc., the regex will fail or match incorrectly. Escape the variant string before using it in the regex pattern using variant.replace(/[.*+?^${}()|[]\]/g, '\$&')`.

Copilot uses AI. Check for mistakes.

- ✅ Smart caching (skip if < 5 hours old)
- ✅ Agentic workflow coordination (skip if agentic activity < 2 hours)
- ✅ Smart caching (skip if < 11 hours old)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Documentation mismatch with workflow implementation. WORKFLOWS.md states "skip if < 11 hours old" (line 505 and 550) but the actual workflow checks for 5 hours (line 95: if [ $HOURS_AGO -gt 5 ]). This creates confusion for maintainers. Update either the documentation to match the 5-hour threshold or update the workflow code to match the documented 11-hour threshold.

Suggested change
- ✅ Smart caching (skip if < 11 hours old)
- ✅ Smart caching (skip if < 5 hours old)

Copilot uses AI. Check for mistakes.
Comment on lines 537 to 548
<time class="article-date" datetime="${article.date}">${formatDate(article.date)}</time>
<span class="article-type">${localizeType(article.type)}</span>
${langBadge}
</div>
<h2 class="article-title">
<a href="${article.slug}">${article.title}</a>
</h2>
<p class="article-excerpt">${article.excerpt}</p>
${availableDisplay}
<div class="article-tags">
${article.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Potential XSS vulnerability in client-side rendering. Article data (title, excerpt, tags, slug, date) is interpolated directly into HTML template strings without escaping (lines 537-548). If article metadata contains HTML/JavaScript (e.g., <script>alert('xss')</script> in a title), it will execute when rendered. While the data comes from server-generated JSON, defense-in-depth requires client-side escaping. Either:

  1. HTML-escape all dynamic values using a helper function before interpolation
  2. Use safer DOM APIs (createElement + textContent) instead of innerHTML
  3. Verify that server-side generation (generate-news-indexes.js) properly escapes all article metadata in the JSON

The same issue exists in all 14 language index files.

Copilot uses AI. Check for mistakes.
Comment on lines 413 to 761
/**
* Generate article-specific CSS
*/
function generateArticleCSS() {
return ` <style>
${getArticleStyles()}
</style>`;
}

/**
* Get article CSS styles (matching existing articles)
*/
function getArticleStyles() {
return `body {
font-family: var(--font-main, 'Inter', sans-serif);
background: var(--bg-color, #f8f9fa);
color: var(--text-color, #1a1a1a);
line-height: 1.7;
margin: 0;
padding: 0;
}

.news-article {
max-width: 800px;
margin: 2rem auto;
padding: 0 1.5rem;
}

.article-header {
border-bottom: 3px solid var(--primary-color, #006633);
padding-bottom: 1.5rem;
margin-bottom: 2rem;
}

.article-header h1 {
font-size: 2.25rem;
line-height: 1.2;
margin: 0 0 0.75rem 0;
color: var(--header-color, #006633);
font-weight: 700;
}

.article-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.9rem;
color: var(--text-secondary, #4a4a4a);
align-items: center;
}

.article-meta time {
font-weight: 600;
}

.article-meta .separator {
color: var(--border-color, #ccc);
}

/* Event Calendar */
.event-calendar {
background: linear-gradient(135deg, var(--primary-color, #006633) 0%, var(--primary-light, #007744) 100%);
color: white;
border-radius: var(--border-radius-lg, 12px);
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 4px 6px var(--card-shadow, rgba(0, 102, 51, 0.08));
}

.event-calendar h2 {
font-size: 1.5rem;
margin: 0 0 1.5rem 0;
font-weight: 700;
text-align: center;
}

.calendar-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}

.calendar-day {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: var(--border-radius, 8px);
padding: 1.25rem;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.2s ease, background 0.2s ease;
}

.calendar-day:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
}

.calendar-day.today {
background: rgba(255, 255, 255, 0.3);
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}

.day-header {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.9;
margin-bottom: 0.25rem;
font-weight: 600;
}

.day-date {
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 0.75rem;
display: block;
}

.event-list {
list-style: none;
padding: 0;
margin: 0;
}

.event-item {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}

.event-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}

.event-time {
display: block;
font-size: 0.75rem;
opacity: 0.8;
margin-bottom: 0.25rem;
font-weight: 600;
}

.event-title {
display: block;
font-size: 0.9rem;
line-height: 1.4;
}

/* Article Content */
.article-content {
font-size: 1.05rem;
line-height: 1.8;
}

.lede {
font-size: 1.15rem;
line-height: 1.7;
font-weight: 500;
color: var(--text-color, #1a1a1a);
margin-bottom: 2rem;
padding-left: 1rem;
border-left: 4px solid var(--primary-color, #006633);
}

.article-content h2 {
font-size: 1.75rem;
color: var(--header-color, #006633);
margin: 2.5rem 0 1rem 0;
font-weight: 700;
}

.article-content h3 {
font-size: 1.35rem;
color: var(--primary-color, #006633);
margin: 2rem 0 0.75rem 0;
font-weight: 600;
}

.article-content p {
margin-bottom: 1.25rem;
}

.article-content ul,
.article-content ol {
margin: 1.25rem 0;
padding-left: 2rem;
}

.article-content li {
margin-bottom: 0.75rem;
}

.article-content a {
color: var(--link-color, #007744);
text-decoration: underline;
text-decoration-color: rgba(0, 119, 68, 0.3);
text-underline-offset: 2px;
transition: all 0.2s ease;
}

.article-content a:hover {
color: var(--link-hover, #006633);
text-decoration-color: var(--link-hover, #006633);
}

.context-box {
background: var(--badge-bg, #f8f9fa);
border-left: 4px solid var(--primary-color, #006633);
padding: 1.5rem;
margin: 2rem 0;
border-radius: var(--border-radius-sm, 4px);
}

.context-box h3 {
margin-top: 0;
font-size: 1.2rem;
color: var(--primary-color, #006633);
}

/* Watch Section */
.watch-section {
background: linear-gradient(135deg, var(--primary-color, #006633) 0%, var(--primary-light, #007744) 100%);
color: white;
border-radius: var(--border-radius-lg, 12px);
padding: 2rem;
margin: 2rem 0;
}

.watch-section h2 {
font-size: 1.5rem;
margin: 0 0 1.5rem 0;
font-weight: 700;
text-align: center;
}

.watch-list {
list-style: none;
padding: 0;
margin: 0;
}

.watch-list li {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
line-height: 1.6;
}

.watch-list li:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}

.watch-list strong {
display: block;
margin-bottom: 0.5rem;
font-size: 1.05rem;
}

/* Article Footer */
.article-footer {
border-top: 2px solid var(--section-border, #e9ecef);
margin-top: 3rem;
padding-top: 2rem;
}

.article-sources {
background: var(--badge-bg, #f8f9fa);
padding: 1.5rem;
border-radius: var(--border-radius, 8px);
margin-bottom: 1.5rem;
}

.article-sources h3 {
margin-top: 0;
color: var(--primary-color, #006633);
font-size: 1.2rem;
}

.article-sources p {
margin: 0.75rem 0;
font-size: 0.95rem;
color: var(--text-secondary, #4a4a4a);
}

.article-nav {
text-align: center;
}

.back-to-news {
display: inline-block;
color: var(--link-color, #007744);
text-decoration: none;
font-weight: 600;
padding: 0.75rem 1.5rem;
border: 2px solid var(--link-color, #007744);
border-radius: var(--border-radius, 8px);
transition: all 0.3s ease;
}

.back-to-news:hover {
background: var(--link-color, #007744);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 119, 68, 0.2);
}

/* Responsive Design */
@media (max-width: 768px) {
.news-article {
padding: 0 1rem;
}

.article-header h1 {
font-size: 1.75rem;
}

.article-content {
font-size: 1rem;
}

.calendar-grid {
grid-template-columns: 1fr;
}
}

/* Print Styles */
@media print {
body {
background: white;
}

.event-calendar,
.watch-section {
background: white;
color: black;
border: 2px solid #006633;
}

.back-to-news {
display: none;
}
}`;
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Dead code: unused CSS generation functions. The functions generateArticleCSS() (line 416) and getArticleStyles() (line 425) are defined but never called or exported. The comment on line 322 confirms styles are now in styles.css, making these functions obsolete. Remove both functions to reduce code maintenance burden.

Copilot uses AI. Check for mistakes.
* articles in its target language).
*
* If cross-language discovery is needed again, this function can be used instead of
* passing articlesByLang[langKey] to generateIndexHTML() on line 958.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Outdated reference to line 958 in JSDoc comment. The actual line where generateIndexHTML() is called is now 1023. Update the comment to reference the correct line number.

Suggested change
* passing articlesByLang[langKey] to generateIndexHTML() on line 958.
* passing articlesByLang[langKey] to generateIndexHTML() on line 1023.

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Member

pethers commented Feb 14, 2026

@copilot apply changes based on the comments in this thread

…ization, Norwegian hreflang, dead code, outdated refs

1. Fixed regex injection (#2807520445) - escape special chars in party detection
2. Added XSS protection (#2807520455) - escapeHtml helper in all 14 indexes
3. Restored JSON-LD sanitization (#2807520439) - newline/whitespace normalization
4. Fixed Norwegian hreflang (#2807520430) - reverted nb→no in all indexes
5. Removed dead code (#2807520461) - deleted unused CSS generation functions
6. Updated line reference (#2807520467) - corrected JSDoc to line 1023
7. Fixed documentation (#2807520449) - updated to match 5-hour threshold

Co-authored-by: pethers <[email protected]>
@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@github-actions
Copy link
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers marked this pull request as ready for review February 14, 2026 14:24
@pethers pethers merged commit 2c0d36f into main Feb 14, 2026
14 checks passed
@pethers pethers deleted the copilot/improve-multi-language-sync branch February 14, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants