import {
t,
setLanguage,
getLanguage,
initI18n,
onLanguageChange,
faqMarkdown,
} from "./js/i18n.js";
import { state, clearUserState } from "./js/state.js";
import { toast } from "./js/dom.js";
import {
handleAuthError,
renderWelcome,
renderPhasePill,
renderCounts,
renderMySuggestions,
renderAllSuggestions,
renderVotes,
syncVoteScores,
renderResults,
renderPhaseTitles,
updatePhaseNav,
configureUiRuntime,
} from "./js/ui.js";
import { api } from "./js/api.js";
import { loadSuggestData, loadVoteData, refreshPhaseData } from "./js/data.js";
import { setupAuthHandlers } from "./js/app-auth-handlers.js";
import { setupAdminHandlers } from "./js/app-admin-handlers.js";
import { setupVoteNavigationHandlers } from "./js/app-vote-nav-handlers.js";
const REFRESH_MIN_MS = 3000;
const REFRESH_MAX_MS = 20000;
const EVENTS_RECONNECT_MIN_MS = 1000;
const EVENTS_RECONNECT_MAX_MS = 15000;
let refreshInFlight = null;
let refreshTimerId = null;
let refreshSchedulerStarted = false;
let unchangedRefreshCycles = 0;
let nextRefreshDelayMs = REFRESH_MIN_MS;
let stateEventSource = null;
let eventsReconnectTimerId = null;
let eventsReconnectDelayMs = EVENTS_RECONNECT_MIN_MS;
async function runSerializedRefresh() {
if (refreshInFlight) return refreshInFlight;
refreshInFlight = refreshPhaseData().finally(() => {
refreshInFlight = null;
});
return refreshInFlight;
}
async function refreshWithUiErrorHandling() {
try {
const changed = await runSerializedRefresh();
updateRefreshCadence(changed === true);
if (state.isAuthenticated) {
ensureStateEventStream();
} else {
closeStateEventStream();
}
} catch (err) {
// Back off after transient failures to avoid hammering server/dependencies.
nextRefreshDelayMs = Math.min(nextRefreshDelayMs * 2, REFRESH_MAX_MS);
if (handleAuthError(err, clearUserState)) {
closeStateEventStream();
return;
}
toast(err.message, true);
}
}
function closeStateEventStream() {
if (eventsReconnectTimerId !== null) {
window.clearTimeout(eventsReconnectTimerId);
eventsReconnectTimerId = null;
}
if (stateEventSource) {
stateEventSource.close();
stateEventSource = null;
}
}
function scheduleStateEventReconnect() {
if (eventsReconnectTimerId !== null || !state.isAuthenticated) return;
eventsReconnectTimerId = window.setTimeout(() => {
eventsReconnectTimerId = null;
ensureStateEventStream();
}, eventsReconnectDelayMs);
eventsReconnectDelayMs = Math.min(
Math.round(eventsReconnectDelayMs * 1.8),
EVENTS_RECONNECT_MAX_MS,
);
}
function ensureStateEventStream() {
if (!state.isAuthenticated || typeof window.EventSource === "undefined") {
closeStateEventStream();
return;
}
if (stateEventSource) return;
stateEventSource = new EventSource(api.stateEventsUrl(), {
withCredentials: true,
});
stateEventSource.onopen = () => {
eventsReconnectDelayMs = EVENTS_RECONNECT_MIN_MS;
};
stateEventSource.onerror = () => {
if (!stateEventSource) return;
stateEventSource.close();
stateEventSource = null;
scheduleStateEventReconnect();
};
stateEventSource.addEventListener("state", () => {
unchangedRefreshCycles = 0;
nextRefreshDelayMs = baseRefreshDelayForPhase();
if (!document.hidden && !state.adminStatusSelectActive) {
refreshWithUiErrorHandling();
}
});
}
function scheduleNextRefresh() {
refreshTimerId = window.setTimeout(async () => {
if (!document.hidden && !state.adminStatusSelectActive) {
await refreshWithUiErrorHandling();
}
scheduleNextRefresh();
}, nextRefreshDelayMs);
}
function startRefreshScheduler() {
if (refreshSchedulerStarted) return;
refreshSchedulerStarted = true;
document.addEventListener("visibilitychange", () => {
if (!document.hidden && !state.adminStatusSelectActive) {
unchangedRefreshCycles = 0;
nextRefreshDelayMs = baseRefreshDelayForPhase();
refreshWithUiErrorHandling();
}
});
if (refreshTimerId !== null) {
window.clearTimeout(refreshTimerId);
}
scheduleNextRefresh();
}
function updateRefreshCadence(changed) {
const base = baseRefreshDelayForPhase();
if (changed) {
unchangedRefreshCycles = 0;
nextRefreshDelayMs = base;
return;
}
unchangedRefreshCycles = Math.min(unchangedRefreshCycles + 1, 8);
const growth = Math.pow(1.35, unchangedRefreshCycles);
nextRefreshDelayMs = Math.min(Math.round(base * growth), REFRESH_MAX_MS);
}
function baseRefreshDelayForPhase() {
switch (state.phase) {
case "Vote":
return REFRESH_MIN_MS;
case "Suggest":
return 5000;
case "Results":
return 7000;
default:
return 5000;
}
}
configureUiRuntime({
refreshPhaseData: runSerializedRefresh,
loadSuggestData,
loadVoteData,
handleAuthError: (err) => handleAuthError(err, clearUserState),
});
function setupHandlers() {
setupAuthHandlers({ runSerializedRefresh });
setupAdminHandlers({ runSerializedRefresh });
setupVoteNavigationHandlers({ runSerializedRefresh });
setupLanguageSwitchers();
document.getElementById("logout")?.addEventListener("click", () => {
closeStateEventStream();
});
onLanguageChange(() => {
updateLanguageButtons();
renderWelcome();
renderPhasePill();
renderCounts();
renderPhaseTitles();
renderMySuggestions();
renderAllSuggestions();
if (state.phase === "Vote") {
renderVotes();
state.votesRendered = true;
syncVoteScores();
}
if (state.phase === "Results") {
renderResults();
}
updatePhaseNav();
});
document.querySelectorAll(".help-chip").forEach((chip) => {
chip.addEventListener("click", () => openFaqModal());
});
}
async function main() {
await initI18n();
setupHandlers();
await refreshWithUiErrorHandling();
startRefreshScheduler();
}
main();
function updateLanguageButtons() {
document.querySelectorAll(".lang-button").forEach((btn) => {
btn.textContent = "🌐";
btn.title = t("lang.label");
btn.setAttribute("aria-label", t("lang.label"));
});
}
function setupLanguageSwitchers() {
const switches = document.querySelectorAll(".lang-switch");
const closeAll = () =>
switches.forEach((wrap) =>
wrap.querySelector(".lang-menu")?.classList.add("hidden"),
);
switches.forEach((wrap) => {
const btn = wrap.querySelector(".lang-button");
const menu = wrap.querySelector(".lang-menu");
if (!btn || !menu) return;
btn.addEventListener("click", (e) => {
e.preventDefault();
const isHidden = menu.classList.contains("hidden");
closeAll();
if (isHidden) menu.classList.remove("hidden");
});
menu.querySelectorAll("[data-lang]").forEach((item) =>
item.addEventListener("click", () => {
const lang = item.dataset.lang;
if (lang) setLanguage(lang);
closeAll();
}),
);
});
document.addEventListener("click", (e) => {
if (!e.target.closest(".lang-switch")) closeAll();
});
updateLanguageButtons();
}
function markdownToHtml(md) {
const lines = md.trim().split(/\r?\n/);
const html = [];
let inList = false;
let inParagraph = false;
const escapeHtml = (text) =>
text.replace(/&/g, "&").replace(//g, ">");
const formatInline = (text) =>
escapeHtml(text)
.replace(/\*\*(.+?)\*\*/g, "$1")
.replace(/`([^`]+)`/g, "$1");
const closeParagraph = () => {
if (inParagraph) {
html.push("
"); inParagraph = true; } html.push(formatInline(trimmed)); }); closeParagraph(); closeList(); return html.join("\n"); } function openFaqModal() { const overlay = document.createElement("div"); overlay.className = "edit-modal"; const panel = document.createElement("div"); panel.className = "edit-panel faq-panel"; panel.innerHTML = `