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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions enterprise-dashboard-accessibility-guard/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
frames/
__pycache__/
*.tmp
38 changes: 38 additions & 0 deletions enterprise-dashboard-accessibility-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Enterprise Dashboard Accessibility Guard

Self-contained Enterprise Tooling slice for issue #19.

This module evaluates institutional admin dashboard releases before they are shown to admins, included in scheduled exports, or summarized through webhook notices. It uses synthetic dashboard records only and does not call external accessibility scanners, SSO providers, webhook endpoints, or private institutional systems.

## What It Checks

- Critical metric color contrast and warning-level contrast checks for noncritical content
- Missing, invalid, or unparseable contrast evidence, including unresolved CSS color tokens and noncritical missing contrast evidence
- Missing screen-reader labels
- Keyboard reachability and focus traps
- Missing visible focus indicators for keyboard-reachable dashboard controls
- Malformed dashboard component evidence that would otherwise crash release assessment
- Malformed top-level dashboard packets that would otherwise crash before reviewer evidence is generated
- Malformed reduced-motion evidence that would otherwise crash animated chart assessment
- Private user, project, or direct identifier data embedded in screen-reader labels, table summaries, or export summaries
- Missing table and export summaries
- Heading-order skips
- Missing reduced-motion fallbacks for animated dashboard content

## Commands

```bash
npm run check
npm test
npm run demo
npm run demo:video
```

`npm run demo` writes JSON, Markdown, and SVG reviewer artifacts under `reports/`. `npm run demo:video` renders a short local MP4 walkthrough.

## Safety

- Synthetic sample data only
- No private dashboard data, SSO records, webhook calls, or network access
- No credentials, tokens, payment details, or institutional secrets
- Release decisions are guard outputs, not production enforcement actions
37 changes: 37 additions & 0 deletions enterprise-dashboard-accessibility-guard/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Acceptance Notes

- Adds `enterprise-dashboard-accessibility-guard/` as an independent module.
- Keeps all records synthetic and local.
- Uses dependency-free Node.js logic for deterministic dashboard release decisions.
- Covers blocked, clean, and warning-only dashboard states with tests.
- Treats noncritical low-contrast content as a remediation warning before public release.
- Blocks release when critical dashboard contrast evidence is missing, invalid, or still expressed as unresolved CSS color tokens.
- Treats missing noncritical dashboard contrast evidence as a remediation warning before public release.
- Blocks release when keyboard-reachable dashboard controls suppress visible focus indicators.
- Blocks malformed dashboard component evidence before it can crash release assessment.
- Blocks malformed top-level dashboard packets before they can crash release assessment.
- Blocks malformed reduced-motion evidence before it can crash animated chart assessment.
- Blocks release when private data or direct identifiers appear in screen-reader labels, table summaries, or export accessibility summaries.
- Generates reviewer artifacts:
- `reports/blocked-packet.json`
- `reports/missing-contrast-packet.json`
- `reports/missing-noncritical-contrast-packet.json`
- `reports/malformed-component-packet.json`
- `reports/malformed-dashboard-packet.json`
- `reports/malformed-motion-packet.json`
- `reports/clean-packet.json`
- `reports/warning-packet.json`
- `reports/accessibility-report.md`
- `reports/summary.svg`
- `reports/demo.mp4`

## Local Validation

Run:

```bash
npm run check
npm test
npm run demo
npm run demo:video
```
96 changes: 96 additions & 0 deletions enterprise-dashboard-accessibility-guard/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const fs = require('fs');
const path = require('path');

const { assessDashboardRelease } = require('./index');
const {
blockedDashboard,
cleanDashboard,
warningDashboard,
missingContrastDashboard,
missingNoncriticalContrastDashboard,
malformedComponentDashboard,
malformedDashboardPacket,
malformedMotionDashboard
} = require('./sample-data');

const reportsDir = path.join(__dirname, 'reports');
fs.mkdirSync(reportsDir, { recursive: true });

const packets = [
['blocked-packet.json', assessDashboardRelease(blockedDashboard)],
['missing-contrast-packet.json', assessDashboardRelease(missingContrastDashboard)],
['missing-noncritical-contrast-packet.json', assessDashboardRelease(missingNoncriticalContrastDashboard)],
['malformed-component-packet.json', assessDashboardRelease(malformedComponentDashboard)],
['malformed-dashboard-packet.json', assessDashboardRelease(malformedDashboardPacket)],
['malformed-motion-packet.json', assessDashboardRelease(malformedMotionDashboard)],
['clean-packet.json', assessDashboardRelease(cleanDashboard)],
['warning-packet.json', assessDashboardRelease(warningDashboard)]
];

for (const [fileName, packet] of packets) {
fs.writeFileSync(path.join(reportsDir, fileName), `${JSON.stringify(packet, null, 2)}\n`);
}

fs.writeFileSync(path.join(reportsDir, 'accessibility-report.md'), renderMarkdown(packets));
fs.writeFileSync(path.join(reportsDir, 'summary.svg'), renderSvg(packets));

for (const [fileName, packet] of packets) {
console.log(`${fileName}: ${packet.status}; findings=${packet.findings.length}; digest=${packet.auditDigest.slice(0, 12)}`);
}

function renderMarkdown(packetRows) {
const lines = [
'# Enterprise Dashboard Accessibility Report',
'',
'| Packet | Status | Dashboard | Export | Webhook | Findings |',
'| --- | --- | --- | --- | --- | --- |'
];

for (const [fileName, packet] of packetRows) {
lines.push([
fileName,
packet.status,
packet.releaseLanes.adminDashboard,
packet.releaseLanes.scheduledExport,
packet.releaseLanes.webhookNotice,
packet.findings.map((finding) => finding.code).join(', ') || 'none'
].join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
}

lines.push('');
lines.push('All packets use synthetic dashboard records and deterministic SHA-256 audit digests.');
return `${lines.join('\n')}\n`;
}

function renderSvg(packetRows) {
const height = 108 + packetRows.length * 72 + 36;
const rows = packetRows.map(([, packet], index) => {
const y = 105 + index * 72;
const color = packet.status === 'hold_accessibility_release' ? '#dc2626' : packet.status === 'remediate_before_public_release' ? '#d97706' : '#16a34a';
return `
<g transform="translate(48 ${y})">
<rect width="1104" height="50" rx="6" fill="#f8fafc" stroke="#cbd5e1"/>
<circle cx="28" cy="25" r="11" fill="${color}"/>
<text x="58" y="21" font-size="18" font-family="Arial" fill="#0f172a">${escapeXml(packet.dashboardId)}</text>
<text x="58" y="39" font-size="13" font-family="Arial" fill="#475569">${escapeXml(packet.status)} | findings ${packet.findings.length} | digest ${packet.auditDigest.slice(0, 16)}</text>
</g>`;
}).join('');

return [
`<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="${height}" viewBox="0 0 1200 ${height}">`,
` <rect width="1200" height="${height}" fill="#e2e8f0"/>`,
' <text x="48" y="52" font-size="31" font-family="Arial" font-weight="700" fill="#0f172a">Enterprise Dashboard Accessibility Guard</text>',
' <text x="48" y="80" font-size="16" font-family="Arial" fill="#334155">Institutional dashboards, exports, and webhook notices are gated before release.</text>',
rows,
'</svg>',
''
].join('\n');
}

function escapeXml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
Loading