/* ============================================================ PlayBull Integration β verbindet Trilogy Hub mit dem PlayBull-Backend (echtes Login, Live-Trading, Casino). Additiv: laeuft NACH app.js, ohne dessen Logik zu entfernen. ============================================================ */ (() => { "use strict"; const $ = (s, c = document) => c.querySelector(s); const $$ = (s, c = document) => Array.from(c.querySelectorAll(s)); const PB = { user: null, assets: new Map(), portfolio: null, socket: null, chart: null, parisLive: false, casinoLoaded: false, }; window.PlayBull = PB; /* -------------------- API -------------------- */ async function api(path, body, method = "POST") { const opts = { method, headers: { "Content-Type": "application/json" }, credentials: "same-origin" }; if (body && method !== "GET") opts.body = JSON.stringify(body); const res = await fetch(path, method === "GET" ? { credentials: "same-origin" } : opts); const data = await res.json().catch(() => ({ ok: false, error: "Netzwerkfehler" })); return data; } const get = (p) => api(p, null, "GET"); /* -------------------- Format -------------------- */ const eur = (n, d = 2) => "β¬" + (Number(n) || 0).toLocaleString("de-DE", { minimumFractionDigits: d, maximumFractionDigits: d }); const px = (n) => (Number(n) < 1 ? (Number(n)).toLocaleString("de-DE", { maximumFractionDigits: 4 }) : eur(n)); const signed = (n) => (n >= 0 ? "+" : "-") + "β¬" + Math.abs(Number(n) || 0).toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); /* -------------------- Toast -------------------- */ function toast(msg, type = "") { let wrap = $("#pbToasts"); if (!wrap) { wrap = document.createElement("div"); wrap.id = "pbToasts"; wrap.style.cssText = "position:fixed;top:80px;left:0;right:0;z-index:200;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none"; document.body.appendChild(wrap); } const el = document.createElement("div"); const border = type === "win" ? "var(--success)" : type === "lose" ? "var(--error)" : "var(--border)"; el.style.cssText = `pointer-events:auto;background:var(--surface);border:1px solid ${border};color:var(--text);padding:11px 18px;border-radius:12px;font-weight:600;font-size:.9rem;box-shadow:0 10px 30px rgba(0,0,0,.45);max-width:90%`; el.innerHTML = msg; wrap.appendChild(el); setTimeout(() => { el.style.transition = "opacity .3s"; el.style.opacity = "0"; }, 2600); setTimeout(() => el.remove(), 3000); } /* -------------------- Modal -------------------- */ function openModal(html, maxw = "440px") { closeModal(); const back = document.createElement("div"); back.className = "modal-backdrop"; back.id = "pbModal"; back.innerHTML = `
${isLogin ? "Noch kein Account?" : "Schon registriert?"}
`); $("#pbModal [data-close]").onclick = closeModal; $("#pbAuthSwitch").onclick = () => openAuthModal(isLogin ? "register" : "login"); const submit = async () => { const username = $("#pbUser").value.trim(); const password = $("#pbPass").value; const r = await api(isLogin ? "/api/login" : "/api/register", { username, password }); if (!r.ok) { $("#pbAuthErr").textContent = r.error || "Fehler"; return; } PB.user = r.user; closeModal(); toast(`Willkommen, ${r.user.username}! π`, "win"); onAuthChange(); }; $("#pbAuthSubmit").onclick = submit; $("#pbPass").addEventListener("keydown", (e) => { if (e.key === "Enter") submit(); }); setTimeout(() => $("#pbUser")?.focus(), 50); } PB.openAuthModal = openAuthModal; async function logout() { await api("/api/logout", {}); PB.user = null; PB.parisLive = false; onAuthChange(); toast("Ausgeloggt"); } function onAuthChange() { // Members-only Navigation ein/ausblenden $$("[data-members]").forEach((el) => { el.hidden = !PB.user; }); // aktuelle Seite aktualisieren const route = (location.hash || "#home").replace("#", ""); if (route === "paris") setupParis(); if (route === "casino") setupCasino(); // Falls eingeloggt: Portfolio laden if (PB.user) loadPortfolio(); } /* -------------------- Portfolio / Markt -------------------- */ async function loadMarket() { const r = await get("/api/market"); if (r.ok) r.assets.forEach((a) => PB.assets.set(a.symbol, a)); } async function loadPortfolio() { if (!PB.user) return; const r = await get("/api/portfolio"); if (r.ok) { PB.portfolio = r.portfolio; if (PB.user) PB.user.balance = r.portfolio.balance; } } /* ==================== PARIS β LIVE TRADING ==================== */ function setupParis() { // Demo-Handler von app.js abklemmen: Login-Form + Logout klonen const form = $("#loginForm"); if (form && !form.dataset.pbWired) { const clone = form.cloneNode(true); form.replaceWith(clone); clone.dataset.pbWired = "1"; clone.addEventListener("submit", async (e) => { e.preventDefault(); const u = clone.querySelector("#username").value.trim(); const pw = clone.querySelector("#password").value; const err = clone.querySelector("#loginError"); // 1) Login versuchen, 2) sonst automatisch registrieren let r = await api("/api/login", { username: u, password: pw }); if (!r.ok) { const reg = await api("/api/register", { username: u, password: pw }); if (reg.ok) r = reg; } if (!r.ok) { err.textContent = r.error || "Login fehlgeschlagen"; return; } err.textContent = ""; PB.user = r.user; onAuthChange(); revealDashboard(); }); // Hinweis-Text anpassen const hint = clone.querySelector("p.text-faint"); if (hint) hint.innerHTML = 'Dein PlayBull-Konto β neu? Einfach Name + Passwort eingeben, der Account wird automatisch erstellt.'; } const logoutBtn = $("#logoutBtn"); if (logoutBtn && !logoutBtn.dataset.pbWired) { const lc = logoutBtn.cloneNode(true); logoutBtn.replaceWith(lc); lc.dataset.pbWired = "1"; lc.addEventListener("click", () => { logout(); $("#parisDashboard").classList.add("hidden"); $("#parisLogin").classList.remove("hidden"); }); } if (PB.user) revealDashboard(); } async function revealDashboard() { if (!PB.user) return; $("#parisLogin")?.classList.add("hidden"); $("#parisDashboard")?.classList.remove("hidden"); await loadPortfolio(); injectDashboardExtras(); renderMarketTable(); renderTradeLog(); renderHoldings(); updateKpis(); requestAnimationFrame(() => { try { buildAllocationChart(); } catch {} }); PB.parisLive = true; if (window.lucide) window.lucide.createIcons(); } // Logged-in-as Anzeige + Casino-CTA in die Navbar setzen function injectDashboardExtras() { const chip = $$("#parisDashboard .chip").find((c) => /Logged in as|Eingeloggt/.test(c.textContent)); if (chip) chip.textContent = `Eingeloggt: ${PB.user.username}`; const bar = $("#parisDashboard .wrap.flex"); if (bar && !$("#pbCasinoCta")) { const a = document.createElement("button"); a.id = "pbCasinoCta"; a.className = "btn btn-ghost !min-h-0 !py-2.5"; a.innerHTML = 'Casino & Games '; a.onclick = () => { location.hash = "#casino"; }; const logout = $("#parisDashboard #logoutBtn"); logout?.parentElement?.insertBefore(a, logout); } } function updateKpis() { const p = PB.portfolio; if (!p) return; const pnl = (p.holdings || []).reduce((s, h) => s + h.pnl, 0); // Reihenfolge im KPI-Grid: Portfolio Value, Today's P&L, Open Positions, Buying Power const grid = $("#parisDashboard .grid.grid-cols-2"); if (grid) { const vals = $$("[data-counter]", grid); if (vals[0]) vals[0].textContent = eur(p.netWorth); if (vals[1]) { vals[1].textContent = signed(pnl); vals[1].style.color = pnl >= 0 ? "var(--success)" : "var(--error)"; } if (vals[2]) vals[2].textContent = String((p.holdings || []).length); if (vals[3]) vals[3].textContent = eur(p.balance); // statische "(+0.66%)" Klammer hinter P&L entfernen, falls vorhanden const pnlExtra = vals[1]?.parentElement?.querySelector("span.text-sm"); if (pnlExtra) pnlExtra.textContent = ""; } // Portfolio-Wert oben in der Navbar const navPv = $("#parisDashboard .text-right [data-counter]"); if (navPv) navPv.textContent = eur(p.netWorth); } function renderMarketTable() { const table = $("#watchlistTable"); if (!table) return; const assets = [...PB.assets.values()]; const posBySym = {}; (PB.portfolio?.holdings || []).forEach((h) => (posBySym[h.symbol] = h)); table.innerHTML = `Guthaben: ${eur(PB.user.balance)}
`); let mode = side; const qty = $("#tmQty"); const refresh = () => { const cur = PB.assets.get(sym); $("#tmPx").textContent = px(cur.price); const cost = (parseFloat(qty.value) || 0) * cur.price; $("#tmEst").textContent = `β ${eur(cost)}`; $("#tmGo").textContent = mode === "buy" ? "Jetzt kaufen" : "Jetzt verkaufen"; $("#tmGo").className = "btn w-full " + (mode === "buy" ? "btn-primary" : "btn-ghost"); }; $("#pbModal [data-close]").onclick = closeModal; $$("#pbModal [data-mode]").forEach((b) => b.onclick = () => { mode = b.dataset.mode; $$("#pbModal [data-mode]").forEach((x) => x.className = "btn flex-1 " + (x.dataset.mode === mode ? "btn-primary" : "btn-ghost")); refresh(); }); $$("#pbModal [data-amt]").forEach((c) => c.onclick = () => { qty.value = c.dataset.amt; refresh(); }); $("#pbModal [data-max]").onclick = () => { qty.value = (PB.user.balance / PB.assets.get(sym).price).toFixed(4); refresh(); }; qty.addEventListener("input", refresh); $("#tmGo").onclick = async () => { const r = await api("/api/trade", { symbol: sym, side: mode, qty: parseFloat(qty.value) }); if (!r.ok) return toast(r.error, "lose"); const tradedQty = parseFloat(qty.value); PB.user.balance = r.balance; closeModal(); toast(`${mode === "buy" ? "Gekauft" : "Verkauft"}: ${tradedQty} ${sym} @ ${px(r.price)}`, "win"); await loadPortfolio(); updateKpis(); renderMarketTable(); renderTradeLog(); buildAllocationChart(); }; refresh(); } async function renderTradeLog() { const log = $("#tradeLog"); if (!log) return; const r = await get("/api/trades"); const trades = r.ok ? r.trades : []; log.innerHTML = trades.length ? trades.slice(0, 12).map((t) => { const isBuy = t.side === "buy"; const d = new Date(t.ts).toLocaleString("de-DE", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }); return `Noch keine Positionen.
'; return; } PB.chart = new Chart(ctx, { type: "doughnut", data: { labels, datasets: [{ data, backgroundColor: labels.map((_, i) => chartColors[i % chartColors.length]), borderColor: "transparent", borderWidth: 2, hoverOffset: 8 }] }, options: { cutout: "62%", responsive: true, plugins: { legend: { display: false }, tooltip: { callbacks: { label: (c) => ` ${c.label}: ${eur(c.parsed)}` } } } }, }); const total = data.reduce((a, b) => a + b, 0); $("#chartLegend").innerHTML = labels.map((l, i) => `