function sanitize(str) { const d = document.createElement("div"); d.textContent = String(str || ""); return d.innerHTML; } const State = { theme: localStorage.getItem("bg-theme") || "dark", language: localStorage.getItem("bg-lang") || "en", // LOGIC-4 FIX: persist language searchResults: [], currentEntity: null, feedSocket: null, apiConnected: false, }; const Views = { home: () => { const main = document.getElementById("app-main"); main.innerHTML = `
Sach Ko Prakash Mein Laana

Public Transparency
Intelligence Platform

Every claim backed by official records. Every relationship traceable to its source. Investigative intelligence for journalists, researchers, and citizens.

21 official data sources | 15 parallel investigators | 22 Indian languages
${[1,2,3,4].map(() => `
`).join("")}

How It Works

Enter any entity. The platform runs 15 specialist investigators in parallel, each analysing from a different angle. Findings are synthesised -- where three or more investigators agree, confidence is marked as high.

${[ ["01", "Search", "Enter any politician, company, ministry, or scheme name in any Indian language."], ["02", "Investigate", "15 specialist AI investigators run in parallel across 21 official data sources."], ["03", "Synthesise", "Findings are cross-validated. Agreed patterns are marked with high confidence."], ["04", "Export", "Download a tamper-evident PDF dossier with a SHA-256 integrity hash. (Not a legal document -- for research use only.)"], ].map(([num, title, desc]) => `
${num}
${title}
${desc}
`).join("")}

Live Feed

Connecting to live feed...
View all intelligence
`; document.getElementById("home-search-btn").addEventListener("click", () => { const q = document.getElementById("home-search-input").value.trim(); const _l=State.language&&State.language!=='en'?'&lang='+State.language:''; if (q) Router.navigate(`/search?q=${encodeURIComponent(q)}${_l}`); }); document.getElementById("home-search-input").addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = e.target.value.trim(); const _l=State.language&&State.language!=='en'?'&lang='+State.language:''; if (q) Router.navigate(`/search?q=${encodeURIComponent(q)}${_l}`); } }); Api.stats().then(data => { const grid = document.getElementById("stats-grid"); if (!grid) return; const nodes = data.nodes || {}; const stats = [ [Object.values(nodes).reduce((a,b)=>a+b,0).toLocaleString("en-IN"), "Total Entities"], [(nodes.Politician || 0).toLocaleString("en-IN"), "Politicians Tracked"], [(nodes.Company || 0).toLocaleString("en-IN"), "Companies Mapped"], [(nodes.Contract || 0).toLocaleString("en-IN"), "Contracts Indexed"], ]; grid.innerHTML = stats .map(([v,l]) => `
${v || "--"}
${l}
`).join(""); }).catch(() => { const grid = document.getElementById("stats-grid"); if (grid) grid.innerHTML = `
Live data unavailable -- backend may be starting up.
`; }); Views._connectFeedToHome(); }, _connectFeedToHome: () => { // FEED-2 FIX: reset retry counter each time _connectFeedToHome is called // so navigating Home -> elsewhere -> Home restarts the 20-retry budget if (typeof connect !== 'undefined') connect._retries = 0; function connect(delay) { delay = delay || 2000; let ws; try { ws = Api.createFeedSocket(); } catch (_) { return; } ws.onmessage = (event) => { try { const data = JSON.parse(event.data); const container = document.getElementById("home-feed"); if (!container) { ws.close(); return; } container.innerHTML = ""; // Show real feed items when available, fall back to heartbeat message const items = data.items || []; if (items.length > 0) { items.slice(0, 3).forEach(item => { container.appendChild(Components.FeedItem({ headline: `[${item.label || "Entity"}] ${item.name || "--"}`, risk_level: "MODERATE", detected_at: item.scraped_at || data.at, source: item.source || "BharatGraph", })); }); } else if (data.message && data.message.includes("pipeline")) { // BUG-12 FIX: "run /admin/pipeline" is a backend message that // was never shown to users -- show a friendly callout instead if (!document.getElementById("feed-empty-callout")) { const callout = document.createElement("div"); callout.id = "feed-empty-callout"; callout.style.cssText = "padding:16px;background:rgba(255,153,51,0.08);border:1px solid rgba(255,153,51,0.25);border-radius:8px;font-size:12px;color:var(--color-saffron);text-align:center;margin:8px"; callout.textContent = "Database is being populated. Intelligence data will appear here after the pipeline completes."; container.appendChild(callout); } } else { container.appendChild(Components.FeedItem({ headline: data.message || "New intelligence available", risk_level: "MODERATE", detected_at: data.at, source: "BharatGraph Feed", })); } } catch (_) {} }; ws.onerror = () => {}; // BUG-6 FIX + M-07 FIX: reconnect with max retry cap ws.onclose = () => { const container = document.getElementById("home-feed"); if (!container) return; // user navigated away -- stop retrying if ((connect._retries || 0) >= 20) { // M-07 FIX: max 20 retries (~10 min at 30s ceiling) -- stop after that const msg = document.createElement("div"); msg.style.cssText = "padding:12px;text-align:center;font-size:12px;color:var(--text-muted)"; msg.textContent = "Live feed unavailable. Refresh the page to reconnect."; container.appendChild(msg); return; } connect._retries = (connect._retries || 0) + 1; const nextDelay = Math.min(delay * 2, 30000); setTimeout(() => connect(nextDelay), nextDelay); }; } connect(2000); }, search: () => { const params = new URLSearchParams(window.location.hash.split("?")[1] || ""); const query = params.get("q") || ""; const type = params.get("type") || ""; const main = document.getElementById("app-main"); main.innerHTML = `
Filter: ${["All","politician","company","audit","contract","ministry","party","scheme","tender","electoralbond","regulatory","enforcement","insolvency","ngo","parliamentquestion","vigilancecircular","icijentity","sanctionedentity","courtcase","localbody","datagov","crimereport"].map(t => ` `).join("")}
${query ? (() => { const w = document.createElement("div"); w.appendChild(Components.SkeletonGroup(5)); return w.innerHTML; })() : ""}
`; // B-06 FIX: wire data-nav-path filter buttons after DOM is built document.querySelectorAll("[data-nav-path]").forEach(function(btn) { btn.addEventListener("click", function() { Router.navigate(btn.dataset.navPath); }); }); // C-01 FIX: pre-fill the search input with the current query // so users can see what they searched and edit it const _searchInput = document.getElementById("search-input"); if (_searchInput && query) _searchInput.value = query; document.getElementById("search-btn").addEventListener("click", () => { const q = document.getElementById("search-input").value.trim(); const _l=State.language&&State.language!=='en'?'&lang='+State.language:''; if (q) Router.navigate(`/search?q=${encodeURIComponent(q)}${_l}`); }); document.getElementById("search-input").addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = e.target.value.trim(); const _l=State.language&&State.language!=='en'?'&lang='+State.language:''; if (q) Router.navigate(`/search?q=${encodeURIComponent(q)}${_l}`); } }); if (!query) { document.getElementById("search-results").innerHTML = `
Enter a name to begin searching
`; return; } // BUG-19 FIX: skeleton was rendered twice -- once in the innerHTML template // above (when query is present) and once here via explicit appendChild. // The innerHTML="" cleared the first one but caused a double-render flash. // Now only render via innerHTML template; skip this explicit block. // (skeletons variable kept for any legacy references below this block) const _skeletonContainer = document.getElementById("search-results"); if (_skeletonContainer && !_skeletonContainer.hasChildNodes()) { const skeletons = document.createElement("div"); skeletons.appendChild(Components.SkeletonGroup(5)); _skeletonContainer.appendChild(skeletons); } const lang = (typeof State !== 'undefined' && State.language) ? State.language : 'en'; const searchPromise = (lang && lang !== 'en') ? Api.multilingualSearch(query, lang) : Api.search(query, type ? type.toLowerCase() : null, 20, lang); searchPromise.then(rawData => { // Normalise shape: multilingual returns {id, type} while main search // returns {entity_id, entity_type}. Unify before rendering. const data = { ...rawData, total: rawData.total || (rawData.results || []).length, query: rawData.query || query, results: (rawData.results || []).map(r => ({ entity_id: r.entity_id || r.id || "", entity_type: r.entity_type || r.type || "Unknown", name: r.name || r.title || r.entity_id || r.id || "", state: r.state || null, party: r.party || null, source: r.source || r.dataset || null, sources: r.sources || [], })), }; const container = document.getElementById("search-results"); if (!container) return; container.innerHTML = ""; if (!data.results || data.results.length === 0) { container.innerHTML = `
No results found for "${sanitize(query)}"
`; return; } const header = document.createElement("div"); header.style.cssText = "margin-bottom:var(--space-4);font-size:var(--font-size-sm);color:var(--text-muted)"; header.textContent = `${data.total} result${data.total !== 1 ? "s" : ""} for "${data.query}"`; container.appendChild(header); const list = document.createElement("div"); list.style.cssText = "display:flex;flex-direction:column;gap:var(--space-3)"; data.results.forEach(entity => { list.appendChild(Components.ResultCard(entity, (e) => { Router.navigate(`/entity/${e.entity_id}`); })); }); container.appendChild(list); }).catch(err => { const container = document.getElementById("search-results"); if (container) container.innerHTML = `
Search failed. The API may be starting up -- please retry in a moment.
`; }); }, entity: ({ id }) => { const main = document.getElementById("app-main"); main.innerHTML = `
${[120,80,400,300].map(h => `
` ).join("")}
`; Promise.all([ Api.profile(id).catch(() => null), Api.risk(id).catch(() => null), Api.graphConnections(id, 2).catch(() => null), ]).then(([profile, risk, graph]) => { const container = document.getElementById("entity-content"); if (!container) return; if (!profile && !risk) { container.innerHTML = `
Entity not found: ${sanitize(id)}
`; return; } const name = profile?.name || risk?.entity_name || id; const type = profile?.entity_type || "Unknown"; const score = risk?.risk_score ?? 0; const level = risk?.risk_level || "UNKNOWN"; container.innerHTML = `
← Back to results
${sanitize(type)}

${sanitize(name)}

${profile?.overview ? Object.entries(profile.overview) .filter(([,v]) => v) .map(([k,v]) => ` ${sanitize(k)}: ${sanitize(String(v))} `).join("") : ""}

Analytical Findings

${risk?.factors?.filter(f => f.score > 0).map(f => `
${f.name.replace(/_/g," ")}
${f.description}
` ).join("") || `

No findings in current dataset.

`}
Structural Risk Indicator

${sanitize(risk?.explanation || "Risk explanation unavailable.")}

`; const barContainer = document.getElementById("risk-bar-container"); if (barContainer) barContainer.appendChild(Components.RiskBar(score, level)); const badgeContainer = document.getElementById("risk-badge-container"); if (badgeContainer) badgeContainer.appendChild(Components.RiskBadge(level)); document.querySelectorAll(".tab-bar__tab").forEach(tab => { tab.addEventListener("click", () => { document.querySelectorAll(".tab-bar__tab").forEach(t => t.classList.remove("active")); tab.classList.add("active"); const tabId = tab.dataset.tab; ["findings","graph","evidence"].forEach(t => { const el = document.getElementById(`tab-content-${t}`); if (el) el.style.display = t === tabId ? "block" : "none"; }); if (tabId === "graph" && graph) { GraphRenderer.init("entity-graph", graph); GraphRenderer.renderLegend("graph-legend"); } else if (GraphRenderer._currentSimulation) { // L-06 FIX: stop simulation when switching away from graph tab GraphRenderer._currentSimulation.stop(); } }); }); }); }, liveFeed: () => { const main = document.getElementById("app-main"); main.innerHTML = `

Live Intelligence Feed

Connecting...

Real-time analytical intelligence derived from CAG audit reports, government procurement data, and official regulatory filings.

Connecting to feed...
`; const feedItems = []; // BUG-3+4+20 FIX: reconnect with exponential backoff, render data.items, // show meaningful onerror message instead of swallowing silently. function connectFeed(delay) { delay = delay || 2000; let ws; try { ws = Api.createFeedSocket(); } catch (_) { const s = document.getElementById("feed-status"); if (s) s.textContent = "WebSocket unavailable"; return; } ws.onopen = () => { const s = document.getElementById("feed-status"); if (s) s.textContent = "Connected"; }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); // BUG-4 FIX: render real Neo4j records from data.items when available const items = data.items || []; const seenIds = new Set(feedItems.map(i => i._id).filter(Boolean)); const newItems = items.filter(i => i.id && !seenIds.has(i.id)); if (newItems.length > 0) { newItems.forEach(item => { feedItems.unshift({ headline: "[" + (item.label || "Entity") + "] " + sanitize(item.name || "-"), risk_level: ({"EnforcementAction":"VERY_HIGH","RegulatoryOrder":"HIGH","AuditReport":"HIGH","ElectoralBond":"MODERATE","Contract":"MODERATE","VigilanceCircular":"MODERATE","PressRelease":"LOW"})[item.label] || "MODERATE", detected_at: item.scraped_at || data.at || new Date().toISOString(), source: sanitize(item.source || "BharatGraph"), _id: item.id, }); }); } else if (data.message && data.message.includes("pipeline")) { // FEED-1 FIX: liveFeed view had no pipeline-message guard. // The admin system message was rendered as fake intelligence. // Show user-friendly empty-state instead. const container2 = document.getElementById("feed-container"); const callout = document.createElement("div"); callout.id = "lf-empty-callout"; callout.style.cssText = "padding:20px;text-align:center;color:var(--color-saffron);font-size:13px;border:1px solid rgba(255,153,51,0.25);border-radius:8px;margin:8px"; callout.textContent = "Intelligence database is being populated. Check back after the daily pipeline run completes."; container2.appendChild(callout); } else { feedItems.unshift({ headline: sanitize(data.message || "Feed update received"), risk_level: "MODERATE", detected_at: data.at || new Date().toISOString(), source: "BharatGraph API", }); } if (feedItems.length > 50) feedItems.pop(); const container = document.getElementById("feed-container"); if (!container) { ws.close(); return; } container.innerHTML = ""; feedItems.forEach(item => container.appendChild(Components.FeedItem(item))); } catch (_) {} }; // BUG-20 FIX: show meaningful error instead of swallowing silently ws.onerror = (err) => { const s = document.getElementById("feed-status"); if (s) s.textContent = "Connection error -- retrying..."; }; // M-07 FIX: reconnect with max 20 retry cap ws.onclose = () => { const container = document.getElementById("feed-container"); if (!container) return; if ((connectFeed._retries || 0) >= 20) { const s = document.getElementById("feed-status"); if (s) s.textContent = "Feed unavailable -- refresh to retry"; return; } connectFeed._retries = (connectFeed._retries || 0) + 1; const s = document.getElementById("feed-status"); if (s) s.textContent = "Reconnecting..."; const nextDelay = Math.min(delay * 2, 30000); setTimeout(() => connectFeed(nextDelay), nextDelay); }; } connectFeed(2000); }, "connection-map": (params) => { const urlParams = new URLSearchParams(window.location.hash.split("?")[1] || ""); const entityId = urlParams.get("entity") || ""; const entityName = urlParams.get("name") || entityId; const main = document.getElementById("app-main"); main.innerHTML = `

Connection Map

Evidence-based relationship map for: ${sanitize(entityName)}

Relationship Graph
★ Strong   ◇ Medium   ○ Weak
Evidence Chain
Path Finder
`; if (!entityId) return; // Load evidence Api.nodeEvidence(entityId).then(data => { const edges = data.edges || []; const listEl = document.getElementById("conn-evidence-list"); if (!listEl) return; listEl.innerHTML = edges.length ? edges.map(e => `
${sanitize(e.rel_label||"")}
${sanitize(e.connected_to||e.connected_id||"--")}
${sanitize(e.why||"")}
📄 ${sanitize(e.source||"")}
`).join("") : `
No connections in current dataset
`; // Render D3 graph Views._renderConnectionGraph(entityId, entityName, edges); }).catch(() => { const el = document.getElementById("conn-graph"); if (el) el.innerHTML = `
Could not load graph data
`; }); }, _findPath: (entityA) => { const b = document.getElementById("path-target").value.trim(); const res = document.getElementById("path-result"); if (!b || !res) return; res.innerHTML = `
`; Api._request(`/connection-map?a=${encodeURIComponent(entityA)}&b=${encodeURIComponent(b)}`) .then(data => { const path = data.path || data.paths || []; res.innerHTML = path.length ? `
${path.length} hop path found
${(Array.isArray(path[0]) ? path[0] : path).map(n => `${sanitize(n)} ->` ).join("").replace(/]*>-><\/span>$/, "")}
` : `
No path found
`; }).catch(() => { res.innerHTML = `
Path search unavailable
`; }); }, _renderConnectionGraph: (centerId, centerName, edges) => { const container = document.getElementById("conn-graph"); if (!container || !window.d3) { if (container) container.innerHTML = `
Graph visualization requires D3.js
`; return; } const w = container.clientWidth || 600, h = container.clientHeight || 440; container.innerHTML = ""; const nodes = [{id: centerId, name: centerName, group: "center"}]; const links = []; const seen = new Set([centerId]); edges.slice(0, 20).forEach(e => { // M-10 FIX: Math.random() creates different IDs each render -- duplicates accumulate. // Use deterministic fallback based on edge properties. const tid = e.connected_id || e.connected_to || ("unknown-" + btoa(JSON.stringify({l:e.rel_label,w:e.why})).slice(0, 8)); if (!seen.has(tid)) { seen.add(tid); nodes.push({id:tid, name:e.connected_to||tid, group:e.rel_label||"OTHER"}); } links.push({source:centerId, target:tid, label:e.rel_label||"", strength:e.strength||"medium"}); }); const svg = d3.select(container).append("svg").attr("width","100%").attr("height","100%") .attr("viewBox",`0 0 ${w} ${h}`); const sim = d3.forceSimulation(nodes) .force("link",d3.forceLink(links).id(d=>d.id).distance(120)) .force("charge",d3.forceManyBody().strength(-300)) .force("center",d3.forceCenter(w/2,h/2)); const link = svg.append("g").selectAll("line").data(links).join("line") .attr("stroke","#555").attr("stroke-width",1.5).attr("stroke-opacity",0.6); const node = svg.append("g").selectAll("circle").data(nodes).join("circle") .attr("r", d => d.group==="center"?16:10) .attr("fill", d => d.group==="center"?"var(--color-saffron)":"var(--accent-primary)") .attr("stroke","var(--bg-primary)").attr("stroke-width",2) .style("cursor","pointer") .on("click", (e,d) => { if(d.id!==centerId) EvidencePanel.open(d.id, d.name); }); const label = svg.append("g").selectAll("text").data(nodes).join("text") .text(d => (d.name||d.id).slice(0,18)) .attr("font-size","10px").attr("fill","var(--text-primary)") .attr("text-anchor","middle").attr("dy","26px"); sim.on("tick", () => { link.attr("x1",d=>d.source.x).attr("y1",d=>d.source.y) .attr("x2",d=>d.target.x).attr("y2",d=>d.target.y); node.attr("cx",d=>d.x).attr("cy",d=>d.y); label.attr("x",d=>d.x).attr("y",d=>d.y); }); }, about: () => { const main = document.getElementById("app-main"); main.innerHTML = `

