feat(admin): Admin-Panel-Frontend (/admin)
- content/admin.md (appshell, noindex), renderAdmin in hosting-app.js: Kennzahlen-Kacheln (Kunden/Abos/Instanzen/MRR) + Kundentabelle - Admin-Link im Konto-Header (nur wenn account.is_admin) - Admin-Styles (Stat-Kacheln, Tabelle) in custom.css Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Admin
|
||||
layout: appshell
|
||||
hostingpage: admin
|
||||
toc: false
|
||||
noindex: true
|
||||
---
|
||||
@@ -126,7 +126,9 @@
|
||||
const head =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Mein Konto</div>' +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div>' +
|
||||
'<div style="display:flex;gap:14px;align-items:center">' +
|
||||
(account.is_admin ? '<a class="hosting-link" href="/admin/">Admin</a>' : "") +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div></div>' +
|
||||
'<div class="hosting-sub" style="text-align:left;margin-bottom:20px">' + esc(account.email) + "</div>" +
|
||||
'<div class="hosting-tabs">' +
|
||||
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('<div class="hosting-sub">Lädt…</div>', 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('<div class="hosting-msg err">' + esc(err.message) + "</div>" +
|
||||
'<a class="hosting-link" href="/konto/">Zurück zum Konto</a>');
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = (label, val) =>
|
||||
'<div class="admin-stat"><div class="admin-stat-num">' + esc(val) + "</div>" +
|
||||
'<div class="admin-stat-label">' + label + "</div></div>";
|
||||
|
||||
const rows = accounts.accounts.map((a) =>
|
||||
"<tr>" +
|
||||
"<td><b>" + esc(a.email) + "</b>" + (a.is_admin ? ' <span class="admin-tag">Admin</span>' : "") +
|
||||
(a.company ? '<div class="muted" style="font-size:12px">' + esc(a.company) + "</div>" : "") + "</td>" +
|
||||
"<td>" + (a.plan ? esc(a.plan) + '<div class="muted" style="font-size:12px">' + esc(a.sub_status || "") + "</div>" : '<span class="muted">—</span>') + "</td>" +
|
||||
"<td style=\"text-align:center\">" + esc(a.instance_count) + "</td>" +
|
||||
"<td class=\"muted\" style=\"font-size:12px\">" + esc(new Date(a.created_at).toLocaleDateString("de-CH")) + "</td>" +
|
||||
"</tr>"
|
||||
).join("");
|
||||
|
||||
const html =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Admin</div>' +
|
||||
'<a class="hosting-link" href="/konto/">Mein Konto</a></div>' +
|
||||
'<div class="admin-stats">' +
|
||||
stat("Kunden", stats.accounts) +
|
||||
stat("Aktive Abos", stats.activeSubscriptions) +
|
||||
stat("Instanzen", stats.activeInstances + "/" + stats.instances) +
|
||||
stat("MRR (CHF)", stats.mrrChf) +
|
||||
"</div>" +
|
||||
'<div style="margin:24px 0 10px;font-weight:600;font-size:14px">Kunden</div>' +
|
||||
'<table class="admin-table"><thead><tr><th>Konto</th><th>Abo</th><th>Inst.</th><th>Seit</th></tr></thead>' +
|
||||
"<tbody>" + (rows || '<tr><td colspan="4" class="muted">Noch keine Kunden.</td></tr>') + "</tbody></table>";
|
||||
|
||||
root.innerHTML = card(html, true);
|
||||
}
|
||||
|
||||
({ login: renderLogin, register: renderRegister, konto: renderKonto, preise: renderPreise, admin: renderAdmin }[page] || renderLogin)();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user