(() => { const API_BASE = "/api"; async function api(path, options = {}) { const res = await fetch(`${API_BASE}${path}`, { credentials: "same-origin", headers: { "Content-Type": "application/json", ...(options.headers || {}), }, ...options, }); let data = {}; try { data = await res.json(); } catch (_) {} if (!res.ok) { throw new Error(data.error || `Request failed: ${res.status}`); } return data; } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[ch])); } function replaceText(root, from, to) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes = []; while (walker.nextNode()) { if (walker.currentNode.nodeValue.includes(from)) { nodes.push(walker.currentNode); } } for (const node of nodes) { node.nodeValue = node.nodeValue.replaceAll(from, to); } } function buildLoginForm() { const form = document.createElement("form"); form.className = "top-account-form"; form.action = "#"; form.method = "post"; form.setAttribute("aria-label", "Account login or registration"); let mode = "login"; function render() { form.classList.toggle("is-register", mode === "register"); form.classList.toggle("is-login", mode !== "register"); if (mode === "register") { form.innerHTML = `
`; return; } form.innerHTML = `
`; } function setMessage(text, kind = "error") { const message = form.querySelector(".auth-message"); if (!message) return; message.textContent = text || ""; message.classList.remove("is-error", "is-ok"); message.classList.add(kind === "ok" ? "is-ok" : "is-error"); } form.addEventListener("click", (event) => { const button = event.target.closest("button[data-action]"); if (!button) return; const action = button.dataset.action; if (action === "show-register") { event.preventDefault(); mode = "register"; render(); form.querySelector('input[name="username"]')?.focus(); return; } if (action === "cancel-register") { event.preventDefault(); mode = "login"; render(); form.querySelector('input[name="username"]')?.focus(); } }); form.addEventListener("submit", async (event) => { event.preventDefault(); const submitter = event.submitter; const action = submitter?.dataset.action || "login"; const username = form.querySelector('input[name="username"]')?.value.trim() || ""; const password = form.querySelector('input[name="password"]')?.value || ""; const email = form.querySelector('input[name="email"]')?.value.trim() || ""; setMessage(""); try { if (action === "create-account") { await api("/register", { method: "POST", body: JSON.stringify({ username, email, password }), }); setMessage("Account created. Check your email to verify your account.", "ok"); window.setTimeout(() => { window.location.href = "account-ready.html"; }, 900); return; } await api("/login", { method: "POST", body: JSON.stringify({ username, password }), }); window.location.href = "account-ready.html"; } catch (err) { setMessage(err.message); } }); render(); return form; } function buildSignedInStatus(user) { const wrap = document.createElement("div"); wrap.className = "top-account-status"; wrap.innerHTML = ` Signed in as ${escapeHtml(user.username)} `; wrap.querySelector("[data-logout]").addEventListener("click", async () => { await api("/logout", { method: "POST" }); window.location.href = "index.html"; }); return wrap; } function renderHeader(user) { const header = document.querySelector(".site-header--accountline"); if (!header) return; header.querySelectorAll(".top-account-form, .top-account-status").forEach((el) => el.remove()); if (user) { header.appendChild(buildSignedInStatus(user)); } else { header.appendChild(buildLoginForm()); } } function findCardByHeading(text) { const needle = text.toLowerCase(); for (const heading of document.querySelectorAll("h1, h2, h3, h4")) { if (!heading.textContent.toLowerCase().includes(needle)) continue; const card = heading.closest(".setup-card, .card, section, article, div"); if (card) return card; } return null; } function findBBCard() { return document.querySelector("[data-bb-card], .setup-card--bb") || findCardByHeading("Blue Burst"); } function badge(text, kind = "") { const cls = kind ? `badge ${kind}` : "badge"; return `${escapeHtml(text)}`; } function updateAccountStatusBadges(accountData) { if (!accountData?.bb) return; const bb = accountData.bb; let label = "BB PASSWORD NEEDED"; let warn = true; if (bb.created && bb.ready) { label = "ACCOUNT READY"; warn = false; } else if (bb.created || bb.account_id) { label = "BB SYNC PENDING"; warn = true; } for (const badge of document.querySelectorAll(".status-badges .badge")) { const text = badge.textContent.trim().toUpperCase(); if (text.includes("BB ")) { badge.textContent = label; badge.classList.toggle("badge--warn", warn); } } } function renderBBCard(accountData) { const card = findBBCard(); if (!card || !accountData?.user) return; const user = accountData.user; const bb = accountData.bb || { created: false, ready: false, sync_status: "missing", username: user.username, account_id: null, }; const bbCreated = !!(bb.created || bb.account_id); const bbReady = !!bb.ready; const bbBadgeLabel = bbReady ? "ACCOUNT READY" : "BB SYNC PENDING"; if (bbCreated) { card.innerHTML = `

Blue Burst

Blue Burst Account

${badge(bbBadgeLabel, bbReady ? "" : "badge--warn")}
BB username ${escapeHtml(bb.username)}
BB account ID ${escapeHtml(bb.account_id)}

Change your Blue Burst login password. This updates the account file, then it needs to sync to the ships.

`; } else { card.innerHTML = `

Blue Burst

Create Blue Burst Account

${badge("BB PASSWORD NEEDED", "badge--warn")}
BB username ${escapeHtml(user.username)}
BB account ID created after setup

Your Blue Burst username will match your website account name.

`; } const form = card.querySelector(".bb-account-form"); if (!form) return; form.addEventListener("submit", async (event) => { event.preventDefault(); const message = form.querySelector(".bb-message"); const password = form.querySelector('input[name="password"]').value; const confirmPassword = form.querySelector('input[name="confirm_password"]').value; const action = form.dataset.bbAction; message.textContent = ""; message.classList.remove("is-error", "is-ok"); const endpoint = action === "change-password" ? "/bb/change-password" : "/bb/create"; try { const result = await api(endpoint, { method: "POST", body: JSON.stringify({ password, confirm_password: confirmPassword, }), }); message.textContent = result?.bb?.ready ? "Saved and synced." : "Saved. Sync queued; refresh shortly."; message.classList.add("is-ok"); const fresh = await api("/account"); renderBBCard(fresh); } catch (err) { message.textContent = err.message; message.classList.add("is-error"); } }); } function renderAccountEmail(accountData) { const hero = document.querySelector(".account-hero-card"); const title = document.querySelector("#account-title"); if (!hero || !title) return; for (const p of Array.from(hero.querySelectorAll("p"))) { if ( p.textContent.includes("Manage your Blue Burst login") || p.classList.contains("account-email-line") ) { p.remove(); } } let box = hero.querySelector("#account-email-summary"); if (!box) { box = document.createElement("div"); box.id = "account-email-summary"; box.className = "account-email-summary"; title.insertAdjacentElement("afterend", box); } const email = accountData?.email || {}; const emailText = email.email || "No email address set"; const verified = !!email.verified; box.innerHTML = `
Email ${escapeHtml(emailText)} ${verified ? "Verified" : "Verification needed"}

`; const emailButton = box.querySelector(".account-email-update"); const passwordButton = box.querySelector(".account-password-update"); const emailForm = box.querySelector(".account-email-form"); const passwordForm = box.querySelector(".account-password-form"); const emailCancel = box.querySelector("[data-email-cancel]"); const passwordCancel = box.querySelector("[data-password-cancel]"); const message = box.querySelector(".account-control-message"); const emailInput = box.querySelector("#account-email-input"); function setMessage(text, kind = "") { message.textContent = text || ""; message.className = "account-control-message"; if (kind) message.classList.add(kind); } function hideForms() { emailForm.hidden = true; passwordForm.hidden = true; } emailButton.addEventListener("click", () => { hideForms(); emailForm.hidden = false; emailInput.value = email.email || ""; setMessage(""); emailInput.focus(); }); passwordButton.addEventListener("click", () => { hideForms(); passwordForm.hidden = false; setMessage(""); passwordForm.querySelector('input[name="current_password"]')?.focus(); }); emailCancel.addEventListener("click", () => { emailForm.hidden = true; setMessage(""); }); passwordCancel.addEventListener("click", () => { passwordForm.hidden = true; passwordForm.reset(); setMessage(""); }); emailForm.addEventListener("submit", async (event) => { event.preventDefault(); const newEmail = emailInput.value.trim(); setMessage(""); try { const result = await api("/email/start", { method: "POST", body: JSON.stringify({ email: newEmail }), }); const sent = result?.verification?.sent; const debugLink = result?.verification?.debug_link; setMessage(sent ? "Verification email sent. Check your inbox." : "Verification link created.", "is-ok"); if (debugLink) { const a = document.createElement("a"); a.href = debugLink; a.target = "_blank"; a.rel = "noopener noreferrer"; a.textContent = " Open verification link"; message.appendChild(a); } const updated = await api("/account"); renderAccountEmail(updated); } catch (err) { setMessage(err.message || String(err), "is-error"); } }); passwordForm.addEventListener("submit", async (event) => { event.preventDefault(); const currentPassword = passwordForm.querySelector('input[name="current_password"]').value; const newPassword = passwordForm.querySelector('input[name="password"]').value; const confirmPassword = passwordForm.querySelector('input[name="confirm_password"]').value; setMessage(""); try { await api("/password/change", { method: "POST", body: JSON.stringify({ current_password: currentPassword, password: newPassword, confirm_password: confirmPassword, }), }); passwordForm.reset(); passwordForm.hidden = true; setMessage("Password changed.", "is-ok"); } catch (err) { setMessage(err.message || String(err), "is-error"); } }); } function renderAccountPage(user, accountData) { const main = document.querySelector(".account-layout"); if (!main) return; if (!user) { main.innerHTML = `

Account Dashboard

Sign in or register

Use the account form at the top right to create or open your PSO Peeps website account.

Not signed in
`; return; } replaceText(document.body, "chuudoku", user.username); const title = document.querySelector("#account-title"); if (title) title.textContent = user.username; renderAccountEmail(accountData); updateAccountStatusBadges(accountData); // Account dashboard BB card is server-rendered. // Do not let the generic app bootstrap rewrite it into a stale layout. return; } async function boot() { let user = null; let accountData = null; try { const me = await api("/me"); if (me.authenticated) { user = me.user; } } catch (_) {} if (user) { try { accountData = await api("/account"); } catch (_) { accountData = { authenticated: true, user, bb: { ready: false, username: user.username, account_id: null, }, v2_v3_keys: [], }; } } renderHeader(user); renderAccountPage(user, accountData); } document.addEventListener("DOMContentLoaded", boot); })();