/* RAPPORT Hosting — Frontend-Logik (Vanilla JS). * * Reine Client-Seite: spricht ausschließlich mit dem proprietären Backend * unter /api (RAPPORT-HOST). Hier liegt KEINE Geschäftslogik, nur fetch + * Rendering — so bleibt RAPPORT-WEBSITE sauber AGPL/öffentlich. * * Token im localStorage; gerendert wird in #hosting-root je nach data-page. */ (function () { "use strict"; const root = document.getElementById("hosting-root"); if (!root) return; const page = root.dataset.page || "login"; const TOKEN_KEY = "rapport_host_token"; const tok = { get: () => localStorage.getItem(TOKEN_KEY), set: (t) => localStorage.setItem(TOKEN_KEY, t), clear: () => localStorage.removeItem(TOKEN_KEY), get isLoggedIn() { return !!localStorage.getItem(TOKEN_KEY); }, }; async function api(method, path, body) { const headers = { "Content-Type": "application/json" }; const t = tok.get(); if (t) headers.Authorization = "Bearer " + t; const res = await fetch("/api" + path, { method, headers, body: body ? JSON.stringify(body) : undefined, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || ("Fehler " + res.status)); return data; } const go = (p) => { window.location.href = p; }; const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); function card(inner, wide) { return '
' + inner + "
"; } function msg(el, text, kind) { el.innerHTML = text ? '
' + esc(text) + "
" : ""; } // ── Login ────────────────────────────────────────────────────────────── function renderLogin() { root.innerHTML = card( '
Anmelden
' + '
Zu Ihrer Rapport-Instanz
' + '
' + '
' + '' + '' + '' + '' + '' + "
" + '
Noch kein Konto? ' + '
' ); const m = root.querySelector("#m"); root.querySelector("#toReg").onclick = () => go("/register/"); root.querySelector("#f").onsubmit = async (e) => { e.preventDefault(); const sub = root.querySelector("#sub"); sub.disabled = true; msg(m, "", ""); try { const { token } = await api("POST", "/auth/login", { email: root.querySelector("#email").value.trim(), password: root.querySelector("#pw").value, }); tok.set(token); go("/konto/"); } catch (err) { msg(m, err.message, "err"); sub.disabled = false; } }; } // ── Registrierung ────────────────────────────────────────────────────── function renderRegister() { root.innerHTML = card( '
Konto erstellen
' + '
In Minuten zur eigenen Instanz
' + '
' + '
' + '' + '' + '' + '' + '' + "
" + '
Schon ein Konto? ' + '
' ); const m = root.querySelector("#m"); root.querySelector("#toLogin").onclick = () => go("/login/"); root.querySelector("#f").onsubmit = async (e) => { e.preventDefault(); const sub = root.querySelector("#sub"); sub.disabled = true; msg(m, "", ""); try { const { token } = await api("POST", "/auth/register", { email: root.querySelector("#email").value.trim(), password: root.querySelector("#pw").value, }); tok.set(token); go("/konto/"); } catch (err) { msg(m, err.message, "err"); sub.disabled = false; } }; } // ── Konto / Dashboard ────────────────────────────────────────────────── async function renderKonto() { if (!tok.isLoggedIn) return go("/login/"); root.innerHTML = card('
Lädt…
', true); let data; try { data = await api("GET", "/account/me"); } catch (err) { if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); } root.innerHTML = card('
' + esc(err.message) + "
"); return; } const { account, subscription, instance, plans } = data; const params = new URLSearchParams(location.search); const justOk = params.get("provisioned") === "1"; let html = '
' + '
Konto
' + '
' + '
' + esc(account.email) + "
"; if (justOk) html += '
Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.
'; if (subscription) { html += '
Abo' + esc(subscription.plan) + " · " + esc(subscription.status) + "
"; } if (instance) { html += '
Instanz' + esc(instance.status) + "
" + 'Rapport öffnen →'; } else { html += '
Wählen Sie ein Abo, um Ihre Instanz freizuschalten:
' + '
' + (plans || []).map(planCard).join("") + "
"; } root.innerHTML = card(html, true); root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); }; root.querySelectorAll("[data-plan]").forEach((b) => { b.onclick = async () => { b.disabled = true; try { const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan }); go(url); } catch (err) { alert(err.message); b.disabled = false; } }; }); } function planCard(p) { return '
' + (p.recommended ? '
Empfohlen
' : "") + '
' + esc(p.name) + "
" + '
CHF ' + esc(p.priceChf) + '/' + esc(p.interval) + "
" + "" + '
'; } // ── Preise (öffentlich, mit CTA in den Flow) ─────────────────────────── async function renderPreise() { root.innerHTML = card('
Lädt…
', true); let plans = []; try { plans = (await api("GET", "/billing/plans")).plans; } catch (_) {} const html = '
Abo wählen
' + '
Monatlich kündbar · Preise in CHF, exkl. MwSt.
' + '
' + plans.map(planCard).join("") + "
"; root.innerHTML = card(html, true); root.querySelectorAll("[data-plan]").forEach((b) => { b.onclick = () => go(tok.isLoggedIn ? "/konto/" : "/register/"); }); } ({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise }[page] || renderLogin)(); })();