diff --git a/backend/app/templates/index.html b/backend/app/templates/index.html
index 407949f..c78c8ee 100644
--- a/backend/app/templates/index.html
+++ b/backend/app/templates/index.html
@@ -673,34 +673,83 @@ function dashboardApp() {
const tableBody = document.getElementById('domains-table-body');
if (!tableBody) return;
- tableBody.innerHTML = '';
+ tableBody.textContent = '';
domains.forEach(domain => {
const row = document.createElement('tr');
row.className = 'table-row';
-
- row.innerHTML = `
-
- ${domain.domain_name}
- |
- ${domain.total_emails || 0} |
-
-
- ${domain.pass_rate || 0}%
-
- |
- ${domain.failed_count || 0} |
- ${domain.report_count || 0} |
-
-
-
- Details
-
- |
- `;
-
+
+ row.appendChild(this.createDomainNameCell(domain.domain_name));
+ row.appendChild(this.createTextCell(this.formatLargeNumber(domain.total_emails || 0)));
+ row.appendChild(this.createPassRateCell(domain.pass_rate || 0));
+ row.appendChild(this.createTextCell(this.formatLargeNumber(domain.failed_count || 0)));
+ row.appendChild(this.createTextCell(this.formatLargeNumber(domain.report_count || 0)));
+ row.appendChild(this.createDetailsCell(domain));
+
tableBody.appendChild(row);
});
+ },
+
+ createDomainNameCell(domainName) {
+ const cell = document.createElement('td');
+ cell.className = 'table-cell';
+
+ const content = document.createElement('div');
+ content.className = 'font-medium';
+ content.textContent = domainName || 'Unknown';
+ cell.appendChild(content);
+
+ return cell;
+ },
+
+ createPassRateCell(passRate) {
+ const cell = document.createElement('td');
+ cell.className = 'table-cell';
+
+ const badge = document.createElement('span');
+ badge.className = 'inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/20 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300';
+ badge.textContent = `${passRate}%`;
+ cell.appendChild(badge);
+
+ return cell;
+ },
+
+ createDetailsCell(domain) {
+ const cell = document.createElement('td');
+ cell.className = 'table-cell text-right';
+
+ const link = document.createElement('a');
+ link.className = 'btn btn-outline btn-sm';
+ const domainId = String(domain.id ?? domain.domain_name ?? '');
+ link.href = `/domains/${encodeURIComponent(domainId)}`;
+ link.appendChild(this.createDetailsIcon());
+ link.appendChild(document.createTextNode('Details'));
+ cell.appendChild(link);
+
+ return cell;
+ },
+
+ createDetailsIcon() {
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+ svg.setAttribute('width', '14');
+ svg.setAttribute('height', '14');
+ svg.setAttribute('viewBox', '0 0 24 24');
+ svg.setAttribute('fill', 'none');
+ svg.setAttribute('stroke', 'currentColor');
+ svg.setAttribute('stroke-width', '2');
+ svg.setAttribute('stroke-linecap', 'round');
+ svg.setAttribute('stroke-linejoin', 'round');
+ svg.setAttribute('class', 'mr-1');
+
+ const axis = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ axis.setAttribute('d', 'M3 3v18h18');
+ svg.appendChild(axis);
+
+ const trend = document.createElementNS('http://www.w3.org/2000/svg', 'path');
+ trend.setAttribute('d', 'm19 9-5 5-4-4-3 3');
+ svg.appendChild(trend);
+
+ return svg;
}
}
}
diff --git a/backend/app/tests/test_dashboard_template_security.py b/backend/app/tests/test_dashboard_template_security.py
new file mode 100644
index 0000000..595ba6d
--- /dev/null
+++ b/backend/app/tests/test_dashboard_template_security.py
@@ -0,0 +1,20 @@
+from pathlib import Path
+
+
+def test_dashboard_domain_table_uses_safe_dom_rendering():
+ """Domain names and counts come from report data and must not be HTML-rendered."""
+ template = (Path(__file__).resolve().parents[1] / "templates" / "index.html").read_text()
+ populate_start = template.index("populateDomainsTable(domains)")
+ helper_start = template.index("createDomainNameCell(domainName)")
+ populate_body = template[populate_start:helper_start]
+
+ assert "innerHTML" not in populate_body
+ assert ".textContent" in populate_body
+ assert "createDomainNameCell" in populate_body
+ assert "createDetailsCell" in populate_body
+
+
+def test_dashboard_domain_details_links_are_encoded():
+ template = (Path(__file__).resolve().parents[1] / "templates" / "index.html").read_text()
+
+ assert "encodeURIComponent(domainId)" in template