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]) => `
`;
// 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 = `
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;