About BharatGraph

A public transparency platform built on official government data.

${[ ["Legal Notice", "BharatGraph processes only publicly available government records published by the Election Commission of India, Ministry of Corporate Affairs, Government e-Marketplace, Comptroller and Auditor General, Press Information Bureau, and Parliament of India. All outputs are statistical observations derived from official sources. No claim made by this platform constitutes a legal finding or an accusation of wrongdoing."], ["Right to Correction", "Any individual or organisation featured in this platform has the right to submit corrections. Contact us through the GitHub repository issue tracker with documentary evidence. Verified corrections will be incorporated within 14 days."], ["Data Sources", "21 official Indian government data sources plus international datasets from OpenSanctions (sanctions and PEP screening), ICIJ Offshore Leaks (Panama, Pandora, Paradise Papers), and Wikidata (entity enrichment)."], ["Methodology", "The BharatGraph Multi-Investigator Engine runs 15 specialist AI investigators in parallel. Each queries the knowledge graph independently. Findings confirmed by two or more investigators are marked as high-confidence (MODERATE when 2, HIGH when 3+). All language output is validated against a forbidden-words list that prohibits accusatory terminology."], ].map(([title, body]) => `
${title}

${body}

`).join("")}
`; }, notFound: (params) => { // BUG-11 FIX: render a proper 404 page instead of leaving frozen spinner const main = document.getElementById("app-main"); if (!main) return; const badPath = sanitize((params && params.path) || window.location.hash || "/"); main.innerHTML = `
404
Page not found
The path ${badPath} is not registered.
`; }, }; function toggleTheme() { State.theme = State.theme === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", State.theme); localStorage.setItem("bg-theme", State.theme); const btn = document.getElementById("theme-btn"); if (btn) btn.textContent = State.theme === "dark" ? "Light" : "Dark"; } // ?? Language Application ?????????????????????????????????????????????????????? // BUG-10 FIX: full DOM translation via data-i18n attributes // M-05 FIX: cache language labels to avoid re-fetching on every change const _langLabelCache = {}; // M-05 FIX: cache language labels -- applyLanguage() was re-fetching on every change // causing multiple concurrent API calls during rapid language switching // M-05 FIX: cache language labels -- applyLanguage() was re-fetching on every change // causing multiple concurrent API calls during rapid language switching async function applyLanguage(lang) { // LOGIC-4 FIX: save language preference so it survives page refresh if (lang) localStorage.setItem("bg-lang", lang); const badge = document.getElementById("lang-badge"); if (!lang || lang === "en") { document.querySelectorAll("[data-i18n]").forEach(el => { const def = el.getAttribute("data-i18n-default"); if (!def) return; if (el.hasAttribute("placeholder")) el.placeholder = def; else el.textContent = def; }); if (badge) badge.textContent = "EN"; State.uiLabels = {}; return; } try { // Return cached labels if we already fetched this language const data = _langLabelCache[lang] || await Api.uiLabels(lang).then(d => { _langLabelCache[lang] = d; return d; }); const labels = data.labels || {}; // Apply every translated label to every matching data-i18n DOM element. // Auto-scales: new keys in languages.py are applied automatically. document.querySelectorAll("[data-i18n]").forEach(el => { const value = labels[el.getAttribute("data-i18n")]; if (!value) return; if (el.hasAttribute("placeholder")) el.placeholder = value; else el.textContent = value; }); // Also update dynamic search input (rendered after page load) const ph = document.querySelector(".search-bar__input, #search-input"); if (ph && labels.search_placeholder) ph.placeholder = labels.search_placeholder; if (badge) { badge.textContent = lang.toUpperCase(); badge.title = data.native_name || lang; } State.uiLabels = labels; State.langName = data.language_name || lang; State.nativeName = data.native_name || lang; } catch (e) { console.warn("[i18n] Could not load UI labels for", lang, e.message); } } window.applyLanguage = applyLanguage; function initApp() { document.documentElement.setAttribute("data-theme", State.theme); Router.register("/", Views.home); Router.register("/search", Views.search); Router.register("/entity/:id", Views.entity); Router.register("/connection-map", Views["connection-map"]); Router.register("/live-feed", Views.liveFeed); Router.register("/about", Views.about); // BUG-11 FIX: register wildcard so unknown URLs show 404 page // instead of frozen spinner (router.js supports "*" but it was never called) Router.register("*", Views.notFound); // BUG-02 FIX: was single immediate check -- failed permanently on HF cold start. // BUG-25 FIX: message said "Start: uvicorn" -- wrong for HuggingFace users. // Fix: retry up to 5 times with exponential backoff (2s, 4s, 8s, 16s, 30s). (function checkHealth(attempt) { Api.health() .then(data => { State.apiConnected = true; const dot = document.getElementById("api-status-dot"); const text = document.getElementById("api-status-text"); if (dot) dot.style.background = data.neo4j_connected ? "var(--color-risk-low)" : "var(--color-saffron)"; if (text) text.textContent = data.neo4j_connected ? "Connected" : "API Only"; }) .catch(() => { if (attempt < 5) { const delay = Math.min(2000 * Math.pow(2, attempt - 1), 30000); setTimeout(() => checkHealth(attempt + 1), delay); } else { Components.Toast("API unavailable. Please refresh the page in a moment.", "error"); } }); })(1); Router.init(); // Apply language on load if not English if (State.language && State.language !== "en") { applyLanguage(State.language); } } window.addEventListener("DOMContentLoaded", initApp); window.toggleTheme = toggleTheme;