Add interactive CIA intelligence dashboard with election forecasting#23
Add interactive CIA intelligence dashboard with election forecasting#23
Conversation
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request adds a comprehensive interactive dashboard for visualizing CIA (Citizen Intelligence Agency) intelligence exports, including party performance, Swedish Election 2026 predictions, MP rankings, voting patterns, and committee network analysis. The implementation includes 6 JSON data files, 3 JavaScript modules (547 LOC total), 2 HTML pages (English and Swedish), dashboard-specific CSS (592 LOC), and homepage integration.
Changes:
- Interactive dashboard with Chart.js v4.4.1 visualizations for 349 MPs, 8 parties, and comprehensive parliamentary analytics
- Data loading module with local cache and API fallback strategy, error handling, and loading states
- Responsive mobile-first design (320px-1440px+) with WCAG 2.1 AA accessibility features, ARIA labels, and semantic HTML5
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| dashboard/index.html | English dashboard page with semantic HTML5, ARIA labels, Chart.js integration, breadcrumb navigation |
| dashboard/index_sv.html | Swedish dashboard page, consistent translation and structure with English version |
| dashboard/styles.css | Comprehensive dashboard styles with mobile-first responsive design, accessibility enhancements, print styles |
| dashboard/cia-data-loader.js | Data loading module with fallback strategy, proper error handling, modular exports |
| dashboard/cia-visualizations.js | Chart.js rendering for party performance, rankings, voting patterns, committee network |
| dashboard/election-predictions.js | Election 2026 predictions and coalition scenario rendering |
| dashboard/README.md | Comprehensive documentation covering features, architecture, security, dependencies |
| data/cia-exports/current/*.json | 6 JSON files with mock CIA intelligence data (overview, election, party, rankings, committees, voting) |
| index.html | Homepage integration with dashboard promo section link |
dashboard/election-predictions.js
Outdated
| <div style="font-size: 0.75rem; color: var(--text-secondary);">Current</div> | ||
| <strong>${party.currentSeats}</strong> | ||
| </div> | ||
| <div class="seats-arrow">→</div> | ||
| <div class="seats-predicted"> | ||
| <div style="font-size: 0.75rem; color: var(--text-secondary);">Predicted</div> |
There was a problem hiding this comment.
Inline styles are used in the dynamically generated HTML, violating CSP (Content Security Policy) guidelines. According to the repository's security architecture and custom coding guidelines, all styles should be externalized to CSS files.
The inline styles on lines 28 and 33 should be replaced with CSS classes. For example, create a class like .seats-label in styles.css with the font-size and color properties, then use that class instead of inline styles in the generated HTML.
| <div style="font-size: 0.75rem; color: var(--text-secondary);">Current</div> | |
| <strong>${party.currentSeats}</strong> | |
| </div> | |
| <div class="seats-arrow">→</div> | |
| <div class="seats-predicted"> | |
| <div style="font-size: 0.75rem; color: var(--text-secondary);">Predicted</div> | |
| <div class="seats-label">Current</div> | |
| <strong>${party.currentSeats}</strong> | |
| </div> | |
| <div class="seats-arrow">→</div> | |
| <div class="seats-predicted"> | |
| <div class="seats-label">Predicted</div> |
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Inline styles removed and replaced with CSS class .seats-label in dashboard/styles.css.
dashboard/cia-visualizations.js
Outdated
|
|
||
| container.innerHTML = top10.rankings.map(mp => ` | ||
| <div class="ranking-item"> | ||
| <div class="ranking-number">${mp.rank}</div> | ||
| <div class="ranking-info"> | ||
| <div class="ranking-name">${mp.firstName} ${mp.lastName}</div> | ||
| <div class="ranking-party">${mp.party}</div> | ||
| <div class="ranking-role">${mp.role}</div> | ||
| </div> | ||
| <div class="ranking-score"> | ||
| <div class="score-value">${mp.influenceScore.toFixed(1)}</div> | ||
| <div class="score-label">Influence</div> | ||
| </div> | ||
| </div> | ||
| `).join(''); |
There was a problem hiding this comment.
Potential XSS (Cross-Site Scripting) vulnerability: Data from external sources is directly injected into innerHTML without sanitization. If the JSON data from CIA exports contains malicious HTML/JavaScript, it could execute in the user's browser.
String values like mp.firstName, mp.lastName, mp.party, and mp.role are directly interpolated into HTML without escaping. While the current mock data appears safe, production data could potentially contain malicious content.
Recommendation: Either:
- Use textContent for plain text values (already done correctly in renderKeyMetrics for numeric values)
- Implement HTML sanitization using DOMPurify or similar library
- Use DOM methods (createElement, appendChild) instead of innerHTML for better security
| container.innerHTML = top10.rankings.map(mp => ` | |
| <div class="ranking-item"> | |
| <div class="ranking-number">${mp.rank}</div> | |
| <div class="ranking-info"> | |
| <div class="ranking-name">${mp.firstName} ${mp.lastName}</div> | |
| <div class="ranking-party">${mp.party}</div> | |
| <div class="ranking-role">${mp.role}</div> | |
| </div> | |
| <div class="ranking-score"> | |
| <div class="score-value">${mp.influenceScore.toFixed(1)}</div> | |
| <div class="score-label">Influence</div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| if (!container) { | |
| return; | |
| } | |
| // Clear existing content safely | |
| container.textContent = ''; | |
| const fragment = document.createDocumentFragment(); | |
| top10.rankings.forEach(mp => { | |
| const item = document.createElement('div'); | |
| item.className = 'ranking-item'; | |
| const number = document.createElement('div'); | |
| number.className = 'ranking-number'; | |
| number.textContent = String(mp.rank); | |
| const info = document.createElement('div'); | |
| info.className = 'ranking-info'; | |
| const name = document.createElement('div'); | |
| name.className = 'ranking-name'; | |
| name.textContent = `${mp.firstName} ${mp.lastName}`; | |
| const party = document.createElement('div'); | |
| party.className = 'ranking-party'; | |
| party.textContent = mp.party; | |
| const role = document.createElement('div'); | |
| role.className = 'ranking-role'; | |
| role.textContent = mp.role; | |
| info.appendChild(name); | |
| info.appendChild(party); | |
| info.appendChild(role); | |
| const score = document.createElement('div'); | |
| score.className = 'ranking-score'; | |
| const scoreValue = document.createElement('div'); | |
| scoreValue.className = 'score-value'; | |
| scoreValue.textContent = mp.influenceScore.toFixed(1); | |
| const scoreLabel = document.createElement('div'); | |
| scoreLabel.className = 'score-label'; | |
| scoreLabel.textContent = 'Influence'; | |
| score.appendChild(scoreValue); | |
| score.appendChild(scoreLabel); | |
| item.appendChild(number); | |
| item.appendChild(info); | |
| item.appendChild(score); | |
| fragment.appendChild(item); | |
| }); | |
| container.appendChild(fragment); |
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Replaced innerHTML with safe DOM methods (createElement, textContent, appendChild). All user data now safely rendered via document fragment for better security and performance.
dashboard/election-predictions.js
Outdated
| container.innerHTML = parties.map(party => { | ||
| const changeClass = party.change >= 0 ? 'positive' : 'negative'; | ||
| const changeSymbol = party.change >= 0 ? '+' : ''; | ||
| const cardClass = party.change >= 0 ? 'gain' : 'loss'; | ||
|
|
||
| return ` | ||
| <div class="prediction-card ${cardClass}"> | ||
| <h3 class="prediction-party">${party.name}</h3> | ||
| <div class="prediction-seats"> | ||
| <div class="seats-current"> | ||
| <div style="font-size: 0.75rem; color: var(--text-secondary);">Current</div> | ||
| <strong>${party.currentSeats}</strong> | ||
| </div> | ||
| <div class="seats-arrow">→</div> | ||
| <div class="seats-predicted"> | ||
| <div style="font-size: 0.75rem; color: var(--text-secondary);">Predicted</div> | ||
| <strong>${party.predictedSeats}</strong> | ||
| </div> | ||
| </div> | ||
| <div class="seats-change ${changeClass}"> | ||
| ${changeSymbol}${party.change} seats (${party.voteShare}%) | ||
| </div> | ||
| <div class="confidence-interval"> | ||
| 95% CI: ${party.confidenceInterval.min}-${party.confidenceInterval.max} seats | ||
| </div> | ||
| </div> | ||
| `; | ||
| }).join(''); | ||
| } | ||
|
|
||
| /** | ||
| * Render coalition scenarios | ||
| */ | ||
| renderCoalitionScenarios() { | ||
| const container = document.getElementById('coalition-scenarios'); | ||
| const { coalitionScenarios } = this.data; | ||
|
|
||
| container.innerHTML = coalitionScenarios.map(scenario => { | ||
| const majorityClass = scenario.majority ? 'yes' : 'no'; | ||
| const majorityText = scenario.majority ? 'Majority ✓' : 'No Majority'; | ||
|
|
||
| return ` | ||
| <div class="scenario-card"> | ||
| <div class="scenario-probability">${scenario.probability}%</div> | ||
| <h3 class="scenario-name">${scenario.name}</h3> | ||
| <div class="scenario-composition"> | ||
| ${scenario.composition.map(partyId => | ||
| `<span class="party-badge">${partyId}</span>` | ||
| ).join('')} | ||
| </div> | ||
| <div class="scenario-seats"> | ||
| <strong>${scenario.totalSeats}</strong> seats (175 required for majority) | ||
| </div> | ||
| <span class="scenario-majority ${majorityClass}"> | ||
| ${majorityText} | ||
| </span> | ||
| <div style="margin-top: var(--spacing-sm); font-size: var(--font-size-sm); color: var(--text-secondary);"> | ||
| Risk Level: <strong>${scenario.riskLevel}</strong> | ||
| </div> | ||
| </div> | ||
| `; | ||
| }).join(''); |
There was a problem hiding this comment.
Potential XSS (Cross-Site Scripting) vulnerability: Data from external sources is directly injected into innerHTML without sanitization. String values like party.name, scenario.name, and partyId are directly interpolated into HTML without escaping.
While the current mock data appears safe, production data from CIA election analysis exports could potentially contain malicious HTML/JavaScript that would execute in the user's browser.
Recommendation: Either:
- Implement HTML sanitization using DOMPurify or similar library before injecting into innerHTML
- Use DOM methods (createElement, appendChild, textContent) instead of innerHTML
- At minimum, create a helper function to escape HTML entities before interpolation
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Completely rewrote renderSeatPredictions() and renderCoalitionScenarios() using DOM methods instead of innerHTML. All party names and scenario data now safely rendered via createElement and textContent.
| "title": "Voting Patterns Analysis", | ||
| "description": "Heatmap data for parliamentary voting behavior and patterns", | ||
| "lastUpdated": "2026-02-05T15:00:00Z", | ||
| "analysisperiod": "2022-2026", |
There was a problem hiding this comment.
Inconsistent naming convention: The property "analysisperiod" uses lowercase while other similar properties in the codebase use camelCase (e.g., "lastUpdated").
For consistency with the rest of the JSON structure and JavaScript conventions, this should be renamed to "analysisPeriod" with a capital "P".
| "analysisperiod": "2022-2026", | |
| "analysisPeriod": "2022-2026", |
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Renamed "analysisperiod" to "analysisPeriod" for consistency with camelCase convention used throughout the codebase.
dashboard/README.md
Outdated
|
|
||
| ### Content Security Policy | ||
| - Chart.js loaded via CDN with SRI (Subresource Integrity) | ||
| - No inline scripts (CSP compliant) |
There was a problem hiding this comment.
Documentation inaccuracy: The README states "No inline scripts (CSP compliant)" under the Security section, but the implementation actually includes inline styles in multiple places:
- Homepage dashboard link section (index.html lines 143-149)
- JavaScript-generated HTML in election-predictions.js (lines 28, 33, 74)
This documentation should be updated to reflect the actual implementation, or better yet, the inline styles should be removed to match the documented security posture.
| - No inline scripts (CSP compliant) | |
| - No inline scripts; limited inline styles currently in use (CSP hardening in progress) |
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Updated README security section to accurately state "No inline scripts; inline styles being phased out (CSP hardening in progress)". All inline styles have now been removed.
dashboard/cia-visualizations.js
Outdated
| container.innerHTML = committees.committees.map(committee => ` | ||
| <div class="committee-card"> | ||
| <h3 class="committee-name">${committee.name}</h3> | ||
| <div class="committee-stats"> | ||
| <div class="committee-stat"> | ||
| <span class="stat-label">Members:</span> | ||
| <span class="stat-value">${committee.memberCount}</span> | ||
| </div> | ||
| <div class="committee-stat"> | ||
| <span class="stat-label">Influence:</span> | ||
| <span class="stat-value">${committee.influenceScore.toFixed(1)}</span> | ||
| </div> | ||
| <div class="committee-stat"> | ||
| <span class="stat-label">Meetings/Year:</span> | ||
| <span class="stat-value">${committee.meetingsPerYear}</span> | ||
| </div> | ||
| <div class="committee-stat"> | ||
| <span class="stat-label">Documents:</span> | ||
| <span class="stat-value">${committee.documentsProcessed}</span> | ||
| </div> | ||
| </div> | ||
| <div class="committee-issues"> | ||
| <h4>Key Issues</h4> | ||
| ${committee.keyIssues.map(issue => | ||
| `<span class="issue-tag">${issue}</span>` | ||
| ).join('')} | ||
| </div> | ||
| </div> | ||
| `).join(''); | ||
|
|
||
| // Add simple network visualization note | ||
| const networkViz = document.getElementById('network-visualization'); | ||
| networkViz.innerHTML = ` | ||
| <div> | ||
| <p><strong>Network Graph:</strong> Interactive committee network visualization would be rendered here using D3.js or similar library.</p> | ||
| <p>Current data shows ${committees.networkGraph.nodes.length} committees with ${committees.networkGraph.edges.length} interconnections.</p> | ||
| </div> | ||
| `; |
There was a problem hiding this comment.
Potential XSS (Cross-Site Scripting) vulnerability: Data from external sources is directly injected into innerHTML without sanitization. String values like committee.name and issue (from committee.keyIssues array) are directly interpolated into HTML without escaping.
While the current mock data appears safe, production data from CIA exports could potentially contain malicious HTML/JavaScript that would execute in the user's browser.
Recommendation: Either:
- Implement HTML sanitization using DOMPurify or similar library before injecting into innerHTML
- Use DOM methods (createElement, appendChild, textContent) instead of innerHTML for better security
- At minimum, create a helper function to escape HTML entities before interpolation
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Replaced innerHTML with safe DOM methods. Committee cards now constructed using createElement, textContent, and appendChild for secure rendering.
| ``` | ||
| dashboard/ | ||
| ├── index.html # Main dashboard page (English) | ||
| ├── styles.css # Dashboard-specific styles | ||
| ├── cia-data-loader.js # Data loading module | ||
| ├── cia-visualizations.js # Chart rendering module | ||
| └── election-predictions.js # Election 2026 predictions renderer |
There was a problem hiding this comment.
Documentation incompleteness: The File Structure section lists only "index.html" for the Main dashboard page, but the implementation includes both English (index.html) and Swedish (index_sv.html) versions.
The file structure should be updated to:
dashboard/
├── index.html # Main dashboard page (English)
├── index_sv.html # Main dashboard page (Swedish)
├── styles.css # Dashboard-specific styles
...
This would accurately reflect the multi-language implementation mentioned elsewhere in the README (line 118).
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Updated README file structure to include both index.html (English) and index_sv.html (Swedish).
dashboard/index.html
Outdated
| <a href="index_da.html" title="Dansk">Dansk</a> | | ||
| <a href="index_no.html" title="Norsk">Norsk</a> | | ||
| <a href="index_fi.html" title="Suomi">Suomi</a> |
There was a problem hiding this comment.
Broken links: The language switcher in the footer links to dashboard pages that don't exist yet (index_da.html, index_no.html, index_fi.html). According to the PR description, only English and Swedish versions are implemented, with "Additional 12 language versions" listed under "Remaining" work.
These links will result in 404 errors until the other language versions are created. Consider either:
- Removing these links until the pages exist
- Adding a "Coming soon" indicator
- Linking to the main site's language pages as a fallback (e.g., ../index_da.html)
The same issue exists in index_sv.html at the same location.
| <a href="index_da.html" title="Dansk">Dansk</a> | | |
| <a href="index_no.html" title="Norsk">Norsk</a> | | |
| <a href="index_fi.html" title="Suomi">Suomi</a> | |
| <a href="../index_da.html" title="Dansk">Dansk</a> | | |
| <a href="../index_no.html" title="Norsk">Norsk</a> | | |
| <a href="../index_fi.html" title="Suomi">Suomi</a> |
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Language switcher links now point to main site pages (../index_da.html, ../index_no.html, ../index_fi.html) as fallback until dashboard versions are created.
dashboard/index_sv.html
Outdated
| <a href="index_da.html" title="Dansk">Dansk</a> | | ||
| <a href="index_no.html" title="Norsk">Norsk</a> | | ||
| <a href="index_fi.html" title="Suomi">Suomi</a> |
There was a problem hiding this comment.
Broken links: The language switcher in the footer links to dashboard pages that don't exist yet (index_da.html, index_no.html, index_fi.html). According to the PR description, only English and Swedish versions are implemented, with "Additional 12 language versions" listed under "Remaining" work.
These links will result in 404 errors until the other language versions are created. Consider either:
- Removing these links until the pages exist
- Adding a "Coming soon" indicator
- Linking to the main site's language pages as a fallback (e.g., ../index_da.html)
| <a href="index_da.html" title="Dansk">Dansk</a> | | |
| <a href="index_no.html" title="Norsk">Norsk</a> | | |
| <a href="index_fi.html" title="Suomi">Suomi</a> | |
| <a href="../index_da.html" title="Dansk">Dansk</a> | | |
| <a href="../index_no.html" title="Norsk">Norsk</a> | | |
| <a href="../index_fi.html" title="Suomi">Suomi</a> |
There was a problem hiding this comment.
Fixed in commit 3d15ec4. Language switcher links now point to main site pages (../index_da.html, ../index_no.html, ../index_fi.html) as fallback until dashboard versions are created.
dashboard/cia-visualizations.js
Outdated
|
|
||
| // Prepare data for matrix visualization | ||
| const matrix = votingPatterns.votingMatrix; | ||
| const datasets = matrix.agreementMatrix.map((row, i) => ({ |
|
@copilot apply changes based on the comments in this thread |
…issues Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
All 14 review comments have been addressed in commit 3d15ec4: Security Fixes:
Code Quality:
Documentation:
HTML validation: 0 errors | JSON validation: All valid Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
dashboard/README.md
Outdated
| ### External Libraries | ||
| - **Chart.js v4.4.1**: Chart rendering (60KB minified) | ||
| - CDN: `https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js` | ||
| - SRI: `sha384-VzJbN2v1vFcGJCeP5T5XREFm3+OIH7d2qRMOA/fHtfDO5Cd2Qc6bjqH8R5RZqQkQ` |
There was a problem hiding this comment.
The SRI hash documented in the README.md file (line 178) does not match the SRI hash actually used in the HTML files (lines 29 in both index.html and index_sv.html). The HTML files use the correct hash 'sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb', but the README still documents the old incorrect hash. This discrepancy could confuse developers who reference the documentation.
| - SRI: `sha384-VzJbN2v1vFcGJCeP5T5XREFm3+OIH7d2qRMOA/fHtfDO5Cd2Qc6bjqH8R5RZqQkQ` | |
| - SRI: `sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb` |
There was a problem hiding this comment.
Fixed in commit bd223ce. Updated README.md SRI hash to match implementation: sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb
dashboard/cia-visualizations.js
Outdated
| /** | ||
| * Helper function to escape HTML to prevent XSS | ||
| * @param {string} text - Text to escape | ||
| * @returns {string} - Escaped text | ||
| */ | ||
| function escapeHtml(text) { | ||
| const div = document.createElement('div'); | ||
| div.textContent = text; | ||
| return div.innerHTML; | ||
| } | ||
|
|
There was a problem hiding this comment.
The escapeHtml helper function is defined but never actually used in this file. While the code correctly uses DOM methods (createElement, textContent) for security, which is even better than escaping, this unused function should be removed to avoid confusion and maintain code cleanliness. If the function was intended for future use, consider whether it's actually needed given that DOM methods are being used throughout.
| /** | |
| * Helper function to escape HTML to prevent XSS | |
| * @param {string} text - Text to escape | |
| * @returns {string} - Escaped text | |
| */ | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } |
There was a problem hiding this comment.
Fixed in commit bd223ce. Removed unused escapeHtml function from cia-visualizations.js. Function was redundant since all rendering uses safe DOM methods.
dashboard/election-predictions.js
Outdated
| /** | ||
| * Helper function to escape HTML to prevent XSS | ||
| * @param {string} text - Text to escape | ||
| * @returns {string} - Escaped text | ||
| */ | ||
| function escapeHtml(text) { | ||
| const div = document.createElement('div'); | ||
| div.textContent = text; | ||
| return div.innerHTML; | ||
| } |
There was a problem hiding this comment.
The escapeHtml helper function is defined but never actually used in this file. While the code correctly uses DOM methods (createElement, textContent) for security, which is even better than escaping, this unused function should be removed to avoid confusion and maintain code cleanliness. If the function was intended for future use, consider whether it's actually needed given that DOM methods are being used throughout.
There was a problem hiding this comment.
Fixed in commit bd223ce. Removed unused escapeHtml function from election-predictions.js. Function was redundant since all rendering uses safe DOM methods.
dashboard/election-predictions.js
Outdated
| const { keyFactors } = this.data; | ||
|
|
||
| return ` | ||
| <div class="key-factors"> | ||
| <h3>Key Election Factors</h3> | ||
| <ul> | ||
| ${keyFactors.map(factor => `<li>${factor}</li>`).join('')} | ||
| </ul> | ||
| </div> | ||
| `; |
There was a problem hiding this comment.
The renderKeyFactors method returns HTML strings with potential XSS vulnerability. The factor values from the data are directly interpolated into HTML without sanitization. If the CIA export data contains malicious HTML/JavaScript in the keyFactors array, it could execute in the user's browser. This method should be rewritten to use DOM methods like the other render methods in this file (renderSeatPredictions and renderCoalitionScenarios), which correctly use createElement and textContent for security.
| const { keyFactors } = this.data; | |
| return ` | |
| <div class="key-factors"> | |
| <h3>Key Election Factors</h3> | |
| <ul> | |
| ${keyFactors.map(factor => `<li>${factor}</li>`).join('')} | |
| </ul> | |
| </div> | |
| `; | |
| const container = document.getElementById('key-factors'); | |
| const { keyFactors } = this.data; | |
| if (!container) { | |
| return; | |
| } | |
| // Clear existing content safely | |
| container.textContent = ''; | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'key-factors'; | |
| const heading = document.createElement('h3'); | |
| heading.textContent = 'Key Election Factors'; | |
| wrapper.appendChild(heading); | |
| const list = document.createElement('ul'); | |
| keyFactors.forEach(factor => { | |
| const listItem = document.createElement('li'); | |
| // Use textContent to prevent XSS from untrusted factor values | |
| listItem.textContent = String(factor); | |
| list.appendChild(listItem); | |
| }); | |
| wrapper.appendChild(list); | |
| container.appendChild(wrapper); |
There was a problem hiding this comment.
Fixed in commit bd223ce. Completely rewrote renderKeyFactors() using DOM methods (createElement, textContent, appendChild). XSS vulnerability eliminated - key factors now safely rendered.
dashboard/cia-visualizations.js
Outdated
| document.getElementById('metric-total-mps').textContent = | ||
| overview.keyMetrics.totalMPs; | ||
| document.getElementById('metric-total-parties').textContent = | ||
| overview.keyMetrics.totalParties; | ||
| document.getElementById('metric-risk-rules').textContent = | ||
| overview.keyMetrics.totalRiskRules; | ||
| document.getElementById('metric-coalition-seats').textContent = | ||
| overview.keyMetrics.coalitionSeats; | ||
|
|
||
| // Update risk alerts | ||
| document.getElementById('alert-critical').textContent = | ||
| overview.riskAlerts.last90Days.critical; | ||
| document.getElementById('alert-major').textContent = | ||
| overview.riskAlerts.last90Days.major; | ||
| document.getElementById('alert-minor').textContent = | ||
| overview.riskAlerts.last90Days.minor; |
There was a problem hiding this comment.
The renderKeyMetrics method accesses DOM elements without checking if they exist. If any of the elements with IDs like 'metric-total-mps', 'alert-critical', etc. are missing from the HTML, this will throw a TypeError when attempting to set textContent on null. While the current HTML files include these elements, adding null checks would make the code more robust and prevent runtime errors if the HTML structure changes. Consider adding checks like: const element = document.getElementById('metric-total-mps'); if (element) element.textContent = ...
| document.getElementById('metric-total-mps').textContent = | |
| overview.keyMetrics.totalMPs; | |
| document.getElementById('metric-total-parties').textContent = | |
| overview.keyMetrics.totalParties; | |
| document.getElementById('metric-risk-rules').textContent = | |
| overview.keyMetrics.totalRiskRules; | |
| document.getElementById('metric-coalition-seats').textContent = | |
| overview.keyMetrics.coalitionSeats; | |
| // Update risk alerts | |
| document.getElementById('alert-critical').textContent = | |
| overview.riskAlerts.last90Days.critical; | |
| document.getElementById('alert-major').textContent = | |
| overview.riskAlerts.last90Days.major; | |
| document.getElementById('alert-minor').textContent = | |
| overview.riskAlerts.last90Days.minor; | |
| const totalMpsEl = document.getElementById('metric-total-mps'); | |
| if (totalMpsEl) { | |
| totalMpsEl.textContent = overview.keyMetrics.totalMPs; | |
| } | |
| const totalPartiesEl = document.getElementById('metric-total-parties'); | |
| if (totalPartiesEl) { | |
| totalPartiesEl.textContent = overview.keyMetrics.totalParties; | |
| } | |
| const riskRulesEl = document.getElementById('metric-risk-rules'); | |
| if (riskRulesEl) { | |
| riskRulesEl.textContent = overview.keyMetrics.totalRiskRules; | |
| } | |
| const coalitionSeatsEl = document.getElementById('metric-coalition-seats'); | |
| if (coalitionSeatsEl) { | |
| coalitionSeatsEl.textContent = overview.keyMetrics.coalitionSeats; | |
| } | |
| // Update risk alerts | |
| const alertCriticalEl = document.getElementById('alert-critical'); | |
| if (alertCriticalEl) { | |
| alertCriticalEl.textContent = overview.riskAlerts.last90Days.critical; | |
| } | |
| const alertMajorEl = document.getElementById('alert-major'); | |
| if (alertMajorEl) { | |
| alertMajorEl.textContent = overview.riskAlerts.last90Days.major; | |
| } | |
| const alertMinorEl = document.getElementById('alert-minor'); | |
| if (alertMinorEl) { | |
| alertMinorEl.textContent = overview.riskAlerts.last90Days.minor; | |
| } |
There was a problem hiding this comment.
Fixed in commit bd223ce. Added null checks for all 7 getElementById calls in renderKeyMetrics(). Code now handles missing DOM elements gracefully, preventing TypeErrors.
| getSummaryStats() { | ||
| const { parties } = this.data.forecast; | ||
|
|
||
| return { | ||
| totalSeats: parties.reduce((sum, p) => sum + p.predictedSeats, 0), | ||
| gainers: parties.filter(p => p.change > 0).length, | ||
| losers: parties.filter(p => p.change < 0).length, | ||
| stable: parties.filter(p => p.change === 0).length, | ||
| biggestGain: parties.reduce((max, p) => p.change > max.change ? p : max, parties[0]), | ||
| biggestLoss: parties.reduce((min, p) => p.change < min.change ? p : min, parties[0]) | ||
| }; |
There was a problem hiding this comment.
The getSummaryStats method could throw an error if the parties array is empty. The reduce operations on lines 212-213 use parties[0] as the initial value, which will be undefined if the array is empty, leading to a TypeError. While the current data has 8 parties, defensive programming would suggest either checking if the array is non-empty before calling reduce, or providing a safer initial value. For example: parties.length > 0 ? parties.reduce(..., parties[0]) : null
There was a problem hiding this comment.
Fixed in commit bd223ce. Added empty array check in getSummaryStats(). Returns safe default values (zeros/nulls) if parties array is empty, preventing TypeError from reduce operations.
|
@copilot apply changes based on the comments in this thread |
…x XSS in renderKeyFactors Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
All 6 review comments have been addressed in commit bd223ce: Code Quality:
Security:
Robustness:
HTML validation: 0 errors | JSON validation: All valid |
| renderSeatPredictions() { | ||
| const container = document.getElementById('seat-predictions'); | ||
| const { parties } = this.data.forecast; | ||
|
|
||
| if (!container) return; | ||
|
|
||
| // Clear existing content safely | ||
| container.textContent = ''; | ||
|
|
||
| const fragment = document.createDocumentFragment(); | ||
|
|
||
| parties.forEach(party => { |
There was a problem hiding this comment.
The renderSeatPredictions method doesn't check if this.data.forecast or this.data.forecast.parties exists before calling forEach on line 25. If the election data is malformed or missing these properties, this will cause a TypeError.
Add a defensive check: verify that this.data.forecast and this.data.forecast.parties exist and that parties is an array before attempting to iterate over it.
There was a problem hiding this comment.
Fixed in commit 49a11e3. Added defensive checks to verify this.data.forecast and this.data.forecast.parties exist and are arrays before iteration. Method now fails gracefully with console warning if data is malformed.
| renderCoalitionScenarios() { | ||
| const container = document.getElementById('coalition-scenarios'); | ||
| const { coalitionScenarios } = this.data; | ||
|
|
||
| if (!container) return; | ||
|
|
||
| // Clear existing content safely | ||
| container.textContent = ''; | ||
|
|
||
| const fragment = document.createDocumentFragment(); | ||
|
|
||
| coalitionScenarios.forEach(scenario => { |
There was a problem hiding this comment.
The renderCoalitionScenarios method doesn't check if this.data.coalitionScenarios exists or is an array before calling forEach on line 106. If the election data is malformed or missing this property, this will cause a TypeError.
Add a defensive check: verify that coalitionScenarios exists and is an array before attempting to iterate over it.
There was a problem hiding this comment.
Fixed in commit 49a11e3. Added defensive check to verify coalitionScenarios exists and is an array before iteration. Method now fails gracefully with console warning if data is missing.
dashboard/index.html
Outdated
| <script type="module"> | ||
| // Main dashboard initialization | ||
| import { CIADataLoader } from './cia-data-loader.js'; | ||
| import { CIADashboardRenderer } from './cia-visualizations.js'; | ||
| import { Election2026Predictions } from './election-predictions.js'; | ||
|
|
||
| async function initDashboard() { | ||
| const loader = new CIADataLoader(); | ||
|
|
||
| try { | ||
| // Load all CIA exports | ||
| const [overview, election, partyPerf, top10, committees, votingPatterns] = await Promise.all([ | ||
| loader.loadOverviewDashboard(), | ||
| loader.loadElectionAnalysis(), | ||
| loader.loadPartyPerformance(), | ||
| loader.loadTop10Influential(), | ||
| loader.loadCommitteeNetwork(), | ||
| loader.loadVotingPatterns() | ||
| ]); | ||
|
|
||
| // Hide loading state | ||
| document.getElementById('loading-state').classList.add('hidden'); | ||
| document.getElementById('dashboard-content').classList.remove('hidden'); | ||
|
|
||
| // Initialize renderers | ||
| const renderer = new CIADashboardRenderer({ | ||
| overview, | ||
| partyPerf, | ||
| top10, | ||
| committees, | ||
| votingPatterns | ||
| }); | ||
|
|
||
| const electionRenderer = new Election2026Predictions(election); | ||
|
|
||
| // Render all sections | ||
| renderer.renderKeyMetrics(); | ||
| renderer.renderPartyPerformance(); | ||
| renderer.renderTop10Rankings(); | ||
| renderer.renderVotingPatterns(); | ||
| renderer.renderCommitteeNetwork(); | ||
|
|
||
| electionRenderer.renderSeatPredictions(); | ||
| electionRenderer.renderCoalitionScenarios(); | ||
|
|
||
| } catch (error) { | ||
| console.error('Dashboard initialization error:', error); | ||
| document.getElementById('loading-state').classList.add('hidden'); | ||
| document.getElementById('error-state').classList.remove('hidden'); | ||
| document.getElementById('error-message').textContent = error.message; | ||
|
|
||
| // Retry button handler | ||
| document.getElementById('retry-button').addEventListener('click', () => { | ||
| location.reload(); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Initialize on DOM ready | ||
| if (document.readyState === 'loading') { | ||
| document.addEventListener('DOMContentLoaded', initDashboard); | ||
| } else { | ||
| initDashboard(); | ||
| } | ||
| </script> |
There was a problem hiding this comment.
Inline script block violates Content Security Policy (CSP) best practices as specified in the repository's security architecture. According to the custom coding guidelines, inline scripts should be avoided. The dashboard initialization code (lines 180-244) should be moved to a separate JavaScript file (e.g., dashboard-init.js or main.js).
This would align with the documented security posture and the statement in README.md that "inline scripts" should be avoided. While this is acknowledged as "CSP hardening in progress" in the README, moving this initialization code to an external module would complete this hardening effort.
| <script type="module"> | |
| // Main dashboard initialization | |
| import { CIADataLoader } from './cia-data-loader.js'; | |
| import { CIADashboardRenderer } from './cia-visualizations.js'; | |
| import { Election2026Predictions } from './election-predictions.js'; | |
| async function initDashboard() { | |
| const loader = new CIADataLoader(); | |
| try { | |
| // Load all CIA exports | |
| const [overview, election, partyPerf, top10, committees, votingPatterns] = await Promise.all([ | |
| loader.loadOverviewDashboard(), | |
| loader.loadElectionAnalysis(), | |
| loader.loadPartyPerformance(), | |
| loader.loadTop10Influential(), | |
| loader.loadCommitteeNetwork(), | |
| loader.loadVotingPatterns() | |
| ]); | |
| // Hide loading state | |
| document.getElementById('loading-state').classList.add('hidden'); | |
| document.getElementById('dashboard-content').classList.remove('hidden'); | |
| // Initialize renderers | |
| const renderer = new CIADashboardRenderer({ | |
| overview, | |
| partyPerf, | |
| top10, | |
| committees, | |
| votingPatterns | |
| }); | |
| const electionRenderer = new Election2026Predictions(election); | |
| // Render all sections | |
| renderer.renderKeyMetrics(); | |
| renderer.renderPartyPerformance(); | |
| renderer.renderTop10Rankings(); | |
| renderer.renderVotingPatterns(); | |
| renderer.renderCommitteeNetwork(); | |
| electionRenderer.renderSeatPredictions(); | |
| electionRenderer.renderCoalitionScenarios(); | |
| } catch (error) { | |
| console.error('Dashboard initialization error:', error); | |
| document.getElementById('loading-state').classList.add('hidden'); | |
| document.getElementById('error-state').classList.remove('hidden'); | |
| document.getElementById('error-message').textContent = error.message; | |
| // Retry button handler | |
| document.getElementById('retry-button').addEventListener('click', () => { | |
| location.reload(); | |
| }); | |
| } | |
| } | |
| // Initialize on DOM ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', initDashboard); | |
| } else { | |
| initDashboard(); | |
| } | |
| </script> | |
| <script type="module" src="dashboard-init.js"></script> |
There was a problem hiding this comment.
Fixed in commit 49a11e3. Created dashboard-init.js external module and moved all inline initialization code. HTML now uses <script type="module" src="dashboard-init.js"></script>. CSP hardening complete - no inline scripts remain.
| keyFactors.forEach(factor => { | ||
| const listItem = document.createElement('li'); | ||
| // Use textContent to prevent XSS from untrusted factor values | ||
| listItem.textContent = String(factor); | ||
| list.appendChild(listItem); | ||
| }); |
There was a problem hiding this comment.
The renderKeyFactors method doesn't check if keyFactors exists or is an array before calling forEach on line 185. If the election data is malformed or missing this property, this will cause a TypeError.
Add a defensive check: verify that keyFactors exists and is an array before attempting to iterate over it.
| keyFactors.forEach(factor => { | |
| const listItem = document.createElement('li'); | |
| // Use textContent to prevent XSS from untrusted factor values | |
| listItem.textContent = String(factor); | |
| list.appendChild(listItem); | |
| }); | |
| if (Array.isArray(keyFactors)) { | |
| keyFactors.forEach(factor => { | |
| const listItem = document.createElement('li'); | |
| // Use textContent to prevent XSS from untrusted factor values | |
| listItem.textContent = String(factor); | |
| list.appendChild(listItem); | |
| }); | |
| } |
There was a problem hiding this comment.
Fixed in commit 49a11e3. Added defensive check to verify keyFactors exists and is an array before forEach iteration. Method now fails gracefully with console warning if data is invalid.
| renderPartyPerformance() { | ||
| const { partyPerf } = this.data; | ||
|
|
||
| // Party Seats Chart | ||
| const seatsCtx = document.getElementById('party-seats-chart'); | ||
| if (seatsCtx && typeof Chart !== 'undefined') { | ||
| this.charts.seats = new Chart(seatsCtx, { | ||
| type: 'bar', | ||
| data: { | ||
| labels: partyPerf.parties.map(p => p.shortName), | ||
| datasets: [{ | ||
| label: 'Current Seats', | ||
| data: partyPerf.parties.map(p => p.metrics.seats), | ||
| backgroundColor: [ | ||
| 'rgba(224, 32, 32, 0.8)', // S - Red | ||
| 'rgba(221, 171, 0, 0.8)', // SD - Yellow | ||
| 'rgba(82, 126, 196, 0.8)', // M - Blue | ||
| 'rgba(175, 8, 42, 0.8)', // V - Dark Red | ||
| 'rgba(0, 150, 65, 0.8)', // C - Green | ||
| 'rgba(0, 90, 170, 0.8)', // KD - Dark Blue | ||
| 'rgba(83, 160, 60, 0.8)', // MP - Green | ||
| 'rgba(0, 106, 179, 0.8)' // L - Blue | ||
| ], | ||
| borderColor: [ | ||
| 'rgb(224, 32, 32)', | ||
| 'rgb(221, 171, 0)', | ||
| 'rgb(82, 126, 196)', | ||
| 'rgb(175, 8, 42)', | ||
| 'rgb(0, 150, 65)', | ||
| 'rgb(0, 90, 170)', | ||
| 'rgb(83, 160, 60)', | ||
| 'rgb(0, 106, 179)' | ||
| ], | ||
| borderWidth: 2 | ||
| }] | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| title: { | ||
| display: true, | ||
| text: 'Current Riksdag Seats by Party', | ||
| font: { size: 16, weight: 'bold' } | ||
| }, | ||
| legend: { | ||
| display: false | ||
| } | ||
| }, | ||
| scales: { | ||
| y: { | ||
| beginAtZero: true, | ||
| max: 120, | ||
| title: { | ||
| display: true, | ||
| text: 'Number of Seats' | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Party Cohesion Chart | ||
| const cohesionCtx = document.getElementById('party-cohesion-chart'); | ||
| if (cohesionCtx && typeof Chart !== 'undefined') { | ||
| this.charts.cohesion = new Chart(cohesionCtx, { | ||
| type: 'line', | ||
| data: { | ||
| labels: partyPerf.parties.map(p => p.shortName), | ||
| datasets: [{ | ||
| label: 'Voting Cohesion (%)', | ||
| data: partyPerf.parties.map(p => p.voting.cohesionScore), | ||
| borderColor: 'rgb(0, 102, 51)', | ||
| backgroundColor: 'rgba(0, 102, 51, 0.1)', | ||
| tension: 0.4, | ||
| fill: true, | ||
| pointRadius: 5, | ||
| pointHoverRadius: 7 | ||
| }, { | ||
| label: 'Rebellion Rate (%)', | ||
| data: partyPerf.parties.map(p => p.voting.rebellionRate), | ||
| borderColor: 'rgb(220, 53, 69)', | ||
| backgroundColor: 'rgba(220, 53, 69, 0.1)', | ||
| tension: 0.4, | ||
| fill: true, | ||
| pointRadius: 5, | ||
| pointHoverRadius: 7 | ||
| }] | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| title: { | ||
| display: true, | ||
| text: 'Party Voting Cohesion vs Rebellion Rate', | ||
| font: { size: 16, weight: 'bold' } | ||
| } | ||
| }, | ||
| scales: { | ||
| y: { | ||
| beginAtZero: true, | ||
| max: 100, | ||
| title: { | ||
| display: true, | ||
| text: 'Percentage (%)' | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The renderPartyPerformance method doesn't check if partyPerf.parties exists or is an array before calling map() on lines 63, 66, 123, 126, and 135. If the party performance data is malformed or missing the parties property, this will cause a TypeError.
Add a defensive check at the beginning of the method to verify that partyPerf and partyPerf.parties exist and that parties is an array. If not, return early or handle the error gracefully.
There was a problem hiding this comment.
Fixed in commit 49a11e3. Added defensive check at method start to verify partyPerf and partyPerf.parties exist and parties is an array. Returns early with console warning if data structure is invalid.
| renderVotingPatterns() { | ||
| const { votingPatterns } = this.data; | ||
| const ctx = document.getElementById('voting-heatmap'); | ||
|
|
||
| if (!ctx || typeof Chart === 'undefined') return; | ||
|
|
||
| // Prepare data for matrix visualization | ||
| const matrix = votingPatterns.votingMatrix; | ||
|
|
||
| // Using a bar chart as a simple heatmap alternative | ||
| this.charts.heatmap = new Chart(ctx, { | ||
| type: 'bar', | ||
| data: { | ||
| labels: matrix.labels, | ||
| datasets: matrix.agreementMatrix.map((row, i) => ({ | ||
| label: matrix.partyNames[i], | ||
| data: row, | ||
| backgroundColor: `hsla(${i * 45}, 70%, 50%, 0.6)`, | ||
| stack: 'Stack ' + i | ||
| })) | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| title: { | ||
| display: true, | ||
| text: 'Party Agreement Matrix (%)', | ||
| font: { size: 16, weight: 'bold' } | ||
| }, | ||
| legend: { | ||
| display: true, | ||
| position: 'right' | ||
| } | ||
| }, | ||
| scales: { | ||
| x: { | ||
| title: { | ||
| display: true, | ||
| text: 'Parties' | ||
| } | ||
| }, | ||
| y: { | ||
| beginAtZero: true, | ||
| max: 100, | ||
| title: { | ||
| display: true, | ||
| text: 'Agreement %' | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
The renderVotingPatterns method doesn't check if votingPatterns.votingMatrix exists before accessing its properties (matrix.labels, matrix.partyNames, matrix.agreementMatrix) on lines 250-256. If the voting patterns data is malformed or missing these properties, this will cause a TypeError.
Add defensive checks: verify that votingPatterns, votingPatterns.votingMatrix, and all required nested properties exist before using them.
There was a problem hiding this comment.
Fixed in commit 49a11e3. Added comprehensive defensive checks for votingPatterns.votingMatrix and all nested properties (labels, partyNames, agreementMatrix). Method returns early with console warning if any required property is missing.
| renderCommitteeNetwork() { | ||
| const { committees } = this.data; | ||
| const container = document.getElementById('committee-list'); | ||
|
|
||
| if (!container) return; | ||
|
|
||
| // Clear existing content safely | ||
| container.textContent = ''; | ||
|
|
||
| const fragment = document.createDocumentFragment(); | ||
|
|
||
| committees.committees.forEach(committee => { |
There was a problem hiding this comment.
The renderCommitteeNetwork method doesn't check if committees.committees exists or is an array before calling forEach on line 306. If the committee data is malformed or missing the committees property, this will cause a TypeError.
Add a defensive check: verify that committees and committees.committees exist and that the committees property is an array before attempting to iterate over it.
There was a problem hiding this comment.
Fixed in commit 49a11e3. Added defensive check to verify committees and committees.committees exist and committees property is an array before iteration. Method now fails gracefully with console warning if data is malformed.
dashboard/index_sv.html
Outdated
| <script type="module"> | ||
| // Main dashboard initialization | ||
| import { CIADataLoader } from './cia-data-loader.js'; | ||
| import { CIADashboardRenderer } from './cia-visualizations.js'; | ||
| import { Election2026Predictions } from './election-predictions.js'; | ||
|
|
||
| async function initDashboard() { | ||
| const loader = new CIADataLoader(); | ||
|
|
||
| try { | ||
| // Load all CIA exports | ||
| const [overview, election, partyPerf, top10, committees, votingPatterns] = await Promise.all([ | ||
| loader.loadOverviewDashboard(), | ||
| loader.loadElectionAnalysis(), | ||
| loader.loadPartyPerformance(), | ||
| loader.loadTop10Influential(), | ||
| loader.loadCommitteeNetwork(), | ||
| loader.loadVotingPatterns() | ||
| ]); | ||
|
|
||
| // Hide loading state | ||
| document.getElementById('loading-state').classList.add('hidden'); | ||
| document.getElementById('dashboard-content').classList.remove('hidden'); | ||
|
|
||
| // Initialize renderers | ||
| const renderer = new CIADashboardRenderer({ | ||
| overview, | ||
| partyPerf, | ||
| top10, | ||
| committees, | ||
| votingPatterns | ||
| }); | ||
|
|
||
| const electionRenderer = new Election2026Predictions(election); | ||
|
|
||
| // Render all sections | ||
| renderer.renderKeyMetrics(); | ||
| renderer.renderPartyPerformance(); | ||
| renderer.renderTop10Rankings(); | ||
| renderer.renderVotingPatterns(); | ||
| renderer.renderCommitteeNetwork(); | ||
|
|
||
| electionRenderer.renderSeatPredictions(); | ||
| electionRenderer.renderCoalitionScenarios(); | ||
|
|
||
| } catch (error) { | ||
| console.error('Dashboard initialization error:', error); | ||
| document.getElementById('loading-state').classList.add('hidden'); | ||
| document.getElementById('error-state').classList.remove('hidden'); | ||
| document.getElementById('error-message').textContent = error.message; | ||
|
|
||
| // Retry button handler | ||
| document.getElementById('retry-button').addEventListener('click', () => { | ||
| location.reload(); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Initialize on DOM ready | ||
| if (document.readyState === 'loading') { | ||
| document.addEventListener('DOMContentLoaded', initDashboard); | ||
| } else { | ||
| initDashboard(); | ||
| } | ||
| </script> |
There was a problem hiding this comment.
Inline script block violates Content Security Policy (CSP) best practices as specified in the repository's security architecture. According to the custom coding guidelines, inline scripts should be avoided. The dashboard initialization code (lines 180-244) should be moved to a separate JavaScript file (e.g., dashboard-init.js or main.js).
This would align with the documented security posture and the statement in README.md that "inline scripts" should be avoided. While this is acknowledged as "CSP hardening in progress" in the README, moving this initialization code to an external module would complete this hardening effort.
| <script type="module"> | |
| // Main dashboard initialization | |
| import { CIADataLoader } from './cia-data-loader.js'; | |
| import { CIADashboardRenderer } from './cia-visualizations.js'; | |
| import { Election2026Predictions } from './election-predictions.js'; | |
| async function initDashboard() { | |
| const loader = new CIADataLoader(); | |
| try { | |
| // Load all CIA exports | |
| const [overview, election, partyPerf, top10, committees, votingPatterns] = await Promise.all([ | |
| loader.loadOverviewDashboard(), | |
| loader.loadElectionAnalysis(), | |
| loader.loadPartyPerformance(), | |
| loader.loadTop10Influential(), | |
| loader.loadCommitteeNetwork(), | |
| loader.loadVotingPatterns() | |
| ]); | |
| // Hide loading state | |
| document.getElementById('loading-state').classList.add('hidden'); | |
| document.getElementById('dashboard-content').classList.remove('hidden'); | |
| // Initialize renderers | |
| const renderer = new CIADashboardRenderer({ | |
| overview, | |
| partyPerf, | |
| top10, | |
| committees, | |
| votingPatterns | |
| }); | |
| const electionRenderer = new Election2026Predictions(election); | |
| // Render all sections | |
| renderer.renderKeyMetrics(); | |
| renderer.renderPartyPerformance(); | |
| renderer.renderTop10Rankings(); | |
| renderer.renderVotingPatterns(); | |
| renderer.renderCommitteeNetwork(); | |
| electionRenderer.renderSeatPredictions(); | |
| electionRenderer.renderCoalitionScenarios(); | |
| } catch (error) { | |
| console.error('Dashboard initialization error:', error); | |
| document.getElementById('loading-state').classList.add('hidden'); | |
| document.getElementById('error-state').classList.remove('hidden'); | |
| document.getElementById('error-message').textContent = error.message; | |
| // Retry button handler | |
| document.getElementById('retry-button').addEventListener('click', () => { | |
| location.reload(); | |
| }); | |
| } | |
| } | |
| // Initialize on DOM ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', initDashboard); | |
| } else { | |
| initDashboard(); | |
| } | |
| </script> | |
| <script type="module" src="dashboard-init.js"></script> |
There was a problem hiding this comment.
Fixed in commit 49a11e3. Created dashboard-init.js external module and moved all inline initialization code from Swedish version. HTML now uses <script type="module" src="dashboard-init.js"></script>. CSP hardening complete.
dashboard/cia-data-loader.js
Outdated
| return await response.json(); | ||
| } catch (fallbackError) { | ||
| console.error(`Both primary and fallback failed for ${filename}:`, fallbackError); | ||
| throw new Error(`Failed to load ${filename}: ${error.message}`); |
There was a problem hiding this comment.
Error handling issue: In the fallback catch block (line 35), the final error thrown on line 37 references the original error.message from the first catch block, but should reference fallbackError.message since that's the error from the fallback attempt that actually failed.
This could lead to confusing error messages where users see the error from the initial cache load attempt rather than the more relevant fallback API error. Change line 37 to use fallbackError.message instead of error.message to provide accurate error information.
| throw new Error(`Failed to load ${filename}: ${error.message}`); | |
| throw new Error(`Failed to load ${filename}: ${fallbackError.message}`); |
There was a problem hiding this comment.
Fixed in commit 49a11e3. Changed line 37 to use fallbackError.message instead of error.message. Error messages now accurately reflect the fallback API failure rather than the initial cache failure.
dashboard/cia-visualizations.js
Outdated
| const { overview } = this.data; | ||
|
|
||
| // Update metric values with null checks | ||
| const totalMpsEl = document.getElementById('metric-total-mps'); | ||
| if (totalMpsEl) { | ||
| totalMpsEl.textContent = overview.keyMetrics.totalMPs; | ||
| } | ||
| const totalPartiesEl = document.getElementById('metric-total-parties'); | ||
| if (totalPartiesEl) { | ||
| totalPartiesEl.textContent = overview.keyMetrics.totalParties; | ||
| } | ||
| const riskRulesEl = document.getElementById('metric-risk-rules'); | ||
| if (riskRulesEl) { | ||
| riskRulesEl.textContent = overview.keyMetrics.totalRiskRules; | ||
| } | ||
| const coalitionSeatsEl = document.getElementById('metric-coalition-seats'); | ||
| if (coalitionSeatsEl) { | ||
| coalitionSeatsEl.textContent = overview.keyMetrics.coalitionSeats; | ||
| } | ||
|
|
||
| // Update risk alerts with null checks | ||
| const alertCriticalEl = document.getElementById('alert-critical'); | ||
| if (alertCriticalEl) { | ||
| alertCriticalEl.textContent = overview.riskAlerts.last90Days.critical; | ||
| } | ||
| const alertMajorEl = document.getElementById('alert-major'); | ||
| if (alertMajorEl) { | ||
| alertMajorEl.textContent = overview.riskAlerts.last90Days.major; | ||
| } | ||
| const alertMinorEl = document.getElementById('alert-minor'); | ||
| if (alertMinorEl) { |
There was a problem hiding this comment.
The renderKeyMetrics method checks if DOM elements exist but doesn't verify that the data properties (overview.keyMetrics and overview.riskAlerts.last90Days) are defined before accessing their nested properties. If the API returns malformed data or these objects are missing, this will cause TypeErrors.
Add defensive checks before accessing nested properties:
- Check if overview.keyMetrics exists before accessing totalMPs, totalParties, etc.
- Check if overview.riskAlerts and overview.riskAlerts.last90Days exist before accessing critical, major, minor
| const { overview } = this.data; | |
| // Update metric values with null checks | |
| const totalMpsEl = document.getElementById('metric-total-mps'); | |
| if (totalMpsEl) { | |
| totalMpsEl.textContent = overview.keyMetrics.totalMPs; | |
| } | |
| const totalPartiesEl = document.getElementById('metric-total-parties'); | |
| if (totalPartiesEl) { | |
| totalPartiesEl.textContent = overview.keyMetrics.totalParties; | |
| } | |
| const riskRulesEl = document.getElementById('metric-risk-rules'); | |
| if (riskRulesEl) { | |
| riskRulesEl.textContent = overview.keyMetrics.totalRiskRules; | |
| } | |
| const coalitionSeatsEl = document.getElementById('metric-coalition-seats'); | |
| if (coalitionSeatsEl) { | |
| coalitionSeatsEl.textContent = overview.keyMetrics.coalitionSeats; | |
| } | |
| // Update risk alerts with null checks | |
| const alertCriticalEl = document.getElementById('alert-critical'); | |
| if (alertCriticalEl) { | |
| alertCriticalEl.textContent = overview.riskAlerts.last90Days.critical; | |
| } | |
| const alertMajorEl = document.getElementById('alert-major'); | |
| if (alertMajorEl) { | |
| alertMajorEl.textContent = overview.riskAlerts.last90Days.major; | |
| } | |
| const alertMinorEl = document.getElementById('alert-minor'); | |
| if (alertMinorEl) { | |
| const { overview } = this.data || {}; | |
| if (!overview) { | |
| return; | |
| } | |
| // Update metric values with null checks | |
| const totalMpsEl = document.getElementById('metric-total-mps'); | |
| if (totalMpsEl && overview.keyMetrics) { | |
| totalMpsEl.textContent = overview.keyMetrics.totalMPs; | |
| } | |
| const totalPartiesEl = document.getElementById('metric-total-parties'); | |
| if (totalPartiesEl && overview.keyMetrics) { | |
| totalPartiesEl.textContent = overview.keyMetrics.totalParties; | |
| } | |
| const riskRulesEl = document.getElementById('metric-risk-rules'); | |
| if (riskRulesEl && overview.keyMetrics) { | |
| riskRulesEl.textContent = overview.keyMetrics.totalRiskRules; | |
| } | |
| const coalitionSeatsEl = document.getElementById('metric-coalition-seats'); | |
| if (coalitionSeatsEl && overview.keyMetrics) { | |
| coalitionSeatsEl.textContent = overview.keyMetrics.coalitionSeats; | |
| } | |
| // Update risk alerts with null checks | |
| const hasRiskAlerts = overview.riskAlerts && overview.riskAlerts.last90Days; | |
| const alertCriticalEl = document.getElementById('alert-critical'); | |
| if (alertCriticalEl && hasRiskAlerts) { | |
| alertCriticalEl.textContent = overview.riskAlerts.last90Days.critical; | |
| } | |
| const alertMajorEl = document.getElementById('alert-major'); | |
| if (alertMajorEl && hasRiskAlerts) { | |
| alertMajorEl.textContent = overview.riskAlerts.last90Days.major; | |
| } | |
| const alertMinorEl = document.getElementById('alert-minor'); | |
| if (alertMinorEl && hasRiskAlerts) { |
There was a problem hiding this comment.
Fixed in commit 49a11e3. Enhanced renderKeyMetrics with checks for overview, overview.keyMetrics, and overview.riskAlerts.last90Days before accessing nested properties. All element updates now verify both element existence and data validity.
|
@copilot apply changes based on the comments in this thread |
…ternal file Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
All 11 review comments have been addressed in commit 49a11e3: Defensive Programming:
CSP Compliance:
Error Handling:
HTML validation: 0 errors | JSON validation: All valid |
| /** | ||
| * Calculate and return summary statistics | ||
| */ | ||
| getSummaryStats() { |
There was a problem hiding this comment.
Missing defensive check for data structure. The method accesses this.data.forecast without first validating that this.data and this.data.forecast exist. If this.data is null/undefined or if forecast property is missing, this will throw a TypeError.
Add a defensive check at the beginning of the method to validate the data structure before attempting to access nested properties.
| getSummaryStats() { | |
| getSummaryStats() { | |
| // Defensive check for forecast and parties structure | |
| if (!this.data || !this.data.forecast || !Array.isArray(this.data.forecast.parties)) { | |
| console.warn('Invalid or missing election forecast data for summary stats'); | |
| return { | |
| totalSeats: 0, | |
| gainers: 0, | |
| losers: 0, | |
| stable: 0, | |
| biggestGain: null, | |
| biggestLoss: null | |
| }; | |
| } |
There was a problem hiding this comment.
Fixed in commit 05386c5. Added comprehensive validation to check this.data, this.data.forecast, and this.data.forecast.parties exist before accessing. Method now returns safe defaults with console warning if data structure is invalid.
dashboard/election-predictions.js
Outdated
| const date = new Date(this.data.electionDate); | ||
| return date.toLocaleDateString('en-US', { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric' | ||
| }); |
There was a problem hiding this comment.
Missing defensive check for data. The method accesses this.data.electionDate without first validating that this.data exists or that electionDate property is present. If this.data is null/undefined or if electionDate is missing/invalid, this will either throw a TypeError or create an Invalid Date.
Add defensive checks to validate that the data exists and that electionDate is a valid date string before attempting to parse it.
| const date = new Date(this.data.electionDate); | |
| return date.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| // Defensive checks for election date data | |
| if (!this.data || !this.data.electionDate) { | |
| console.warn('Invalid or missing election date data'); | |
| return ''; | |
| } | |
| const date = new Date(this.data.electionDate); | |
| // Validate that the constructed date is valid | |
| if (Number.isNaN(date.getTime())) { | |
| console.warn('Election date is not a valid date:', this.data.electionDate); | |
| return ''; | |
| } | |
| try { | |
| return date.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| } catch (error) { | |
| console.warn('Failed to format election date', error); | |
| return ''; | |
| } |
There was a problem hiding this comment.
Fixed in commit 05386c5. Added defensive checks to validate this.data and electionDate exist, verify Date object is valid (NaN check), and wrap toLocaleDateString in try-catch. Returns empty string with console warning if any validation fails.
|
@copilot apply changes based on the comments in this thread |
… methods Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Both review comments have been addressed in commit 05386c5: Data Validation:
HTML validation: 0 errors | JSON validation: All valid |
|
|
||
| const scoreValue = document.createElement('div'); | ||
| scoreValue.className = 'score-value'; | ||
| scoreValue.textContent = mp.influenceScore.toFixed(1); |
There was a problem hiding this comment.
The renderTop10Rankings method accesses nested properties mp.influenceScore without validation. While there are checks for the rankings array, individual MP objects within that array might be missing expected properties. If an MP object lacks influenceScore, the call to toFixed(1) at line 233 will throw a TypeError.
Add validation for each MP object to ensure required properties exist before attempting to access them.
| scoreValue.textContent = mp.influenceScore.toFixed(1); | |
| const influenceScore = (mp && typeof mp.influenceScore === 'number' && Number.isFinite(mp.influenceScore)) | |
| ? mp.influenceScore | |
| : null; | |
| scoreValue.textContent = influenceScore !== null ? influenceScore.toFixed(1) : 'N/A'; |
| committees.committees.forEach(committee => { | ||
| const card = document.createElement('div'); | ||
| card.className = 'committee-card'; | ||
|
|
||
| const name = document.createElement('h3'); | ||
| name.className = 'committee-name'; | ||
| name.textContent = committee.name; | ||
|
|
||
| const stats = document.createElement('div'); | ||
| stats.className = 'committee-stats'; | ||
|
|
||
| // Helper to create stat item | ||
| const createStat = (label, value) => { | ||
| const stat = document.createElement('div'); | ||
| stat.className = 'committee-stat'; | ||
|
|
||
| const statLabel = document.createElement('span'); | ||
| statLabel.className = 'stat-label'; | ||
| statLabel.textContent = label + ':'; | ||
|
|
||
| const statValue = document.createElement('span'); | ||
| statValue.className = 'stat-value'; | ||
| statValue.textContent = value; | ||
|
|
||
| stat.appendChild(statLabel); | ||
| stat.appendChild(statValue); | ||
| return stat; | ||
| }; | ||
|
|
||
| stats.appendChild(createStat('Members', committee.memberCount)); | ||
| stats.appendChild(createStat('Influence', committee.influenceScore.toFixed(1))); | ||
| stats.appendChild(createStat('Meetings/Year', committee.meetingsPerYear)); | ||
| stats.appendChild(createStat('Documents', committee.documentsProcessed)); | ||
|
|
||
| const issues = document.createElement('div'); | ||
| issues.className = 'committee-issues'; | ||
|
|
||
| const issuesHeading = document.createElement('h4'); | ||
| issuesHeading.textContent = 'Key Issues'; | ||
| issues.appendChild(issuesHeading); | ||
|
|
||
| committee.keyIssues.forEach(issue => { | ||
| const tag = document.createElement('span'); | ||
| tag.className = 'issue-tag'; | ||
| tag.textContent = issue; | ||
| issues.appendChild(tag); | ||
| }); |
There was a problem hiding this comment.
The renderCommitteeNetwork method accesses nested properties like committee.influenceScore, committee.memberCount, and committee.keyIssues without validation. At line 369, toFixed(1) is called on committee.influenceScore, and at line 380, forEach is called on committee.keyIssues without checking if these properties exist or are the expected types.
Add validation for each committee object to ensure required properties exist and are the correct types before attempting to access or iterate them.
| console.error('Dashboard initialization error:', error); | ||
| document.getElementById('loading-state').classList.add('hidden'); | ||
| document.getElementById('error-state').classList.remove('hidden'); | ||
| document.getElementById('error-message').textContent = error.message; |
There was a problem hiding this comment.
The error.message property is directly assigned to textContent of the error-message element. While using textContent prevents XSS, if the error object doesn't have a message property (or if message is undefined), this will display "undefined" to the user, which is not user-friendly.
Consider providing a fallback generic error message if error.message is not available, such as: document.getElementById('error-message').textContent = error.message || 'An unknown error occurred while loading dashboard data.'
| document.getElementById('error-message').textContent = error.message; | |
| document.getElementById('error-message').textContent = | |
| (error && error.message) || 'An unknown error occurred while loading dashboard data.'; |
| * Render key factors affecting the election | ||
| */ | ||
| renderKeyFactors() { | ||
| const container = document.getElementById('key-factors'); | ||
|
|
||
| if (!container) { | ||
| return; | ||
| } | ||
|
|
||
| // Defensive check for keyFactors | ||
| if (!this.data || !Array.isArray(this.data.keyFactors)) { | ||
| console.warn('Invalid or missing key factors data'); | ||
| return; | ||
| } | ||
|
|
||
| const { keyFactors } = this.data; | ||
|
|
||
| // Clear existing content safely | ||
| container.textContent = ''; | ||
|
|
||
| const wrapper = document.createElement('div'); | ||
| wrapper.className = 'key-factors'; | ||
|
|
||
| const heading = document.createElement('h3'); | ||
| heading.textContent = 'Key Election Factors'; | ||
| wrapper.appendChild(heading); | ||
|
|
||
| const list = document.createElement('ul'); | ||
|
|
||
| keyFactors.forEach(factor => { | ||
| const listItem = document.createElement('li'); | ||
| // Use textContent to prevent XSS from untrusted factor values | ||
| listItem.textContent = String(factor); | ||
| list.appendChild(listItem); | ||
| }); | ||
|
|
||
| wrapper.appendChild(list); | ||
| container.appendChild(wrapper); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
The renderKeyFactors method is defined in the Election2026Predictions class but is never called in dashboard-init.js. The method references a DOM element with id "key-factors" which doesn't exist in either dashboard/index.html or dashboard/index_sv.html.
This appears to be dead code. Either:
- Add the call to electionRenderer.renderKeyFactors() in dashboard-init.js and add the corresponding HTML element
- Remove the unused renderKeyFactors method if it's not part of the current implementation plan
Note: The election-analysis.json data file includes a keyFactors array, suggesting this feature was intended but not fully implemented.
| * Render key factors affecting the election | |
| */ | |
| renderKeyFactors() { | |
| const container = document.getElementById('key-factors'); | |
| if (!container) { | |
| return; | |
| } | |
| // Defensive check for keyFactors | |
| if (!this.data || !Array.isArray(this.data.keyFactors)) { | |
| console.warn('Invalid or missing key factors data'); | |
| return; | |
| } | |
| const { keyFactors } = this.data; | |
| // Clear existing content safely | |
| container.textContent = ''; | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'key-factors'; | |
| const heading = document.createElement('h3'); | |
| heading.textContent = 'Key Election Factors'; | |
| wrapper.appendChild(heading); | |
| const list = document.createElement('ul'); | |
| keyFactors.forEach(factor => { | |
| const listItem = document.createElement('li'); | |
| // Use textContent to prevent XSS from untrusted factor values | |
| listItem.textContent = String(factor); | |
| list.appendChild(listItem); | |
| }); | |
| wrapper.appendChild(list); | |
| container.appendChild(wrapper); | |
| } | |
| /** |
| getFormattedElectionDate() { | ||
| // Defensive checks for election date data | ||
| if (!this.data || !this.data.electionDate) { | ||
| console.warn('Invalid or missing election date data'); | ||
| return ''; | ||
| } | ||
|
|
||
| const date = new Date(this.data.electionDate); | ||
|
|
||
| // Validate that the constructed date is valid | ||
| if (Number.isNaN(date.getTime())) { | ||
| console.warn('Election date is not a valid date:', this.data.electionDate); | ||
| return ''; | ||
| } | ||
|
|
||
| try { | ||
| return date.toLocaleDateString('en-US', { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric' | ||
| }); | ||
| } catch (error) { | ||
| console.warn('Failed to format election date', error); | ||
| return ''; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Calculate and return summary statistics | ||
| */ | ||
| getSummaryStats() { | ||
| // Defensive check for forecast and parties structure | ||
| if (!this.data || !this.data.forecast || !Array.isArray(this.data.forecast.parties)) { | ||
| console.warn('Invalid or missing election forecast data for summary stats'); | ||
| return { | ||
| totalSeats: 0, | ||
| gainers: 0, | ||
| losers: 0, | ||
| stable: 0, | ||
| biggestGain: null, | ||
| biggestLoss: null | ||
| }; | ||
| } | ||
|
|
||
| const { parties } = this.data.forecast; | ||
|
|
||
| // Handle empty parties array defensively | ||
| if (parties.length === 0) { | ||
| return { | ||
| totalSeats: 0, | ||
| gainers: 0, | ||
| losers: 0, | ||
| stable: 0, | ||
| biggestGain: null, | ||
| biggestLoss: null | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| totalSeats: parties.reduce((sum, p) => sum + p.predictedSeats, 0), | ||
| gainers: parties.filter(p => p.change > 0).length, | ||
| losers: parties.filter(p => p.change < 0).length, | ||
| stable: parties.filter(p => p.change === 0).length, | ||
| biggestGain: parties.reduce((max, p) => p.change > max.change ? p : max, parties[0]), | ||
| biggestLoss: parties.reduce((min, p) => p.change < min.change ? p : min, parties[0]) | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
The getFormattedElectionDate and getSummaryStats methods are defined but never called anywhere in the codebase. These appear to be utility methods that may have been intended for future use or removed features.
Consider either:
- Removing these unused methods to reduce code complexity and maintenance burden
- Adding a comment explaining that these are intended for future features
- Implementing the features that use these methods
This is particularly relevant for getSummaryStats which performs complex calculations that will never be used if the method isn't called.
| // Confidence interval | ||
| const confidenceDiv = document.createElement('div'); | ||
| confidenceDiv.className = 'confidence-interval'; | ||
| confidenceDiv.textContent = `95% CI: ${party.confidenceInterval.min}-${party.confidenceInterval.max} seats`; |
There was a problem hiding this comment.
The renderSeatPredictions method accesses nested properties (party.confidenceInterval.min, party.confidenceInterval.max) without validating that party.confidenceInterval exists. If the CIA data export is malformed and a party object lacks a confidenceInterval property, this will throw a TypeError and crash the rendering.
Add a defensive check before line 86 to validate that party.confidenceInterval exists and has the expected min/max properties. Consider either:
- Checking for existence and providing fallback values
- Logging a warning and skipping that party's confidence interval display
| confidenceDiv.textContent = `95% CI: ${party.confidenceInterval.min}-${party.confidenceInterval.max} seats`; | |
| let ciText = '95% CI: N/A'; | |
| if ( | |
| party.confidenceInterval && | |
| typeof party.confidenceInterval.min === 'number' && | |
| typeof party.confidenceInterval.max === 'number' | |
| ) { | |
| ciText = `95% CI: ${party.confidenceInterval.min}-${party.confidenceInterval.max} seats`; | |
| } else { | |
| console.warn('Missing or invalid confidenceInterval for party', party.name, party); | |
| } | |
| confidenceDiv.textContent = ciText; |
| scenario.composition.forEach(partyId => { | ||
| const badge = document.createElement('span'); | ||
| badge.className = 'party-badge'; | ||
| badge.textContent = partyId; | ||
| composition.appendChild(badge); | ||
| }); |
There was a problem hiding this comment.
The renderCoalitionScenarios method accesses scenario.composition without validation and then calls forEach on it at line 138. If a scenario object is missing the composition property or if composition is not an array, this will throw a TypeError.
Add a defensive check after line 120 to validate that scenario.composition exists and is an array before attempting to iterate over it. Consider skipping scenarios with invalid data structures.
| labels: partyPerf.parties.map(p => p.shortName), | ||
| datasets: [{ | ||
| label: 'Current Seats', | ||
| data: partyPerf.parties.map(p => p.metrics.seats), | ||
| backgroundColor: [ | ||
| 'rgba(224, 32, 32, 0.8)', // S - Red | ||
| 'rgba(221, 171, 0, 0.8)', // SD - Yellow | ||
| 'rgba(82, 126, 196, 0.8)', // M - Blue | ||
| 'rgba(175, 8, 42, 0.8)', // V - Dark Red | ||
| 'rgba(0, 150, 65, 0.8)', // C - Green | ||
| 'rgba(0, 90, 170, 0.8)', // KD - Dark Blue | ||
| 'rgba(83, 160, 60, 0.8)', // MP - Green | ||
| 'rgba(0, 106, 179, 0.8)' // L - Blue | ||
| ], | ||
| borderColor: [ | ||
| 'rgb(224, 32, 32)', | ||
| 'rgb(221, 171, 0)', | ||
| 'rgb(82, 126, 196)', | ||
| 'rgb(175, 8, 42)', | ||
| 'rgb(0, 150, 65)', | ||
| 'rgb(0, 90, 170)', | ||
| 'rgb(83, 160, 60)', | ||
| 'rgb(0, 106, 179)' | ||
| ], | ||
| borderWidth: 2 | ||
| }] | ||
| }, | ||
| options: { | ||
| responsive: true, | ||
| maintainAspectRatio: false, | ||
| plugins: { | ||
| title: { | ||
| display: true, | ||
| text: 'Current Riksdag Seats by Party', | ||
| font: { size: 16, weight: 'bold' } | ||
| }, | ||
| legend: { | ||
| display: false | ||
| } | ||
| }, | ||
| scales: { | ||
| y: { | ||
| beginAtZero: true, | ||
| max: 120, | ||
| title: { | ||
| display: true, | ||
| text: 'Number of Seats' | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| // Party Cohesion Chart | ||
| const cohesionCtx = document.getElementById('party-cohesion-chart'); | ||
| if (cohesionCtx && typeof Chart !== 'undefined') { | ||
| this.charts.cohesion = new Chart(cohesionCtx, { | ||
| type: 'line', | ||
| data: { | ||
| labels: partyPerf.parties.map(p => p.shortName), | ||
| datasets: [{ | ||
| label: 'Voting Cohesion (%)', | ||
| data: partyPerf.parties.map(p => p.voting.cohesionScore), | ||
| borderColor: 'rgb(0, 102, 51)', | ||
| backgroundColor: 'rgba(0, 102, 51, 0.1)', | ||
| tension: 0.4, | ||
| fill: true, | ||
| pointRadius: 5, | ||
| pointHoverRadius: 7 | ||
| }, { | ||
| label: 'Rebellion Rate (%)', | ||
| data: partyPerf.parties.map(p => p.voting.rebellionRate), | ||
| borderColor: 'rgb(220, 53, 69)', | ||
| backgroundColor: 'rgba(220, 53, 69, 0.1)', | ||
| tension: 0.4, | ||
| fill: true, | ||
| pointRadius: 5, | ||
| pointHoverRadius: 7 | ||
| }] |
There was a problem hiding this comment.
The renderPartyPerformance method accesses deeply nested properties (p.metrics.seats, p.voting.cohesionScore, p.voting.rebellionRate) without validation. If the CIA export data is malformed and parties objects lack metrics or voting properties, this will throw TypeErrors during the map operations.
Add validation to check that each party object has the required nested structure (metrics and voting objects with expected properties) before attempting to access them, or implement error handling within the map callbacks.
✅ CIA Intelligence Dashboard - Final Defensive Programming Enhancements
🛡️ Additional Data Validation
Enhanced Election Predictions Methods
✅ Added comprehensive validation in
getFormattedElectionDate()this.dataandthis.data.electionDateexisttoLocaleDateString()in try-catch for graceful error handling✅ Enhanced validation in
getSummaryStats()this.data,this.data.forecast, andthis.data.forecast.partiesexistpartiesis an array before accessing📊 Impact Summary
✅ Validation Results
HTML Validation: ✅ PASSED (0 errors)
dashboard/index.htmldashboard/index_sv.htmlJSON Validation: ✅ ALL VALID
🎯 Addresses Review Comments
getSummaryStats()to checkthis.data.forecaststructuregetFormattedElectionDate()with date validity checksAll 2 review comments addressed ✅
🔐 Security & Robustness Benefits
💡 Code Quality Improvements
📈 Progress Summary
Total Defensive Checks Added Across All Iterations:
CSP Compliance: ✅ Complete - No inline scripts
XSS Prevention: ✅ Complete - All DOM methods used
Code Quality: ✅ Production-ready with comprehensive validation
Original prompt
This section details on the original issue you should resolve
<issue_title>Interactive Dashboard Visualizing CIA Intelligence Exports</issue_title>
<issue_description>## 📋 Issue Type
Feature - Data Visualization & Dashboard
🎯 Objective
IMPORTANT: CIA platform provides all intelligence data and analysis. This issue focuses on creating an interactive dashboard that visualizes CIA's JSON exports with Swedish election 2026 predictions.
Create an interactive intelligence dashboard that renders CIA platform's data exports with real-time visualizations, Swedish election 2026 predictions, and comprehensive analytics - consuming CIA's pre-processed intelligence for all 349 MPs, 8 parties, and 45 risk rules.
📊 Current State
Measured Metrics:
🚀 Desired State
📊 CIA Data Integration Context
CIA Platform Role:
🏭 CIA Provides: Complete intelligence data, OSINT analysis, pre-processed visualizations
📊 CIA Exports: Overview dashboard, party performance, election analysis, Top 10 rankings
🔍 CIA Analyzes: Voting patterns, influence networks, risk scores
Riksdagsmonitor Role:
📊 Visualizes: CIA's JSON exports in interactive dashboard
🎨 Renders: CIA's intelligence in user-friendly format
🌐 Displays: CIA's analytics across 14 languages
CIA Data Products (consumed by dashboard):
Data Source:
data/cia-exports/current/schemas/cia/*.schema.jsonhttps://www.hack23.com/cia/api/CIA Export Files Used:
Methodology:
DATA_ANALYSIS_INTOP_OSINT.md(451.4 KB)Implementation Notes:
🌐 Translation & Content Alignment
Translation Guide(s): All 4 guides for dashboard labels and tooltips
Related Homepage Page(s):
Multi-Language Scope: Dashboard in all 14 languages
🔧 Implementation Approach
Phase 1: Dashboard Foundation (Static Site Compatible)
CIA Data Loader:
Dashboard Layout: