// BUG-05 FIX: sanitize() removed from components.js -- defined once in app.js. // Both files previously defined it; second definition silently overwrote the first. // app.js loads last, so its sanitize() was always the active one, but this was fragile. const Components = { Spinner: (size = "sm") => { const el = document.createElement("div"); el.className = "spinner"; if (size === "lg") { el.style.width = "40px"; el.style.height = "40px"; } return el; }, Toast: (message, type = "info") => { const el = document.createElement("div"); el.className = "toast"; el.style.borderLeftColor = type === "error" ? "var(--color-risk-very-high)" : type === "success" ? "var(--color-green)" : "var(--color-saffron)"; el.textContent = message; const container = document.getElementById("toast-container"); if (container) { container.appendChild(el); setTimeout(() => el.remove(), 4000); } return el; }, RiskBadge: (level) => { const el = document.createElement("span"); el.className = `risk-badge risk-badge--${level}`; el.textContent = level.replace("_", " "); return el; }, RiskBar: (score, level) => { const wrapper = document.createElement("div"); wrapper.className = "risk-score-bar"; wrapper.innerHTML = `
${score}/100 `; return wrapper; }, EntityBadge: (type) => { const el = document.createElement("span"); const t = (type || "").toLowerCase(); el.className = `badge badge--${t}`; el.textContent = type; return el; }, ResultCard: (entity, onClick) => { const el = document.createElement("div"); el.className = "result-card"; el.setAttribute("role", "button"); el.setAttribute("tabindex", "0"); const type = (entity.entity_type || "").toLowerCase(); // BUG-16 FIX: removed duplicate ministry: "M" key const icons = { politician: "P", company: "C", contract: "K", tender: "T", regulatory: "R", enforcement: "E", electoralbond: "B", insolvency: "I", ngo: "N", ministry: "M", party: "W", scheme: "S", auditreport: "A", courtcase: "J", localbody: "L", pressrelease: "PR", parliamentquestion: "Q" }; const icon = icons[type] || "?"; el.innerHTML = `
${icon}
${sanitize(entity.name || entity.entity_id || "")}
${entity.entity_type || ""} ${entity.state ? `${entity.state}` : ""} ${entity.party ? `${entity.party}` : ""} ${entity.risk_score != null ? ` ${entity.risk_score}/100 ` : ""}
`; el.addEventListener("click", () => onClick && onClick(entity)); el.addEventListener("keydown", (e) => { if (e.key === "Enter") onClick && onClick(entity); }); return el; }, FindingItem: (finding) => { const el = document.createElement("div"); el.className = `finding-item finding-item--${finding.severity || "LOW"}`; const evidence = (finding.evidence || []) .slice(0, 3) .map(e => `${sanitize(e).substring(0, 60)}`) .join(""); // BUG-20 FIX: finding.description was put directly in innerHTML -- XSS risk el.innerHTML = `
${sanitize(finding.severity || "LOW")}
${sanitize(finding.description || "")}
${evidence ? `
${evidence}
` : ""} `; return el; }, FeedItem: (item) => { const el = document.createElement("div"); el.className = "feed-item"; const level = item.risk_level || "MODERATE"; el.innerHTML = `
${sanitize(item.headline || item.message || "")}
${item.source ? `${sanitize(item.source)}` : ""} ${item.detected_at ? `${new Date(item.detected_at).toLocaleString("en-IN")}` : ""}
`; return el; }, StatCard: (value, label) => { const el = document.createElement("div"); el.className = "stat-card"; el.innerHTML = `
${value}
${label}
`; return el; }, Skeleton: (height = 60, width = "100%") => { const el = document.createElement("div"); el.className = "skeleton"; el.style.height = `${height}px`; el.style.width = typeof width === "number" ? `${width}px` : width; return el; }, SkeletonGroup: (count = 3) => { const frag = document.createDocumentFragment(); for (let i = 0; i < count; i++) { const el = Components.Skeleton(72, "100%"); el.style.marginBottom = "12px"; frag.appendChild(el); } return frag; }, }; window.Components = Components;