diff --git a/assets/css/custom.css b/assets/css/custom.css index 6166114..d5b8780 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -853,3 +853,15 @@ nav [class*="font-bold"] { } .hosting-tab:hover { color: var(--rapport-text); } .hosting-tab.active { color: var(--rapport-accent); border-bottom-color: var(--rapport-accent); } + +/* Admin-Panel */ +.admin-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; } +@media (max-width: 700px) { .admin-stats { grid-template-columns: repeat(2, 1fr); } } +.admin-stat { background: var(--rapport-surface2); border: 1px solid var(--rapport-border); border-radius: 12px; padding: 18px; text-align: center; } +.dark .admin-stat { background: #1a1714; border-color: #2d2926; } +.admin-stat-num { font-family: 'Playfair Display', serif; font-size: 30px; font-weight: 700; color: var(--rapport-text); line-height: 1; } +.admin-stat-label { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--rapport-text-3); margin-top: 8px; } +.admin-table { width: 100%; border-collapse: collapse; font-size: 14px; } +.admin-table th { text-align: left; font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--rapport-text-3); padding: 10px 12px; border-bottom: 1px solid var(--rapport-border); } +.admin-table td { padding: 12px; border-bottom: 1px solid var(--rapport-border); vertical-align: top; } +.admin-tag { display: inline-block; background: var(--rapport-accent); color: #fff; font-size: 9px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; padding: 2px 7px; border-radius: 5px; vertical-align: middle; } diff --git a/content/admin.md b/content/admin.md new file mode 100644 index 0000000..eebbd81 --- /dev/null +++ b/content/admin.md @@ -0,0 +1,7 @@ +--- +title: Admin +layout: appshell +hostingpage: admin +toc: false +noindex: true +--- diff --git a/static/js/hosting-app.js b/static/js/hosting-app.js index 4494e1b..bae8cfe 100644 --- a/static/js/hosting-app.js +++ b/static/js/hosting-app.js @@ -126,7 +126,9 @@ const head = '
' + '
Mein Konto
' + - '
' + + '
' + + (account.is_admin ? 'Admin' : "") + + '
' + '
' + esc(account.email) + "
" + '
' + tabs.map(([id, label]) => @@ -274,5 +276,54 @@ }); } - ({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise }[page] || renderLogin)(); + // ── Admin / Betreiber-Bereich ──────────────────────────────────────────── + async function renderAdmin() { + if (!tok.isLoggedIn) return go("/login/"); + root.innerHTML = card('
Lädt…
', true); + let stats, accounts; + try { + [stats, accounts] = await Promise.all([ + api("GET", "/admin/stats"), + api("GET", "/admin/accounts"), + ]); + } catch (err) { + if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); } + // 403 = eingeloggt, aber kein Admin + root.innerHTML = card('
' + esc(err.message) + "
" + + 'Zurück zum Konto'); + return; + } + + const stat = (label, val) => + '
' + esc(val) + "
" + + '
' + label + "
"; + + const rows = accounts.accounts.map((a) => + "" + + "" + esc(a.email) + "" + (a.is_admin ? ' Admin' : "") + + (a.company ? '
' + esc(a.company) + "
" : "") + "" + + "" + (a.plan ? esc(a.plan) + '
' + esc(a.sub_status || "") + "
" : '') + "" + + "" + esc(a.instance_count) + "" + + "" + esc(new Date(a.created_at).toLocaleDateString("de-CH")) + "" + + "" + ).join(""); + + const html = + '
' + + '
Admin
' + + 'Mein Konto
' + + '
' + + stat("Kunden", stats.accounts) + + stat("Aktive Abos", stats.activeSubscriptions) + + stat("Instanzen", stats.activeInstances + "/" + stats.instances) + + stat("MRR (CHF)", stats.mrrChf) + + "
" + + '
Kunden
' + + '' + + "" + (rows || '') + "
KontoAboInst.Seit
Noch keine Kunden.
"; + + root.innerHTML = card(html, true); + } + + ({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise, admin: renderAdmin }[page] || renderLogin)(); })();