Add tests for Risk Assessment Dashboard and improve SEO#163
Conversation
…oard - Implement tests for risk scoring, heat map configuration, and Chart.js visualizations. - Validate CIA CSV data loading and mock data generation. - Ensure proper risk classification logic and color mapping. - Test DOM structure, filter controls, and early warning system functionality. - Verify data attribution and last updated timestamp handling.
…ashboard and news pages
| 'stat-total-votes': 'view_riksdagen_vote_data_ballot_politician_summary', | ||
| 'stat-total-documents': 'document_data', | ||
| 'stat-rule-violations': 'rule_violation', | ||
| 'stat-government-proposals': 'view_riksdagen_goverment_proposals', |
| fetchCIAData(CIA_DATA_URLS.crisisResilience) | ||
| ]); | ||
|
|
||
| const [riskLevels, riskByParty, crisisResilience] = datasets; |
| fetchCIAData(CIA_DATA_URLS.crisisResilience) | ||
| ]); | ||
|
|
||
| const [riskLevels, riskByParty, crisisResilience] = datasets; |
… URL - Changed Open Graph and Twitter image URLs from "https://cia.sourceforge.io/cia-logo.png" to "https://hack23.com/cia-icon-140.webp" in: - news/index_he.html - news/index_ja.html - news/index_ko.html - news/index_nl.html - news/index_no.html - news/index_sv.html - news/index_zh.html - politician-dashboard.html - scripts/article-template.js - scripts/generate-news-indexes.js - sitemap.html - sitemap_ar.html - sitemap_da.html - sitemap_de.html - sitemap_es.html - sitemap_fi.html - sitemap_fr.html - sitemap_he.html - sitemap_ja.html - sitemap_ko.html - sitemap_nl.html - sitemap_no.html - sitemap_sv.html - sitemap_zh.html
…index_zh.html for better clarity and accuracy
…e for event attendance mode
# Conflicts: # tests/back-to-top.test.js # tests/stats-loader.test.js
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Adds a new Risk Assessment & Anomaly Detection dashboard (D3 heatmap + Chart.js charts) and updates stats loading/SEO metadata across dashboards, sitemaps, and news pages, alongside expanded Vitest coverage for UI helpers.
Changes:
- Introduces
js/risk-dashboard.jsandtests/risk-dashboard.test.jsfor risk-scoring/dashboard behaviors. - Refactors
js/stats-loader.jsto usecia-data/extraction_summary_report.csvas the single source of truth (and updates related CIA stats tooling/data files). - Updates OpenGraph/Twitter metadata and icons across sitemaps, dashboards, news index pages, and several news articles.
Reviewed changes
Copilot reviewed 79 out of 85 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/risk-dashboard.test.js | Adds a large Vitest suite for risk dashboard logic/DOM structure and CSV fetch handling. |
| tests/back-to-top.test.js | Expands tests around back-to-top button behavior and accessibility preferences. |
| styles.css | Adds dashboard loader/error styles plus filter-group and chart container helpers. |
| sitemap.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_ar.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_da.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_de.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_es.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_fi.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_fr.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_he.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_ja.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_ko.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_nl.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_no.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_sv.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| sitemap_zh.html | Updates OG/Twitter image and favicon URL for SEO/social sharing. |
| scripts/load-cia-stats.js | Extends extracted stats (tables + views) and bumps version to 1.2.0. |
| scripts/generate-news-indexes.js | Updates social image URLs in generated news index HTML/JSON-LD. |
| scripts/article-template.js | Updates social image URLs in generated article templates/JSON-LD. |
| politician-dashboard.html | Updates OG image URL. |
| news/index.html | Updates OG/Twitter image and adjusts hreflang for Norwegian. |
| news/index_ar.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_da.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_de.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_es.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_fi.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_fr.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_he.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_ja.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_ko.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_nl.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_no.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_sv.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/index_zh.html | Updates OG/Twitter image and JSON-LD org logo URL. |
| news/2026-02-week-ahead-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-week-ahead-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-parliament-agenda-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-parliament-agenda-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-opposition-motions-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-opposition-motions-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-government-propositions-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-government-propositions-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-committee-reports-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-committee-reports-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-10-week-ahead-feb-10-17-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-10-week-ahead-feb-10-17-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-10-pm-eu-summit-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-10-pm-eu-summit-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-10-biodiversity-citizenship-en.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| news/2026-02-10-biodiversity-citizenship-sv.html | Updates OG/Twitter/JSON-LD image/logo URLs. |
| js/stats-loader.js | Switches stats loading to extraction_summary_report.csv and updates DOM update logic. |
| js/risk-dashboard.js | Adds new D3/Chart.js-driven risk assessment dashboard implementation. |
| js/back-to-top.js | Adds an accessible back-to-top IIFE (scroll threshold + reduced-motion handling). |
| dashboard/index.html | SEO + navigation + JSON-LD enhancements, local Chart.js, footer rebuild, hreflang change. |
| dashboard/index_ar.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_da.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_de.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_es.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_fi.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_fr.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_he.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_ja.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_ko.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_nl.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_no.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_sv.html | Updates OG/Twitter image and favicon URL. |
| dashboard/index_zh.html | Updates OG/Twitter image and favicon URL. |
| cia-data/production-stats.json | Updates generated metadata and expands counts to include views/tables. |
| cia-data/extraction_summary_report.csv | Adds the extraction summary CSV used as stats source of truth. |
| cia-data/download-csv.sh | Downloads extraction summary CSV as part of CIA data sync. |
| <!-- Skip to Main Content Link --> | ||
| <a href="#main-dashboard" class="skip-link">Skip to dashboard content</a> | ||
| <!-- Skip to Content Link for Accessibility --> | ||
| <a href="#main-dashboard" class="skip-to-content">Skip to dashboard content</a> |
There was a problem hiding this comment.
The skip link class was changed to skip-to-content, but the primary styling in styles.css is defined for .skip-link. Without matching styles, the skip link may be permanently visible or not positioned correctly. Either keep the .skip-link class or add equivalent base styles for .skip-to-content.
| <a href="#main-dashboard" class="skip-to-content">Skip to dashboard content</a> | |
| <a href="#main-dashboard" class="skip-link">Skip to dashboard content</a> |
| .on('keydown', function(event, d) { | ||
| if (event.key === 'Enter' || event.key === ' ') { | ||
| event.preventDefault(); | ||
| showRiskDetails(d, this); | ||
| } |
There was a problem hiding this comment.
showRiskDetails is referenced in the heatmap cell keydown handler but is not defined anywhere in this script, so pressing Enter/Space on a focused cell will throw a ReferenceError. Define showRiskDetails or reuse the existing click handler logic for keyboard activation.
| // Map risk level to scores | ||
| let baseScore; | ||
| if (level === 'HIGH') baseScore = 7 + Math.random() * 2; | ||
| else if (level === 'MEDIUM') baseScore = 4 + Math.random() * 3; | ||
| else baseScore = 1 + Math.random() * 3; |
There was a problem hiding this comment.
The CIA risk-level transformation assigns baseScore for HIGH/MEDIUM/else, but any risk_level other than HIGH/MEDIUM (e.g. CRITICAL) will fall into the else branch and get a low score (1–4) while still having level=CRITICAL. This makes level inconsistent with score (colors/classification) and can trigger incorrect early warnings.
| if (criticalMPs.length > 0) { | ||
| const uniqueMPs = [...new Set(criticalMPs.map(d => d.politician))]; | ||
| warningBanner.className = 'alert-banner critical'; | ||
|
|
||
| // Build banner content safely using DOM methods | ||
| const strong = document.createElement('strong'); | ||
| strong.textContent = '⚠️ CRITICAL:'; | ||
| warningBanner.appendChild(strong); | ||
| warningBanner.appendChild(document.createTextNode(` ${uniqueMPs.length} MPs with risk level ≥8.0 detected `)); | ||
|
|
||
| const detailsSpan = document.createElement('span'); | ||
| detailsSpan.className = 'alert-details'; | ||
| detailsSpan.textContent = 'Immediate review recommended'; | ||
| warningBanner.appendChild(detailsSpan); | ||
|
|
There was a problem hiding this comment.
The critical-alert branch appends nodes to #earlyWarnings without clearing existing content first. If updateEarlyWarnings runs more than once, the banner will accumulate duplicate text/labels; also it won’t remove any initial placeholder markup. Clear the container (e.g., textContent = '') before appending.
| .text(d => String(d || '').replace('Rule_', 'R')); | ||
|
|
||
| // Add Y axis labels (politicians) - Sample every 10th | ||
| g.append('g') | ||
| .selectAll('text') | ||
| .data(politicians.filter((_, i) => i % 10 === 0)) | ||
| .enter() | ||
| .append('text') | ||
| .attr('x', -10) | ||
| .attr('y', d => yScale(d) + yScale.bandwidth() / 2) | ||
| .attr('text-anchor', 'end') | ||
| .attr('alignment-baseline', 'middle') | ||
| .attr('font-size', '10px') | ||
| .attr('fill', 'currentColor') | ||
| .text(d => d); | ||
|
|
||
| // Create legend | ||
| createLegend(); | ||
|
|
||
| // Filter functionality | ||
| document.getElementById('filterHighRisk').addEventListener('change', function(e) { | ||
| if (e.target.checked) { | ||
| cells.style('opacity', d => d.score >= 6.0 ? 1 : 0.1); | ||
| } else { | ||
| cells.style('opacity', 1); | ||
| } | ||
| }); | ||
|
|
||
| // Rule filter | ||
| const ruleFilter = document.getElementById('riskRuleFilter'); | ||
| rules.forEach(rule => { | ||
| const option = document.createElement('option'); | ||
| option.value = rule; | ||
| option.textContent = String(rule || '').replace('Rule_', 'Risk Rule '); | ||
| ruleFilter.appendChild(option); |
There was a problem hiding this comment.
Heatmap rule label generation uses String(d || ''), which turns numeric rule values like 0 into an empty string. In your CIA-transformed data rule is numeric (ruleIdx), so rule 0 will render as a blank label and also collide with the "All Rules" option when building the select. Avoid || '' here and handle numeric rules explicitly.
| <link rel="alternate" hreflang="en" href="https://riksdagsmonitor.com/news/index.html"> | ||
| <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="nb" href="https://riksdagsmonitor.com/news/index_no.html"> | ||
| <link rel="alternate" hreflang="no" href="https://riksdagsmonitor.com/news/index_no.html"> |
There was a problem hiding this comment.
hreflang was changed to no here, but the other news index pages (news/index_sv.html, etc.) still declare the Norwegian variant as hreflang="nb". Keep this consistent across the whole news index set (prefer nb for Bokmål) to avoid SEO inconsistencies.
| <!-- Additional Meta Tags --> | ||
| <meta name="theme-color" content="#0a0e27"> | ||
| <meta name="color-scheme" content="dark light"> | ||
| <link rel="icon" type="image/png" href="https://cia.sourceforge.io/cia-logo.png"> | ||
| <link rel="icon" type="image/png" href="https://hack23.com/cia-icon-140.webp"> |
There was a problem hiding this comment.
The favicon link now points to a .webp file but still declares type="image/png". This MIME type mismatch can cause incorrect icon handling in some clients. Update the type to image/webp (or omit the type attribute).
| <meta name="theme-color" content="#0a0e27"> | ||
| <meta name="color-scheme" content="dark light"> | ||
| <link rel="icon" type="image/png" href="https://cia.sourceforge.io/cia-logo.png"> | ||
| <link rel="icon" type="image/png" href="https://hack23.com/cia-icon-140.webp"> |
There was a problem hiding this comment.
The favicon link now points to a .webp file but still declares type="image/png". This MIME type mismatch can cause incorrect icon handling in some clients. Update the type to image/webp (or omit the type attribute).
| <link rel="icon" type="image/png" href="https://hack23.com/cia-icon-140.webp"> | |
| <link rel="icon" type="image/webp" href="https://hack23.com/cia-icon-140.webp"> |
| .on('mouseover', function(event, d) { | ||
| tooltip.style('visibility', 'visible') | ||
| .html(` | ||
| <strong>${d.politician}</strong> (${d.party})<br> | ||
| <strong>${d.ruleName}</strong><br> | ||
| Risk Score: <strong>${d.score.toFixed(2)}</strong><br> | ||
| Level: <strong>${d.level}</strong> | ||
| `); | ||
| d3.select(this).attr('stroke', '#000').attr('stroke-width', 2); |
There was a problem hiding this comment.
Tooltip content is built with tooltip.html(...) using values sourced from remote CSV data (politician, party, ruleName). This is an XSS risk if the CSV content is ever malformed/compromised. Prefer building the tooltip with DOM nodes / textContent (or sanitize before inserting HTML).
| for (const row of rows) { | ||
| if (row.status === 'success' && row.object_name && row.row_count) { | ||
| lookup[row.object_name] = parseInt(row.row_count, 10); | ||
| } |
There was a problem hiding this comment.
In the CSV lookup build, the condition row.row_count will be false for legitimate zero counts ('0'), so those objects will never be added to lookup and stats won’t update to 0 when appropriate. Check for row.row_count !== undefined/!== '' instead of truthiness.
Enhance the Risk Assessment & Anomaly Detection Dashboard with comprehensive tests for various functionalities, including risk scoring and data loading. Update meta tags and links for better SEO and navigation on dashboard and news pages. Refactor code for improved readability and maintainability.