Files
IT-Nexus/Benutzerverwaltung.html

2745 lines
112 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<title>IT Nexus — Benutzerverwaltung</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #0f172a;
--bg-deep: #0a1120;
--card: #1e293b;
--card-glass: rgba(255, 255, 255, 0.04);
--card-glass-2: rgba(255, 255, 255, 0.06);
--line: rgba(255, 255, 255, 0.08);
--line-strong: rgba(255, 255, 255, 0.14);
--teal: #3fa3a3;
--teal-2: #5dbcbc;
--teal-glow: rgba(63, 163, 163, 0.18);
--teal-soft: rgba(63, 163, 163, 0.10);
--text: #f1f5f9;
--text-2: #cbd5e1;
--text-3: #94a3b8;
--text-4: #64748b;
--green: #34d399;
--green-soft: rgba(52, 211, 153, 0.14);
--red: #ef4444;
--red-soft: rgba(239, 68, 68, 0.14);
--amber: #f59e0b;
--amber-soft: rgba(245, 158, 11, 0.14);
--blue: #60a5fa;
--blue-soft: rgba(96, 165, 250, 0.14);
--purple: #a78bfa;
--purple-soft: rgba(167, 139, 250, 0.14);
--pink: #f472b6;
--pink-soft: rgba(244, 114, 182, 0.14);
--r-sm: 8px;
--r: 10px;
--r-lg: 14px;
--sidebar-w: 232px;
--list-w: 310px;
--topbar-h: 56px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
background-image:
radial-gradient(1200px 700px at 90% -10%, rgba(63,163,163,0.10), transparent 60%),
radial-gradient(900px 600px at -5% 100%, rgba(63,163,163,0.06), transparent 55%);
background-attachment: fixed;
color: var(--text);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 14px;
line-height: 1.5;
letter-spacing: -0.005em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
::selection { background: var(--teal-glow); color: var(--text); }
/* ===== Shell ===== */
.app {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr;
height: 100vh;
}
/* ===== Sidebar ===== */
.sidebar {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
border-right: 1px solid var(--line);
padding: 18px 10px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--line-strong) transparent;
}
.sidebar::-webkit-scrollbar { width: 6px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
.brand {
display: flex; align-items: center; gap: 10px;
padding: 4px 10px 22px;
}
.brand-mark {
width: 30px; height: 30px;
border-radius: 8px;
background: linear-gradient(135deg, var(--teal-2), var(--teal));
display: grid; place-items: center;
color: #fff; font-weight: 700; font-size: 13px;
box-shadow: 0 4px 14px -4px var(--teal-glow);
}
.brand-name { font-weight: 600; font-size: 13.5px; letter-spacing: -0.01em; }
.brand-sub { font-size: 10.5px; color: var(--text-4); letter-spacing: 0.04em; text-transform: uppercase; margin-top: 1px; }
.nav-search {
display: flex; align-items: center; gap: 8px;
padding: 7px 10px;
background: var(--card-glass);
border: 1px solid var(--line);
border-radius: 8px;
margin: 0 4px 18px;
color: var(--text-3);
font-size: 12.5px;
cursor: text;
}
.nav-search .kbd {
margin-left: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 1px 5px;
background: rgba(255,255,255,0.06);
border: 1px solid var(--line);
border-radius: 4px;
color: var(--text-4);
}
.nav-group { margin-bottom: 14px; }
.nav-group-title {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-4);
padding: 4px 12px 6px;
}
.nav-item {
display: flex; align-items: center; gap: 10px;
padding: 6px 11px;
border-radius: 7px;
color: var(--text-3);
text-decoration: none;
font-size: 13px;
font-weight: 500;
cursor: pointer;
margin-bottom: 1px;
transition: background 0.12s, color 0.12s;
position: relative;
}
.nav-item:hover { background: var(--card-glass); color: var(--text); }
.nav-item svg { width: 15px; height: 15px; stroke-width: 1.75; flex-shrink: 0; color: var(--text-4); }
.nav-item:hover svg { color: var(--text-3); }
.nav-item.active {
background: var(--teal-soft);
color: var(--teal-2);
box-shadow: inset 0 0 0 1px rgba(63,163,163,0.18);
}
.nav-item.active svg { color: var(--teal-2); }
.nav-badge {
margin-left: auto;
font-size: 10.5px;
padding: 1px 7px;
background: rgba(255,255,255,0.07);
border-radius: 999px;
color: var(--text-2);
font-weight: 600;
}
.nav-badge.danger { background: var(--red); color: #fff; }
.nav-badge.accent { background: var(--teal); color: #fff; }
/* ===== Main ===== */
.main {
min-width: 0;
display: grid;
grid-template-rows: var(--topbar-h) 1fr;
overflow: hidden;
}
.topbar {
display: flex; align-items: center;
padding: 0 24px;
border-bottom: 1px solid var(--line);
background: rgba(15, 23, 42, 0.55);
backdrop-filter: blur(18px) saturate(160%);
-webkit-backdrop-filter: blur(18px) saturate(160%);
}
.breadcrumb {
display: flex; align-items: center; gap: 8px;
font-size: 13px; color: var(--text-3);
}
.breadcrumb .current { color: var(--text); font-weight: 500; }
.breadcrumb .sep { color: var(--text-4); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 4px; }
.icon-btn {
width: 32px; height: 32px;
display: grid; place-items: center;
border-radius: 8px;
color: var(--text-3);
cursor: pointer;
border: none;
background: transparent;
transition: background 0.15s, color 0.15s;
position: relative;
}
.icon-btn:hover { background: var(--card-glass); color: var(--text); }
.icon-btn svg { width: 16px; height: 16px; stroke-width: 1.75; }
.icon-btn .dot {
position: absolute; top: 7px; right: 7px;
width: 6px; height: 6px;
background: var(--red);
border: 1.5px solid var(--bg);
border-radius: 50%;
}
.user-chip {
display: flex; align-items: center; gap: 10px;
padding: 4px 4px 4px 12px;
margin-left: 8px;
background: var(--card-glass);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
}
.user-chip .uc-name { font-size: 12.5px; font-weight: 600; color: var(--text); }
.user-chip .uc-av {
width: 24px; height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, var(--teal-2), var(--teal));
display: grid; place-items: center;
font-size: 10px; font-weight: 700; color: #fff;
}
/* ===== Page workspace (master-detail) ===== */
.workspace {
display: grid;
grid-template-columns: var(--list-w) 1fr;
min-height: 0;
overflow: hidden;
}
/* ===== User list (left) ===== */
.list-panel {
border-right: 1px solid var(--line);
background: rgba(15, 23, 42, 0.4);
display: flex; flex-direction: column;
min-height: 0;
}
.list-head {
padding: 18px 20px 14px;
border-bottom: 1px solid var(--line);
}
.list-title-row {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 4px;
}
.list-title {
font-size: 17px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.018em;
}
.list-count {
font-size: 12px;
color: var(--text-4);
font-variant-numeric: tabular-nums;
}
.list-sub {
font-size: 12px;
color: var(--text-3);
margin-bottom: 14px;
}
.list-search {
position: relative;
margin-bottom: 10px;
}
.list-search svg {
position: absolute; left: 10px; top: 50%;
transform: translateY(-50%);
width: 14px; height: 14px;
color: var(--text-4);
pointer-events: none;
}
.list-search input {
width: 100%;
padding: 8px 10px 8px 32px;
background: var(--card-glass);
border: 1px solid var(--line);
border-radius: 8px;
color: var(--text);
font-family: inherit;
font-size: 13px;
outline: none;
transition: border-color 0.15s, background 0.15s;
}
.list-search input:focus {
border-color: rgba(63,163,163,0.4);
background: var(--card-glass-2);
box-shadow: 0 0 0 3px var(--teal-soft);
}
.list-search input::placeholder { color: var(--text-4); }
.filter-row {
display: flex; gap: 6px;
flex-wrap: wrap;
}
.filter-pill {
border: 1px solid var(--line);
background: transparent;
color: var(--text-3);
font-size: 11.5px;
font-weight: 500;
padding: 4px 10px;
border-radius: 999px;
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
}
.filter-pill:hover { color: var(--text); border-color: var(--line-strong); }
.filter-pill.on {
background: rgba(63,163,163,0.14);
border-color: rgba(63,163,163,0.35);
color: var(--teal-2);
}
.list-scroll {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--line-strong) transparent;
padding: 6px 8px 24px;
}
.list-scroll::-webkit-scrollbar { width: 6px; }
.list-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
.list-section-label {
font-size: 10px;
font-weight: 600;
color: var(--text-4);
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 14px 12px 6px;
}
.user-row {
display: flex; align-items: center; gap: 11px;
padding: 9px 12px 9px 11px;
border-radius: 9px;
cursor: pointer;
position: relative;
border: 1px solid transparent;
transition: background 0.12s, border-color 0.12s;
}
.user-row:hover { background: var(--card-glass); }
.user-row .ur-av {
width: 34px; height: 34px;
border-radius: 50%;
display: grid; place-items: center;
font-size: 12px; font-weight: 700;
color: #fff;
flex-shrink: 0;
background: var(--av-bg, linear-gradient(135deg, var(--teal-2), var(--teal)));
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
}
.user-row .ur-body { flex: 1; min-width: 0; }
.user-row .ur-name {
font-size: 13px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.005em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.user-row .ur-meta {
display: flex; align-items: center; gap: 6px;
font-size: 11.5px;
color: var(--text-4);
margin-top: 1px;
}
.user-row .ur-meta .dot {
width: 3px; height: 3px; border-radius: 50%;
background: var(--text-4);
flex-shrink: 0;
}
.user-row .ur-meta .email {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
min-width: 0;
}
.user-row .ur-presence {
position: absolute;
bottom: 8px; left: 31px;
width: 9px; height: 9px;
border-radius: 50%;
background: var(--text-4);
border: 2px solid var(--bg);
box-shadow: 0 0 0 1px var(--line);
}
.user-row .ur-presence.online { background: var(--green); }
.user-row .ur-presence.away { background: var(--amber); }
.user-row.active {
background: var(--teal-soft);
border-color: rgba(63,163,163,0.22);
box-shadow: inset 3px 0 0 var(--teal), 0 0 24px -8px var(--teal-glow);
}
.user-row.active .ur-name { color: var(--text); }
/* role tints for avatars */
.av-admin { background: linear-gradient(135deg, #f87171, #dc2626); }
.av-manager { background: linear-gradient(135deg, var(--teal-2), var(--teal)); }
.av-dev { background: linear-gradient(135deg, #c4b5fd, #8b5cf6); }
.av-staff { background: linear-gradient(135deg, #93c5fd, #3b82f6); }
.av-intern { background: linear-gradient(135deg, #94a3b8, #64748b); }
.av-extern { background: linear-gradient(135deg, #fcd34d, #d97706); }
.role-chip {
display: inline-flex; align-items: center; gap: 5px;
font-size: 10.5px;
font-weight: 600;
padding: 1.5px 7px;
border-radius: 999px;
letter-spacing: 0.01em;
flex-shrink: 0;
}
.role-chip.admin { background: var(--red-soft); color: #fca5a5; }
.role-chip.manager { background: var(--teal-soft); color: var(--teal-2); box-shadow: inset 0 0 0 1px rgba(63,163,163,0.25); }
.role-chip.dev { background: var(--purple-soft);color: #c4b5fd; }
.role-chip.staff { background: var(--blue-soft); color: #93c5fd; }
.role-chip.intern { background: rgba(148,163,184,0.14); color: #cbd5e1; }
.role-chip.extern { background: var(--amber-soft); color: #fcd34d; }
/* ===== Detail panel ===== */
.detail-panel {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--line-strong) transparent;
min-width: 0;
}
.detail-panel::-webkit-scrollbar { width: 8px; }
.detail-panel::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 4px; }
.detail-inner {
padding: 28px 36px 56px;
max-width: 1240px;
}
/* Detail header */
.detail-head {
display: flex; align-items: flex-start; gap: 20px;
flex-wrap: wrap;
padding-bottom: 24px;
border-bottom: 1px solid var(--line);
margin-bottom: 0;
}
.dh-avatar {
width: 60px; height: 60px;
border-radius: 50%;
display: grid; place-items: center;
font-size: 20px; font-weight: 700; color: #fff;
flex-shrink: 0;
box-shadow: 0 0 0 1px rgba(255,255,255,0.1), 0 8px 24px -6px var(--teal-glow);
position: relative;
}
.dh-avatar .dh-presence {
position: absolute;
right: 0; bottom: 2px;
width: 14px; height: 14px;
border-radius: 50%;
background: var(--green);
border: 3px solid var(--bg);
}
.dh-body { flex: 1 1 280px; min-width: 0; padding-top: 2px; }
.dh-name {
font-size: 26px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.025em;
line-height: 1.15;
margin: 0 0 6px;
}
.dh-name-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.dh-handle {
font-size: 12.5px;
color: var(--text-4);
font-family: 'JetBrains Mono', monospace;
padding: 2px 8px;
background: var(--card-glass);
border: 1px solid var(--line);
border-radius: 6px;
}
.dh-email {
font-size: 13.5px;
color: var(--text-3);
margin: 0 0 12px;
display: flex; align-items: center; gap: 6px;
}
.dh-email svg { width: 13px; height: 13px; color: var(--text-4); }
.dh-badges { display: flex; gap: 8px; flex-wrap: wrap; }
.dh-badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11.5px;
font-weight: 600;
padding: 4px 10px;
border-radius: 999px;
background: var(--card-glass);
border: 1px solid var(--line);
color: var(--text-2);
}
.dh-badge svg { width: 12px; height: 12px; }
.dh-badge.role-manager { background: var(--teal-soft); color: var(--teal-2); border-color: rgba(63,163,163,0.3); }
.dh-badge.dept { color: var(--text-2); }
.dh-badge.status-active { background: var(--green-soft); color: var(--green); border-color: rgba(52,211,153,0.25); }
.dh-badge.status-active .pulse { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 0 rgba(52,211,153,0.5); animation: pulse 1.8s infinite; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(52,211,153,0.5); }
70% { box-shadow: 0 0 0 6px rgba(52,211,153,0); }
100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); }
}
.dh-actions { display: flex; gap: 8px; padding-top: 4px; }
.btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--line);
background: var(--card-glass);
color: var(--text);
text-decoration: none;
font-family: inherit;
transition: background 0.15s, border-color 0.15s, transform 0.06s;
white-space: nowrap;
}
.btn:hover { background: var(--card-glass-2); border-color: var(--line-strong); }
.btn:active { transform: scale(0.985); }
.btn svg { width: 14px; height: 14px; stroke-width: 1.75; }
.btn.primary {
background: var(--teal);
border-color: var(--teal);
color: #fff;
box-shadow: 0 0 0 1px rgba(63,163,163,0.4), 0 8px 24px -8px var(--teal-glow);
}
.btn.primary:hover { background: var(--teal-2); border-color: var(--teal-2); }
.btn.danger { color: #fca5a5; }
.btn.danger:hover { background: var(--red-soft); border-color: rgba(239,68,68,0.35); color: #fecaca; }
/* ===== Tabs ===== */
.tabs-bar {
display: flex;
gap: 4px;
margin-top: 22px;
margin-bottom: 24px;
border-bottom: 1px solid var(--line);
position: relative;
overflow-x: auto;
scrollbar-width: none;
}
.tabs-bar::-webkit-scrollbar { display: none; }
.tab {
padding: 10px 14px;
border: none;
background: transparent;
color: var(--text-3);
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
display: inline-flex; align-items: center; gap: 8px;
transition: color 0.15s;
letter-spacing: -0.005em;
}
.tab:hover { color: var(--text); }
.tab.active {
color: var(--teal-2);
border-bottom-color: var(--teal);
}
.tab .tab-count {
font-size: 10.5px;
font-weight: 600;
padding: 1px 6px;
border-radius: 999px;
background: var(--card-glass-2);
color: var(--text-3);
}
.tab.active .tab-count {
background: var(--teal-soft);
color: var(--teal-2);
}
/* ===== Tab content ===== */
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: fadein 0.2s ease; }
@keyframes fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
/* Stat row */
.stat-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.stat {
padding: 16px 18px;
background: var(--card-glass);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
position: relative;
overflow: hidden;
}
.stat-head {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
}
.stat-icon {
width: 28px; height: 28px;
border-radius: 7px;
background: var(--s-bg, var(--card-glass-2));
color: var(--s-color, var(--text-2));
display: grid; place-items: center;
}
.stat-icon svg { width: 14px; height: 14px; stroke-width: 1.75; }
.stat-trend {
font-size: 11px;
color: var(--text-3);
font-variant-numeric: tabular-nums;
}
.stat-trend.up { color: var(--green); }
.stat-trend.down { color: var(--red); }
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.025em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.stat-value .u { font-size: 13px; color: var(--text-3); font-weight: 500; margin-left: 4px; }
.stat-label {
font-size: 12px;
color: var(--text-3);
margin-top: 6px;
}
.s-status { --s-bg: var(--green-soft); --s-color: var(--green); }
.s-assets { --s-bg: var(--blue-soft); --s-color: var(--blue); }
.s-lic { --s-bg: var(--purple-soft); --s-color: var(--purple); }
.s-fido { --s-bg: var(--amber-soft); --s-color: var(--amber); }
/* Grid */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin-bottom: 14px;
}
.grid-3 {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 14px;
}
/* Card (glass) */
.card {
background: var(--card-glass);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 22px;
}
.card-head {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 16px;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.005em;
display: flex; align-items: center; gap: 8px;
}
.card-title svg { width: 14px; height: 14px; color: var(--text-3); }
.card-actions { display: flex; gap: 4px; }
.card-link {
font-size: 12px;
color: var(--teal-2);
text-decoration: none;
font-weight: 500;
}
.card-link:hover { color: var(--teal); }
/* Info rows */
.info-list { display: flex; flex-direction: column; }
.info-row {
display: grid;
grid-template-columns: 140px 1fr;
gap: 16px;
padding: 9px 0;
font-size: 13px;
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.info-row:last-child { border-bottom: none; padding-bottom: 0; }
.info-row:first-child { padding-top: 0; }
.info-row .k {
color: var(--text-4);
font-weight: 500;
display: flex; align-items: center; gap: 8px;
}
.info-row .k svg { width: 13px; height: 13px; }
.info-row .v {
color: var(--text);
font-weight: 500;
min-width: 0;
display: flex; align-items: center; gap: 8px;
flex-wrap: wrap;
}
.info-row .v.mono { font-family: 'JetBrains Mono', monospace; font-size: 12.5px; color: var(--text-2); }
.info-row .v .mini-av {
width: 20px; height: 20px;
border-radius: 50%;
display: grid; place-items: center;
font-size: 9px; font-weight: 700;
color: #fff;
flex-shrink: 0;
}
.copy-btn {
width: 22px; height: 22px;
display: inline-grid; place-items: center;
border: 1px solid var(--line);
background: transparent;
border-radius: 5px;
color: var(--text-4);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
}
.info-row:hover .copy-btn { opacity: 1; }
.copy-btn:hover { color: var(--text); }
.copy-btn svg { width: 11px; height: 11px; }
/* Security item */
.sec-list { display: flex; flex-direction: column; gap: 10px; }
.sec-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px;
background: var(--card-glass-2);
border: 1px solid var(--line);
border-radius: 10px;
}
.sec-icon {
width: 32px; height: 32px;
border-radius: 8px;
background: var(--si-bg, var(--card-glass-2));
color: var(--si-color, var(--text-2));
display: grid; place-items: center;
flex-shrink: 0;
}
.sec-icon svg { width: 15px; height: 15px; stroke-width: 1.75; }
.sec-body { flex: 1; min-width: 0; }
.sec-label { font-size: 12.5px; font-weight: 600; color: var(--text); margin-bottom: 1px; }
.sec-sub { font-size: 11.5px; color: var(--text-4); }
.sec-status {
font-size: 11px;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
flex-shrink: 0;
}
.sec-status.ok { background: var(--green-soft); color: var(--green); }
.sec-status.warn { background: var(--amber-soft); color: var(--amber); }
.sec-status.off { background: rgba(148,163,184,0.14); color: var(--text-3); }
.si-ok { --si-bg: var(--green-soft); --si-color: var(--green); }
.si-warn { --si-bg: var(--amber-soft); --si-color: var(--amber); }
.si-key { --si-bg: var(--teal-soft); --si-color: var(--teal-2); }
.si-pwd { --si-bg: var(--blue-soft); --si-color: var(--blue); }
/* Activity */
.activity-list {
display: flex; flex-direction: column;
position: relative;
}
.activity-list::before {
content: '';
position: absolute;
left: 13px; top: 14px; bottom: 14px;
width: 1px;
background: var(--line);
}
.activity {
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 12px;
align-items: flex-start;
padding: 8px 0;
position: relative;
}
.act-dot {
width: 28px; height: 28px;
border-radius: 50%;
background: var(--card-glass-2);
border: 1px solid var(--line);
display: grid; place-items: center;
color: var(--ad-color, var(--text-3));
z-index: 1;
}
.act-dot svg { width: 12px; height: 12px; stroke-width: 2; }
.ad-login { --ad-color: var(--green); }
.ad-pwd { --ad-color: var(--amber); }
.ad-asset { --ad-color: var(--blue); }
.ad-key { --ad-color: var(--teal-2); }
.ad-perm { --ad-color: var(--purple); }
.act-body { padding: 4px 0; }
.act-text { font-size: 13px; color: var(--text-2); line-height: 1.4; }
.act-text b { color: var(--text); font-weight: 600; }
.act-meta { font-size: 11.5px; color: var(--text-4); margin-top: 2px; font-family: 'JetBrains Mono', monospace; }
.act-time { font-size: 11.5px; color: var(--text-4); padding-top: 6px; white-space: nowrap; font-variant-numeric: tabular-nums; }
/* Groups */
.group-list {
display: flex; flex-wrap: wrap; gap: 8px;
}
.group-chip {
display: inline-flex; align-items: center; gap: 7px;
padding: 6px 12px 6px 8px;
background: var(--card-glass-2);
border: 1px solid var(--line);
border-radius: 999px;
font-size: 12px;
color: var(--text-2);
font-weight: 500;
}
.group-chip .g-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--gc-color, var(--text-3));
}
.group-chip.entra { --gc-color: var(--blue); }
.group-chip.ad { --gc-color: var(--purple); }
.group-chip.role { --gc-color: var(--teal-2); }
.group-chip.dist { --gc-color: var(--amber); }
.group-section { margin-bottom: 16px; }
.group-section:last-child { margin-bottom: 0; }
.group-section h4 {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-4);
margin: 0 0 10px;
}
/* Permissions */
.perm-list { display: flex; flex-direction: column; gap: 6px; }
.perm-item {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
border-radius: 8px;
background: var(--card-glass-2);
border: 1px solid transparent;
transition: border-color 0.15s;
}
.perm-item:hover { border-color: var(--line); }
.perm-item .p-check {
width: 16px; height: 16px;
border-radius: 4px;
background: var(--green-soft);
color: var(--green);
display: grid; place-items: center;
flex-shrink: 0;
}
.perm-item .p-check svg { width: 10px; height: 10px; stroke-width: 3; }
.perm-item.off .p-check { background: rgba(148,163,184,0.14); color: var(--text-4); }
.perm-item .p-label { font-size: 12.5px; color: var(--text-2); flex: 1; }
.perm-item.off .p-label { color: var(--text-4); }
.perm-item .p-scope { font-size: 11px; color: var(--text-4); font-family: 'JetBrains Mono', monospace; }
/* Empty hint for non-Übersicht tabs */
.empty-hint {
padding: 60px 20px;
text-align: center;
color: var(--text-3);
border: 1px dashed var(--line);
border-radius: var(--r-lg);
}
.empty-hint svg { width: 28px; height: 28px; color: var(--text-4); margin-bottom: 12px; }
.empty-hint h3 { font-size: 15px; font-weight: 600; color: var(--text-2); margin: 0 0 4px; }
.empty-hint p { font-size: 13px; margin: 0; max-width: 360px; margin-inline: auto; }
/* Asset table compact */
.mini-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.mini-table th {
text-align: left;
padding: 8px 14px;
background: var(--card-glass-2);
color: var(--text-4);
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
border-bottom: 1px solid var(--line);
}
.mini-table td {
padding: 11px 14px;
border-bottom: 1px solid rgba(255,255,255,0.04);
color: var(--text-2);
vertical-align: middle;
}
.mini-table tr:last-child td { border-bottom: none; }
.mini-table tr:hover td { background: var(--card-glass); }
.mini-table .asset-name { font-weight: 600; color: var(--text); }
.mini-table .asset-sub { font-size: 11.5px; color: var(--text-4); margin-top: 1px; }
.mini-table .mono { font-family: 'JetBrains Mono', monospace; font-size: 11.5px; }
.table-card { padding: 0; overflow: hidden; }
/* ===== Assets tab ===== */
.asset-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.device-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.device-card {
background: var(--card-glass);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
position: relative;
transition: border-color 0.15s, transform 0.15s;
cursor: pointer;
}
.device-card:hover { border-color: var(--line-strong); transform: translateY(-1px); }
.device-card .dc-top {
display: flex; align-items: flex-start; gap: 12px;
margin-bottom: 14px;
}
.device-icon {
width: 44px; height: 44px;
border-radius: 10px;
background: var(--card-glass-2);
border: 1px solid var(--line);
color: var(--text-2);
display: grid; place-items: center;
flex-shrink: 0;
}
.device-icon svg { width: 22px; height: 22px; stroke-width: 1.5; }
.device-icon.nb { color: var(--blue); background: var(--blue-soft); border-color: rgba(96,165,250,0.2); }
.device-icon.mon { color: var(--purple); background: var(--purple-soft); border-color: rgba(167,139,250,0.2); }
.device-icon.phn { color: var(--green); background: var(--green-soft); border-color: rgba(52,211,153,0.2); }
.device-icon.key { color: var(--amber); background: var(--amber-soft); border-color: rgba(245,158,11,0.2); }
.device-icon.dock { color: var(--pink); background: var(--pink-soft); border-color: rgba(244,114,182,0.2); }
.dc-meta { flex: 1; min-width: 0; }
.dc-name {
font-size: 14px; font-weight: 600; color: var(--text);
letter-spacing: -0.005em; line-height: 1.2;
margin-bottom: 3px;
}
.dc-model {
font-size: 12px; color: var(--text-3);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dc-status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 8px var(--green);
flex-shrink: 0;
margin-top: 4px;
}
.dc-status-dot.off { background: var(--text-4); box-shadow: none; }
.dc-status-dot.warn { background: var(--amber); box-shadow: 0 0 8px var(--amber); }
.dc-health {
display: flex; flex-direction: column; gap: 8px;
margin-bottom: 12px;
}
.dc-bar {
display: flex; flex-direction: column; gap: 4px;
}
.dc-bar-label {
display: flex; justify-content: space-between;
font-size: 11px; color: var(--text-4);
}
.dc-bar-label b { color: var(--text-2); font-weight: 600; font-variant-numeric: tabular-nums; }
.dc-bar-track {
height: 4px;
background: rgba(255,255,255,0.05);
border-radius: 2px;
overflow: hidden;
}
.dc-bar-fill {
height: 100%;
background: var(--bar-color, var(--teal));
border-radius: 2px;
}
.dc-footer {
display: flex; align-items: center; justify-content: space-between;
padding-top: 12px;
border-top: 1px solid var(--line);
font-size: 11px;
color: var(--text-4);
font-family: 'JetBrains Mono', monospace;
}
.dc-tag {
display: inline-flex; align-items: center; gap: 4px;
font-family: 'Inter', sans-serif;
font-size: 10.5px;
font-weight: 600;
padding: 2px 7px;
border-radius: 999px;
background: var(--card-glass-2);
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* ===== Licenses tab ===== */
.lic-summary {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
margin-bottom: 22px;
padding: 18px 22px;
background: var(--card-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
}
.lic-summary > div { padding: 0 8px; }
.lic-summary > div + div { border-left: 1px solid var(--line); }
.lic-summary .ls-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-4);
margin-bottom: 6px;
}
.lic-summary .ls-value {
font-size: 22px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.025em;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.lic-summary .ls-sub {
font-size: 11.5px;
color: var(--text-3);
margin-top: 3px;
}
.lic-cat-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-4);
margin: 18px 0 10px;
display: flex; align-items: center; gap: 8px;
}
.lic-cat-title:first-child { margin-top: 0; }
.lic-cat-title .ct-count {
font-family: 'JetBrains Mono', monospace;
color: var(--text-4);
font-weight: 500;
letter-spacing: 0;
}
.lic-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
}
.lic-card {
background: var(--card-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: 12px;
padding: 14px 16px;
display: flex; align-items: center; gap: 12px;
transition: border-color 0.15s;
}
.lic-card:hover { border-color: var(--line-strong); }
.lic-logo {
width: 38px; height: 38px;
border-radius: 9px;
display: grid; place-items: center;
font-size: 14px; font-weight: 700; color: #fff;
flex-shrink: 0;
background: var(--lc-bg, linear-gradient(135deg, var(--teal-2), var(--teal)));
}
.lic-body { flex: 1; min-width: 0; }
.lic-name {
font-size: 13px; font-weight: 600; color: var(--text);
letter-spacing: -0.005em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lic-plan {
font-size: 11.5px; color: var(--text-4);
margin-top: 1px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lic-cost {
font-size: 12px; font-weight: 600;
color: var(--text-2);
font-variant-numeric: tabular-nums;
text-align: right;
flex-shrink: 0;
}
.lic-cost .pm { font-size: 10.5px; color: var(--text-4); font-weight: 500; display: block; margin-top: 1px; }
.lc-m365 { --lc-bg: linear-gradient(135deg, #5b9bff, #2563eb); }
.lc-adobe { --lc-bg: linear-gradient(135deg, #ef4444, #b91c1c); }
.lc-github{ --lc-bg: linear-gradient(135deg, #4a4a52, #1f2937); }
.lc-figma { --lc-bg: linear-gradient(135deg, #f472b6, #a855f7); }
.lc-jb { --lc-bg: linear-gradient(135deg, #fb7185, #f59e0b); }
.lc-slack { --lc-bg: linear-gradient(135deg, #a78bfa, #7c3aed); }
.lc-1pwd { --lc-bg: linear-gradient(135deg, #38bdf8, #0284c7); }
.lc-aws { --lc-bg: linear-gradient(135deg, #fcd34d, #f59e0b); color: #1f2937; }
.lc-zoom { --lc-bg: linear-gradient(135deg, #60a5fa, #1d4ed8); }
.lc-tw { --lc-bg: linear-gradient(135deg, #14b8a6, #0d9488); }
.lc-no { --lc-bg: linear-gradient(135deg, #94a3b8, #475569); }
.lc-int { --lc-bg: linear-gradient(135deg, #f472b6, #db2777); }
/* ===== Entra/AD tab ===== */
.entra-grid {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 14px;
margin-bottom: 14px;
}
.identity-card {
background:
radial-gradient(400px 200px at 100% 0%, rgba(63,163,163,0.08), transparent 60%),
var(--card-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
}
.sync-diagram {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 14px;
align-items: center;
padding: 16px;
background: var(--card-glass-2);
border: 1px solid var(--line);
border-radius: 10px;
margin-bottom: 18px;
}
.sync-node {
text-align: center;
}
.sync-node .sn-icon {
width: 44px; height: 44px;
margin: 0 auto 6px;
border-radius: 11px;
display: grid; place-items: center;
background: var(--card-glass-2);
border: 1px solid var(--line);
color: var(--text-2);
}
.sync-node.cloud .sn-icon { color: var(--blue); background: var(--blue-soft); border-color: rgba(96,165,250,0.25); }
.sync-node.ad .sn-icon { color: var(--purple); background: var(--purple-soft); border-color: rgba(167,139,250,0.25); }
.sync-node .sn-icon svg { width: 22px; height: 22px; stroke-width: 1.5; }
.sync-node .sn-label { font-size: 11.5px; color: var(--text-3); font-weight: 600; }
.sync-node .sn-id { font-size: 10.5px; color: var(--text-4); font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
.sync-arrow {
display: flex; flex-direction: column; align-items: center; gap: 4px;
color: var(--teal-2);
}
.sync-arrow .line {
width: 60px; height: 2px;
background: linear-gradient(90deg, transparent, var(--teal), transparent);
position: relative;
overflow: hidden;
}
.sync-arrow .line::after {
content: '';
position: absolute; top: 0; left: -20%;
width: 20%; height: 100%;
background: var(--teal-2);
box-shadow: 0 0 8px var(--teal);
animation: flow 2s linear infinite;
}
@keyframes flow { from { left: -20%; } to { left: 100%; } }
.sync-arrow .lbl { font-size: 9.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
.ca-list { display: flex; flex-direction: column; gap: 8px; }
.ca-policy {
display: flex; align-items: center; gap: 12px;
padding: 11px 13px;
background: var(--card-glass-2);
border: 1px solid var(--line);
border-radius: 9px;
}
.ca-policy .cap-icon {
width: 28px; height: 28px;
border-radius: 7px;
display: grid; place-items: center;
background: var(--green-soft);
color: var(--green);
flex-shrink: 0;
}
.ca-policy .cap-icon svg { width: 13px; height: 13px; stroke-width: 2.5; }
.ca-policy.blocked .cap-icon { background: var(--red-soft); color: var(--red); }
.ca-policy.report .cap-icon { background: var(--amber-soft); color: var(--amber); }
.ca-policy .cap-body { flex: 1; min-width: 0; }
.ca-policy .cap-name { font-size: 12.5px; font-weight: 600; color: var(--text); }
.ca-policy .cap-cond { font-size: 11px; color: var(--text-4); margin-top: 1px; }
.ca-policy .cap-mode {
font-size: 10px; font-weight: 600;
padding: 2px 7px;
border-radius: 999px;
background: var(--green-soft);
color: var(--green);
letter-spacing: 0.04em;
text-transform: uppercase;
flex-shrink: 0;
}
.ca-policy.report .cap-mode { background: var(--amber-soft); color: var(--amber); }
.token-preview {
margin-top: 14px;
padding: 14px 16px;
background: rgba(0,0,0,0.25);
border: 1px solid var(--line);
border-radius: 9px;
font-family: 'JetBrains Mono', monospace;
font-size: 11.5px;
line-height: 1.6;
color: var(--text-2);
overflow-x: auto;
}
.token-preview .tk { color: var(--teal-2); }
.token-preview .tv { color: #fcd34d; }
.token-preview .tn { color: var(--text-4); }
/* ===== FIDO tab ===== */
.fido-hero {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
margin-bottom: 22px;
}
.fido-key-card {
background:
radial-gradient(300px 160px at 100% 0%, rgba(245,158,11,0.08), transparent 60%),
var(--card-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
position: relative;
overflow: hidden;
}
.fido-key-card.primary { box-shadow: 0 0 0 1px rgba(63,163,163,0.18), 0 16px 40px -16px var(--teal-glow); border-color: rgba(63,163,163,0.25); }
.fkc-head {
display: flex; align-items: flex-start; justify-content: space-between;
margin-bottom: 18px;
}
.fkc-visual {
width: 56px; height: 56px;
border-radius: 12px;
background: linear-gradient(140deg, #fcd34d 0%, #f59e0b 50%, #b45309 100%);
display: grid; place-items: center;
box-shadow: 0 12px 28px -10px rgba(245,158,11,0.4), inset 0 1px 0 rgba(255,255,255,0.3);
color: #422006;
position: relative;
}
.fkc-visual::after {
content: '';
position: absolute;
width: 18px; height: 18px;
border-radius: 50%;
background: rgba(255,255,255,0.25);
border: 1.5px solid rgba(255,255,255,0.5);
}
.fkc-visual svg { width: 22px; height: 22px; position: relative; z-index: 1; }
.fkc-status {
font-size: 10.5px; font-weight: 600;
padding: 3px 10px;
border-radius: 999px;
background: var(--green-soft);
color: var(--green);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.fkc-name {
font-size: 17px;
font-weight: 700;
color: var(--text);
letter-spacing: -0.018em;
margin-bottom: 4px;
}
.fkc-sub {
font-size: 12.5px;
color: var(--text-3);
margin-bottom: 16px;
}
.fkc-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.fkc-stat .v { font-size: 14px; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.fkc-stat .l { font-size: 10.5px; color: var(--text-4); margin-top: 2px; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 500; }
.fkc-stat + .fkc-stat { border-left: 1px solid var(--line); padding-left: 14px; }
.fkc-stat:first-child { padding-right: 14px; }
.add-key-card {
background: var(--card-glass);
backdrop-filter: blur(10px);
border: 1.5px dashed var(--line-strong);
border-radius: var(--r-lg);
padding: 22px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 8px;
color: var(--text-3);
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
min-height: 220px;
}
.add-key-card:hover { border-color: var(--teal); color: var(--teal-2); background: var(--teal-soft); }
.add-key-card .akc-plus {
width: 40px; height: 40px;
border-radius: 50%;
border: 1.5px dashed currentColor;
display: grid; place-items: center;
}
.add-key-card .akc-plus svg { width: 18px; height: 18px; stroke-width: 1.75; }
.add-key-card .akc-title { font-size: 13px; font-weight: 600; color: var(--text-2); }
.add-key-card:hover .akc-title { color: var(--teal-2); }
.add-key-card .akc-sub { font-size: 11.5px; }
/* ===== Lifecycle tab ===== */
.lc-stage-bar {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0;
background: var(--card-glass);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 6px;
margin-bottom: 18px;
position: relative;
}
.lc-stage {
padding: 14px 12px;
text-align: left;
border-radius: 9px;
position: relative;
color: var(--text-4);
}
.lc-stage .lcs-num {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--text-4);
margin-bottom: 4px;
}
.lc-stage .lcs-label {
font-size: 13px;
font-weight: 600;
color: var(--text-3);
letter-spacing: -0.005em;
}
.lc-stage .lcs-date {
font-size: 11px;
color: var(--text-4);
margin-top: 3px;
font-variant-numeric: tabular-nums;
}
.lc-stage.done { color: var(--text-2); }
.lc-stage.done .lcs-num { color: var(--green); }
.lc-stage.done .lcs-label { color: var(--text); }
.lc-stage.current {
background: var(--teal-soft);
box-shadow: inset 0 0 0 1px rgba(63,163,163,0.3);
}
.lc-stage.current .lcs-num { color: var(--teal-2); }
.lc-stage.current .lcs-label { color: var(--teal-2); }
.lc-stage.done::after {
content: '';
position: absolute;
right: -1px; top: 50%;
transform: translateY(-50%);
width: 2px; height: 14px;
background: var(--green);
border-radius: 1px;
opacity: 0.4;
}
.lc-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.milestone-list {
display: flex; flex-direction: column;
position: relative;
}
.milestone-list::before {
content: '';
position: absolute;
left: 9px; top: 18px; bottom: 18px;
width: 2px;
background: var(--line);
}
.milestone {
display: grid;
grid-template-columns: 20px 1fr auto;
gap: 14px;
align-items: flex-start;
padding: 10px 0;
position: relative;
}
.ms-dot {
width: 20px; height: 20px;
border-radius: 50%;
background: var(--card);
border: 2px solid var(--line);
display: grid; place-items: center;
z-index: 1;
margin-top: 1px;
}
.ms-dot.done {
background: var(--green);
border-color: var(--green);
box-shadow: 0 0 0 4px rgba(52,211,153,0.12);
}
.ms-dot.done svg { width: 10px; height: 10px; color: #052e1a; stroke-width: 3.5; }
.ms-dot.current {
border-color: var(--teal);
background: var(--bg);
box-shadow: 0 0 0 4px var(--teal-soft);
}
.ms-dot.current::after {
content: ''; width: 8px; height: 8px;
border-radius: 50%;
background: var(--teal);
box-shadow: 0 0 8px var(--teal);
}
.ms-dot.upcoming { border-style: dashed; }
.ms-body .ms-title {
font-size: 13px; font-weight: 600;
color: var(--text);
letter-spacing: -0.005em;
}
.ms-body .ms-sub {
font-size: 11.5px; color: var(--text-4);
margin-top: 1px;
}
.ms-time {
font-size: 11px; color: var(--text-4);
font-variant-numeric: tabular-nums;
padding-top: 2px;
text-align: right;
white-space: nowrap;
}
.ms-time.future { color: var(--teal-2); }
.contract-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.kv-card {
padding: 14px 16px;
background: var(--card-glass-2);
border: 1px solid var(--line);
border-radius: 10px;
}
.kv-card .kvc-label {
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-4);
margin-bottom: 6px;
}
.kv-card .kvc-value {
font-size: 15px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.005em;
}
.kv-card .kvc-sub {
font-size: 11.5px;
color: var(--text-3);
margin-top: 2px;
}
/* Util */
.row-flex { display: flex; align-items: center; gap: 10px; }
.spacer { flex: 1; }
/* Section label */
.section-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-4);
margin: 24px 0 12px;
}
.section-title:first-of-type { margin-top: 0; }
@media (max-width: 1200px) {
.stat-row { grid-template-columns: repeat(2, 1fr); }
.grid-2, .grid-3 { grid-template-columns: 1fr; }
.entra-grid, .lc-grid { grid-template-columns: 1fr; }
.fido-hero { grid-template-columns: 1fr 1fr; }
.detail-inner { padding: 24px 24px 56px; }
}
@media (max-width: 920px) {
.app { grid-template-columns: 1fr; }
.sidebar { display: none; }
:root { --list-w: 280px; }
.detail-inner { padding: 20px 18px 48px; }
.fido-hero, .asset-stats { grid-template-columns: 1fr; }
.lc-stage-bar { grid-template-columns: repeat(2, 1fr); }
.contract-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="app">
<!-- ====== Sidebar ====== -->
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">C</div>
<div>
<div class="brand-name">Cereda Systems</div>
<div class="brand-sub">IT Nexus</div>
</div>
</div>
<div class="nav-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" width="14" height="14"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<span>Suchen…</span>
<span class="kbd">⌘K</span>
</div>
<div class="nav-group">
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>
Dashboard
</a>
</div>
<div class="nav-group">
<div class="nav-group-title">Helpdesk</div>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M22 12c0-5.5-4.5-10-10-10S2 6.5 2 12v6a2 2 0 0 0 2 2h2v-7H4v-1a8 8 0 0 1 16 0v1h-2v7h2a2 2 0 0 0 2-2z"/></svg>
Tickets <span class="nav-badge danger">2</span>
</a>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v17H6.5a2.5 2.5 0 0 0 0 5H20"/><path d="M8 7h8M8 11h6"/></svg>
Wissensdatenbank
</a>
</div>
<div class="nav-group">
<div class="nav-group-title">IT-Verwaltung</div>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="7" width="20" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M12 12v3"/></svg>
Assets <span class="nav-badge">65</span>
</a>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
Lizenzen
</a>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
FIDO-Keys
</a>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 12a9 9 0 1 1-9-9c2.5 0 4.78 1 6.43 2.57L21 8"/><path d="M21 3v5h-5"/></svg>
Lifecycle
</a>
</div>
<div class="nav-group">
<div class="nav-group-title">Monitoring</div>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
Monitoring
</a>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Defender
</a>
</div>
<div class="nav-group">
<div class="nav-group-title">Administration</div>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Entra Rechte
</a>
<a class="nav-item active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Benutzer <span class="nav-badge accent">132</span>
</a>
<a class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 2 4 5v6c0 5 3.5 8 8 11 4.5-3 8-6 8-11V5z"/><path d="M9 12l2 2 4-4"/></svg>
Security Reports
</a>
</div>
</aside>
<!-- ====== Main ====== -->
<main class="main">
<header class="topbar">
<div class="breadcrumb">
<span>Administration</span>
<span class="sep">/</span>
<span class="current">Benutzerverwaltung</span>
</div>
<div class="topbar-right">
<button class="icon-btn" title="Benutzer einladen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><path d="M20 8v6M23 11h-6"/></svg>
</button>
<button class="icon-btn" title="Export">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
</button>
<button class="icon-btn" title="Benachrichtigungen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M13.7 21a2 2 0 0 1-3.4 0"/></svg>
<span class="dot"></span>
</button>
<div class="user-chip">
<span class="uc-name">Simon Grüssing</span>
<span class="uc-av">SG</span>
</div>
</div>
</header>
<!-- ====== Workspace ====== -->
<div class="workspace">
<!-- === User list === -->
<aside class="list-panel">
<div class="list-head">
<div class="list-title-row">
<div class="list-title">Benutzer</div>
<div class="list-count" id="listCount">132 gesamt</div>
</div>
<div class="list-sub">Verwalte Identitäten, Rollen &amp; Zugriffe</div>
<div class="list-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<input id="searchInput" type="text" placeholder="Name oder E-Mail suchen…" />
</div>
<div class="filter-row" id="filterRow">
<button class="filter-pill on" data-role="all">Alle</button>
<button class="filter-pill" data-role="admin">Admin</button>
<button class="filter-pill" data-role="manager">Manager</button>
<button class="filter-pill" data-role="dev">Dev</button>
<button class="filter-pill" data-role="staff">Staff</button>
<button class="filter-pill" data-role="extern">Extern</button>
</div>
</div>
<div class="list-scroll" id="listScroll">
<!-- users injected by JS -->
</div>
</aside>
<!-- === Detail === -->
<section class="detail-panel">
<div class="detail-inner" id="detailInner">
<!-- detail injected by JS -->
</div>
</section>
</div>
</main>
</div>
<!-- ============ DATA + LOGIC ============ -->
<script>
/* ---- Users ---- */
const USERS = [
{ id: 'sgruessing', first: 'Simon', last: 'Grüssing', email: 's.gruessing@cereda.de', role: 'admin', roleLabel: 'Super Admin', dept: 'IT & Infrastruktur', position: 'Head of IT', phone: '+49 2351 9921-104', location: 'Lüdenscheid', manager: 'Marcus Cereda', managerInit: 'MC', joined: '12. März 2019', presence: 'online', initials: 'SG', avClass: 'av-admin', stats: { assets: 4, lic: 12, fido: 2, sessions: 3 }, mfa: 'FIDO2 + TOTP', lastPwd: 'vor 23 Tagen', lastLogin: 'vor 4 Minuten', risk: 'Niedrig', section: 'Favoriten' },
{ id: 'mcereda', first: 'Marcus', last: 'Cereda', email: 'm.cereda@cereda.de', role: 'admin', roleLabel: 'CEO', dept: 'Geschäftsführung', position: 'Geschäftsführer', phone: '+49 2351 9921-100', location: 'Lüdenscheid', manager: '—', managerInit: '', joined: '01. Jan 2008', presence: 'online', initials: 'MC', avClass: 'av-admin', stats: { assets: 3, lic: 14, fido: 2, sessions: 2 }, mfa: 'FIDO2 + TOTP', lastPwd: 'vor 12 Tagen', lastLogin: 'vor 1 Stunde', risk: 'Niedrig', section: 'Favoriten' },
{ id: 'akoehler', first: 'Anna', last: 'Köhler', email: 'a.koehler@cereda.de', role: 'manager', roleLabel: 'Manager', dept: 'Vertrieb', position: 'Sales Lead DACH', phone: '+49 2351 9921-220', location: 'Lüdenscheid', manager: 'Marcus Cereda', managerInit: 'MC', joined: '04. Sep 2020', presence: 'online', initials: 'AK', avClass: 'av-manager', stats: { assets: 2, lic: 9, fido: 1, sessions: 1 }, mfa: 'FIDO2', lastPwd: 'vor 41 Tagen', lastLogin: 'vor 12 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'tlange', first: 'Thomas', last: 'Lange', email: 't.lange@cereda.de', role: 'dev', roleLabel: 'Developer', dept: 'Engineering', position: 'Senior Software Engineer', phone: '+49 2351 9921-340', location: 'Remote', manager: 'Simon Grüssing', managerInit: 'SG', joined: '17. Mai 2021', presence: 'online', initials: 'TL', avClass: 'av-dev', stats: { assets: 2, lic: 11, fido: 2, sessions: 2 }, mfa: 'FIDO2 + TOTP', lastPwd: 'vor 8 Tagen', lastLogin: 'vor 2 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'jweber', first: 'Julia', last: 'Weber', email: 'j.weber@cereda.de', role: 'dev', roleLabel: 'Developer', dept: 'Engineering', position: 'Frontend Engineer', phone: '+49 2351 9921-342', location: 'Lüdenscheid', manager: 'Simon Grüssing', managerInit: 'SG', joined: '02. Nov 2022', presence: 'away', initials: 'JW', avClass: 'av-dev', stats: { assets: 1, lic: 8, fido: 1, sessions: 1 }, mfa: 'TOTP', lastPwd: 'vor 67 Tagen', lastLogin: 'vor 3 Stunden', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'rkrause', first: 'Robert', last: 'Krause', email: 'r.krause@cereda.de', role: 'staff', roleLabel: 'Mitarbeiter', dept: 'Marketing', position: 'Content Manager', phone: '+49 2351 9921-410', location: 'Lüdenscheid', manager: 'Anna Köhler', managerInit: 'AK', joined: '15. Aug 2023', presence: 'online', initials: 'RK', avClass: 'av-staff', stats: { assets: 1, lic: 6, fido: 1, sessions: 1 }, mfa: 'TOTP', lastPwd: 'vor 33 Tagen', lastLogin: 'vor 28 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'sbecker', first: 'Sandra', last: 'Becker', email: 's.becker@cereda.de', role: 'manager', roleLabel: 'Manager', dept: 'Personal', position: 'HR Manager', phone: '+49 2351 9921-510', location: 'Lüdenscheid', manager: 'Marcus Cereda', managerInit: 'MC', joined: '21. Feb 2019', presence: 'online', initials: 'SB', avClass: 'av-manager', stats: { assets: 1, lic: 7, fido: 1, sessions: 1 }, mfa: 'FIDO2', lastPwd: 'vor 19 Tagen', lastLogin: 'vor 1 Stunde', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'mschmitt', first: 'Michael', last: 'Schmitt', email: 'm.schmitt@cereda.de', role: 'staff', roleLabel: 'Mitarbeiter', dept: 'Vertrieb', position: 'Account Manager', phone: '+49 2351 9921-225', location: 'Köln', manager: 'Anna Köhler', managerInit: 'AK', joined: '07. Apr 2024', presence: 'offline', initials: 'MS', avClass: 'av-staff', stats: { assets: 2, lic: 8, fido: 0, sessions: 0 }, mfa: 'TOTP', lastPwd: 'vor 91 Tagen', lastLogin: 'vor 2 Tagen', risk: 'Mittel', section: 'Alle Benutzer' },
{ id: 'lhoffmann', first: 'Lisa', last: 'Hoffmann', email: 'l.hoffmann@cereda.de', role: 'staff', roleLabel: 'Mitarbeiter', dept: 'Finanzen', position: 'Buchhaltung', phone: '+49 2351 9921-620', location: 'Lüdenscheid', manager: 'Sandra Becker', managerInit: 'SB', joined: '11. Jun 2020', presence: 'online', initials: 'LH', avClass: 'av-staff', stats: { assets: 1, lic: 5, fido: 1, sessions: 1 }, mfa: 'FIDO2', lastPwd: 'vor 14 Tagen', lastLogin: 'vor 22 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'dwagner', first: 'Daniel', last: 'Wagner', email: 'd.wagner@cereda.de', role: 'dev', roleLabel: 'Developer', dept: 'Engineering', position: 'DevOps Engineer', phone: '+49 2351 9921-345', location: 'Remote', manager: 'Simon Grüssing', managerInit: 'SG', joined: '03. Jan 2022', presence: 'online', initials: 'DW', avClass: 'av-dev', stats: { assets: 2, lic: 10, fido: 2, sessions: 2 }, mfa: 'FIDO2 + TOTP', lastPwd: 'vor 6 Tagen', lastLogin: 'vor 8 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'kfischer', first: 'Karin', last: 'Fischer', email: 'k.fischer@cereda.de', role: 'staff', roleLabel: 'Mitarbeiter', dept: 'Office', position: 'Office Management', phone: '+49 2351 9921-110', location: 'Lüdenscheid', manager: 'Sandra Becker', managerInit: 'SB', joined: '18. Sep 2017', presence: 'online', initials: 'KF', avClass: 'av-staff', stats: { assets: 1, lic: 4, fido: 1, sessions: 1 }, mfa: 'TOTP', lastPwd: 'vor 28 Tagen', lastLogin: 'vor 15 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'fbauer', first: 'Felix', last: 'Bauer', email: 'f.bauer@cereda.de', role: 'admin', roleLabel: 'IT Admin', dept: 'IT & Infrastruktur', position: 'Systemadministrator', phone: '+49 2351 9921-103', location: 'Lüdenscheid', manager: 'Simon Grüssing', managerInit: 'SG', joined: '02. Aug 2021', presence: 'online', initials: 'FB', avClass: 'av-admin', stats: { assets: 3, lic: 12, fido: 2, sessions: 2 }, mfa: 'FIDO2 + TOTP', lastPwd: 'vor 11 Tagen', lastLogin: 'vor 6 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'pmueller', first: 'Paula', last: 'Müller', email: 'p.mueller@cereda.de', role: 'staff', roleLabel: 'Mitarbeiter', dept: 'Marketing', position: 'Grafikdesignerin', phone: '+49 2351 9921-415', location: 'Lüdenscheid', manager: 'Robert Krause', managerInit: 'RK', joined: '14. Mai 2023', presence: 'away', initials: 'PM', avClass: 'av-staff', stats: { assets: 1, lic: 6, fido: 1, sessions: 1 }, mfa: 'FIDO2', lastPwd: 'vor 22 Tagen', lastLogin: 'vor 1 Stunde', risk: 'Niedrig', section: 'Alle Benutzer' },
{ id: 'jschwarz', first: 'Jens', last: 'Schwarz', email: 'j.schwarz@externfirma.com', role: 'extern', roleLabel: 'Extern', dept: 'Beratung', position: 'Externer Berater', phone: '+49 30 1234 5678', location: 'Berlin', manager: 'Marcus Cereda', managerInit: 'MC', joined: '15. Jan 2025', presence: 'offline', initials: 'JS', avClass: 'av-extern', stats: { assets: 0, lic: 2, fido: 0, sessions: 0 }, mfa: 'TOTP', lastPwd: 'vor 4 Tagen', lastLogin: 'vor 5 Tagen', risk: 'Mittel', section: 'Alle Benutzer' },
{ id: 'tneumann', first: 'Tim', last: 'Neumann', email: 't.neumann@cereda.de', role: 'staff', roleLabel: 'Praktikant', dept: 'Engineering', position: 'Werkstudent', phone: '+49 2351 9921-348', location: 'Lüdenscheid', manager: 'Thomas Lange', managerInit: 'TL', joined: '01. Apr 2026', presence: 'online', initials: 'TN', avClass: 'av-intern', stats: { assets: 1, lic: 3, fido: 0, sessions: 1 }, mfa: 'TOTP', lastPwd: 'vor 2 Tagen', lastLogin: 'vor 45 Minuten', risk: 'Niedrig', section: 'Alle Benutzer' }
];
/* ---- State ---- */
let state = {
selected: 'tlange',
filter: 'all',
search: '',
tab: 'overview'
};
/* ---- Helpers ---- */
const $ = (q, r=document) => r.querySelector(q);
const $$ = (q, r=document) => [...r.querySelectorAll(q)];
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
/* ---- Render list ---- */
function renderList() {
const term = state.search.trim().toLowerCase();
const filtered = USERS.filter(u => {
if (state.filter !== 'all' && u.role !== state.filter) return false;
if (term) {
const hay = (u.first + ' ' + u.last + ' ' + u.email).toLowerCase();
if (!hay.includes(term)) return false;
}
return true;
});
$('#listCount').textContent = filtered.length + ' von ' + USERS.length;
// Group by section
const sections = {};
filtered.forEach(u => {
(sections[u.section] = sections[u.section] || []).push(u);
});
const order = ['Favoriten', 'Alle Benutzer'];
let html = '';
order.forEach(label => {
if (!sections[label]) return;
html += `<div class="list-section-label">${esc(label)}</div>`;
sections[label].forEach(u => {
html += `
<div class="user-row ${u.id === state.selected ? 'active' : ''}" data-id="${u.id}">
<div class="ur-av ${u.avClass}">${esc(u.initials)}</div>
<div class="ur-presence ${u.presence}"></div>
<div class="ur-body">
<div class="ur-name">${esc(u.first + ' ' + u.last)}</div>
<div class="ur-meta">
<span class="email">${esc(u.email)}</span>
</div>
</div>
<span class="role-chip ${u.role}">${esc(u.roleLabel)}</span>
</div>`;
});
});
if (filtered.length === 0) {
html = `<div style="padding:40px 20px;text-align:center;color:var(--text-4);font-size:13px;">Keine Benutzer gefunden</div>`;
}
$('#listScroll').innerHTML = html;
$$('.user-row').forEach(el => el.addEventListener('click', () => {
state.selected = el.dataset.id;
state.tab = 'overview';
renderList(); renderDetail();
}));
}
/* ---- Render detail ---- */
function renderDetail() {
const u = USERS.find(x => x.id === state.selected) || USERS[0];
const tabs = [
{ id: 'overview', label: 'Übersicht' },
{ id: 'assets', label: 'Assets', count: u.stats.assets },
{ id: 'lic', label: 'Lizenzen', count: u.stats.lic },
{ id: 'entra', label: 'Entra/AD' },
{ id: 'fido', label: 'FIDO-Keys', count: u.stats.fido },
{ id: 'lifecycle', label: 'Lifecycle' }
];
const tabsHtml = tabs.map(t => `
<button class="tab ${state.tab === t.id ? 'active' : ''}" data-tab="${t.id}">
${esc(t.label)}
${t.count != null ? `<span class="tab-count">${t.count}</span>` : ''}
</button>
`).join('');
$('#detailInner').innerHTML = `
<div class="detail-head">
<div class="dh-avatar ${u.avClass}">
${esc(u.initials)}
<span class="dh-presence" style="${u.presence !== 'online' ? 'background:var(--amber)' : ''}${u.presence === 'offline' ? ';background:var(--text-4)' : ''}"></span>
</div>
<div class="dh-body">
<div class="dh-name-row">
<h1 class="dh-name">${esc(u.first + ' ' + u.last)}</h1>
<span class="dh-handle">@${esc(u.id)}</span>
</div>
<div class="dh-email">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="m22 6-10 7L2 6"/></svg>
${esc(u.email)}
</div>
<div class="dh-badges">
<span class="dh-badge role-manager" style="${u.role === 'admin' ? 'background:var(--red-soft);color:#fca5a5;border-color:rgba(239,68,68,0.3)' : u.role === 'dev' ? 'background:var(--purple-soft);color:#c4b5fd;border-color:rgba(167,139,250,0.3)' : u.role === 'staff' ? 'background:var(--blue-soft);color:#93c5fd;border-color:rgba(96,165,250,0.3)' : u.role === 'extern' ? 'background:var(--amber-soft);color:#fcd34d;border-color:rgba(245,158,11,0.3)' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 4 5v6c0 5 3.5 8 8 11 4.5-3 8-6 8-11V5z"/></svg>
${esc(u.roleLabel)}
</span>
<span class="dh-badge dept">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M3 21h18M5 21V7l7-4 7 4v14M9 9h.01M15 9h.01M9 13h.01M15 13h.01M9 17h6"/></svg>
${esc(u.dept)}
</span>
<span class="dh-badge status-active">
<span class="pulse"></span>
Aktiv
</span>
</div>
</div>
<div class="dh-actions">
<button class="btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>
Bearbeiten
</button>
<button class="btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="16" r="1"/><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Passwort reset
</button>
<button class="btn danger" title="Weitere Aktionen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
</button>
</div>
</div>
<div class="tabs-bar" id="tabsBar">${tabsHtml}</div>
<div id="tabContent">${renderTab(u)}</div>
`;
$$('.tab').forEach(el => el.addEventListener('click', () => {
state.tab = el.dataset.tab;
renderDetail();
}));
}
/* ---- Tab content ---- */
function renderTab(u) {
if (state.tab === 'overview') return overviewHtml(u);
if (state.tab === 'assets') return assetsHtml(u);
if (state.tab === 'lic') return licensesHtml(u);
if (state.tab === 'entra') return entraHtml(u);
if (state.tab === 'fido') return fidoHtml(u);
if (state.tab === 'lifecycle') return lifecycleHtml(u);
return '';
}
function overviewHtml(u) {
return `
<div class="tab-panel active">
<div class="stat-row">
<div class="stat">
<div class="stat-head">
<div class="stat-icon s-status"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg></div>
<span class="stat-trend">${esc(u.lastLogin)}</span>
</div>
<div class="stat-value">Aktiv</div>
<div class="stat-label">Konto Status</div>
</div>
<div class="stat">
<div class="stat-head">
<div class="stat-icon s-assets"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="7" width="20" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></div>
<span class="stat-trend">zugewiesen</span>
</div>
<div class="stat-value">${u.stats.assets}</div>
<div class="stat-label">Assets</div>
</div>
<div class="stat">
<div class="stat-head">
<div class="stat-icon s-lic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg></div>
<span class="stat-trend">aktiv</span>
</div>
<div class="stat-value">${u.stats.lic}</div>
<div class="stat-label">Lizenzen</div>
</div>
<div class="stat">
<div class="stat-head">
<div class="stat-icon s-fido"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15.5 7.5 3 3L22 7l-3-3M21 2l-9.6 9.6a5.5 5.5 0 1 1-2 2L19 4"/></svg></div>
<span class="stat-trend">${u.mfa.includes('FIDO') ? 'gepaart' : '—'}</span>
</div>
<div class="stat-value">${u.stats.fido}</div>
<div class="stat-label">FIDO-Keys</div>
</div>
</div>
<div class="grid-2">
<!-- Persönliche Informationen -->
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
Persönliche Informationen
</div>
<a class="card-link" href="#">Bearbeiten</a>
</div>
<div class="info-list">
<div class="info-row">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="7" r="4"/><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg>Position</div>
<div class="v">${esc(u.position)}</div>
</div>
<div class="info-row">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M3 21h18M5 21V7l7-4 7 4v14M9 9h.01M15 9h.01M9 13h.01M15 13h.01M9 17h6"/></svg>Abteilung</div>
<div class="v">${esc(u.dept)}</div>
</div>
<div class="info-row">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M22 16.92V21a1 1 0 0 1-1.11 1 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 3.18 4.11 1 1 0 0 1 4.18 3h4.09a1 1 0 0 1 1 .75c.12.93.34 1.84.66 2.71a1 1 0 0 1-.21 1L8 9.09a16 16 0 0 0 6 6l1.62-1.62a1 1 0 0 1 1-.21c.87.32 1.78.54 2.71.66a1 1 0 0 1 .67 1z"/></svg>Telefon</div>
<div class="v mono">${esc(u.phone)}<button class="copy-btn" title="Kopieren"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div>
</div>
<div class="info-row">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>Standort</div>
<div class="v">${esc(u.location)}</div>
</div>
<div class="info-row">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>Vorgesetzter</div>
<div class="v">
${u.managerInit ? `<span class="mini-av" style="background:linear-gradient(135deg,#5dbcbc,#3fa3a3)">${esc(u.managerInit)}</span>` : ''}
${esc(u.manager)}
</div>
</div>
<div class="info-row">
<div class="k"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>Eintritt</div>
<div class="v">${esc(u.joined)}</div>
</div>
</div>
</div>
<!-- Sicherheit -->
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Sicherheit &amp; MFA
</div>
<span class="dh-badge status-active" style="font-size:10.5px;padding:2px 8px;">Risiko: ${esc(u.risk)}</span>
</div>
<div class="sec-list">
<div class="sec-item">
<div class="sec-icon si-key"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15.5 7.5 3 3L22 7l-3-3M21 2l-9.6 9.6a5.5 5.5 0 1 1-2 2L19 4"/></svg></div>
<div class="sec-body">
<div class="sec-label">Mehrfaktor-Authentifizierung</div>
<div class="sec-sub">${esc(u.mfa)}</div>
</div>
<span class="sec-status ok">Aktiv</span>
</div>
<div class="sec-item">
<div class="sec-icon si-pwd"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="sec-body">
<div class="sec-label">Passwort</div>
<div class="sec-sub">Letzte Änderung ${esc(u.lastPwd)}</div>
</div>
<span class="sec-status ${u.lastPwd.includes('91') || u.lastPwd.includes('67') ? 'warn' : 'ok'}">${u.lastPwd.includes('91') ? 'Veraltet' : 'OK'}</span>
</div>
<div class="sec-item">
<div class="sec-icon si-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg></div>
<div class="sec-body">
<div class="sec-label">Conditional Access</div>
<div class="sec-sub">3 Richtlinien zugewiesen · Compliant</div>
</div>
<span class="sec-status ok">OK</span>
</div>
<div class="sec-item">
<div class="sec-icon ${u.stats.sessions > 0 ? 'si-ok' : 'si-warn'}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
<div class="sec-body">
<div class="sec-label">Aktive Sessions</div>
<div class="sec-sub">${u.stats.sessions} Geräte · Letzte Anmeldung ${esc(u.lastLogin)}</div>
</div>
<span class="sec-status ${u.stats.sessions > 0 ? 'ok' : 'off'}">${u.stats.sessions > 0 ? u.stats.sessions + ' aktiv' : 'Keine'}</span>
</div>
</div>
</div>
</div>
<div class="grid-3">
<!-- Aktivitäten -->
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
Letzte Aktivitäten
</div>
<a class="card-link" href="#">Audit-Log →</a>
</div>
<div class="activity-list">
<div class="activity">
<div class="act-dot ad-login"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3"/></svg></div>
<div class="act-body">
<div class="act-text"><b>Anmeldung erfolgreich</b> via FIDO2 Key</div>
<div class="act-meta">192.168.0.45 · Edge 124 · Windows 11</div>
</div>
<div class="act-time">${esc(u.lastLogin)}</div>
</div>
<div class="activity">
<div class="act-dot ad-asset"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="7" width="20" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></div>
<div class="act-body">
<div class="act-text">Asset <b>TBO-NB-02</b> zugewiesen</div>
<div class="act-meta">Durch Felix Bauer · Lifecycle Update</div>
</div>
<div class="act-time">vor 2 Tagen</div>
</div>
<div class="activity">
<div class="act-dot ad-pwd"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="act-body">
<div class="act-text">Passwort geändert</div>
<div class="act-meta">Eigenständig · Self-Service Portal</div>
</div>
<div class="act-time">${esc(u.lastPwd)}</div>
</div>
<div class="activity">
<div class="act-dot ad-key"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15.5 7.5 3 3L22 7l-3-3M21 2l-9.6 9.6a5.5 5.5 0 1 1-2 2L19 4"/></svg></div>
<div class="act-body">
<div class="act-text">FIDO-Key <b>YubiKey 5C NFC</b> registriert</div>
<div class="act-meta">Backup-Token paired</div>
</div>
<div class="act-time">vor 14 Tagen</div>
</div>
<div class="activity">
<div class="act-dot ad-perm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 12l2 2 4-4"/><circle cx="12" cy="12" r="10"/></svg></div>
<div class="act-body">
<div class="act-text">Gruppenmitgliedschaft <b>${esc(u.dept)}</b> erweitert</div>
<div class="act-meta">+ M365 E5 Lizenzpool</div>
</div>
<div class="act-time">vor 21 Tagen</div>
</div>
</div>
</div>
<!-- Gruppen & Rollen -->
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
Gruppen &amp; Rollen
</div>
</div>
<div class="group-section">
<h4>Entra ID Rollen</h4>
<div class="group-list">
${u.role === 'admin' ? `
<span class="group-chip role"><span class="g-dot"></span>Global Administrator</span>
<span class="group-chip role"><span class="g-dot"></span>Intune Administrator</span>` : ''}
${u.role === 'manager' ? `<span class="group-chip role"><span class="g-dot"></span>User Administrator</span>` : ''}
${u.role === 'dev' ? `<span class="group-chip role"><span class="g-dot"></span>Application Developer</span>` : ''}
${u.role === 'staff' || u.role === 'extern' ? `<span class="group-chip role"><span class="g-dot"></span>Member</span>` : ''}
</div>
</div>
<div class="group-section">
<h4>Sicherheitsgruppen</h4>
<div class="group-list">
<span class="group-chip entra"><span class="g-dot"></span>SG-${esc(u.dept.split(' ')[0])}</span>
<span class="group-chip entra"><span class="g-dot"></span>SG-MFA-Required</span>
<span class="group-chip entra"><span class="g-dot"></span>SG-VPN-Users</span>
${u.role !== 'extern' ? `<span class="group-chip entra"><span class="g-dot"></span>SG-Intranet-Full</span>` : ''}
</div>
</div>
<div class="group-section">
<h4>Verteiler</h4>
<div class="group-list">
<span class="group-chip dist"><span class="g-dot"></span>alle@cereda.de</span>
<span class="group-chip dist"><span class="g-dot"></span>${esc(u.dept.toLowerCase().split(' ')[0])}@cereda.de</span>
</div>
</div>
<div class="group-section">
<h4>App-Berechtigungen</h4>
<div class="perm-list">
<div class="perm-item"><span class="p-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></span><span class="p-label">IT Nexus Portal</span><span class="p-scope">${u.role === 'admin' ? 'admin' : 'user'}</span></div>
<div class="perm-item"><span class="p-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></span><span class="p-label">Microsoft 365</span><span class="p-scope">e3</span></div>
<div class="perm-item ${u.role === 'extern' ? 'off' : ''}"><span class="p-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></span><span class="p-label">Cereda Secure Share</span><span class="p-scope">${u.role === 'extern' ? '—' : 'editor'}</span></div>
<div class="perm-item ${u.role === 'staff' || u.role === 'extern' ? 'off' : ''}"><span class="p-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></span><span class="p-label">Azure DevOps</span><span class="p-scope">${u.role === 'staff' || u.role === 'extern' ? '—' : 'contributor'}</span></div>
</div>
</div>
</div>
</div>
</div>
`;
}
function assetsHtml(u) {
const allAssets = [
{ name: 'TBO-NB-02', type: 'Notebook', model: 'Dell Latitude 7440', sn: 'SN1234567890', cls: 'nb', status: 'online', cpu: 32, ram: 51, disk: 58, warranty: 64, age: '14 Monate', value: '€ 1.890',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="4" width="20" height="13" rx="1.5"/><path d="M1 21h22M9 21v-4M15 21v-4"/></svg>' },
{ name: 'TBO-MON-14', type: 'Monitor', model: 'Dell U2723QE 27" 4K', sn: 'CN0XYZ12345', cls: 'mon', status: 'online', cpu: 0, ram: 0, disk: 0, warranty: 78, age: '11 Monate', value: '€ 620',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="3" width="20" height="14" rx="1.5"/><path d="M8 21h8M12 17v4"/></svg>' },
{ name: 'TBO-PHN-08', type: 'Smartphone', model: 'iPhone 15 Pro 256GB', sn: 'F2LXJ7Q8N9PR', cls: 'phn', status: 'online', cpu: 0, ram: 0, disk: 42, warranty: 88, age: '6 Monate', value: '€ 1.299',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="6" y="2" width="12" height="20" rx="2.5"/><path d="M11 18h2"/></svg>' },
{ name: 'TBO-DOCK-02', type: 'Dock', model: 'Dell WD22TB4', sn: 'CN-DK-22-4421', cls: 'dock', status: 'online', cpu: 0, ram: 0, disk: 0, warranty: 50, age: '20 Monate', value: '€ 320',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="9" width="20" height="9" rx="1.5"/><path d="M6 9V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v3M6 13h4M14 13h4"/></svg>' }
];
const assets = allAssets.slice(0, Math.max(u.stats.assets, 1));
const totalValue = assets.reduce((s, a) => s + parseInt(a.value.replace(/\D/g, '')), 0);
return `
<div class="tab-panel active">
<div class="asset-stats">
<div class="stat">
<div class="stat-head"><div class="stat-icon s-assets"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="7" width="20" height="13" rx="2"/></svg></div></div>
<div class="stat-value">${assets.length}</div>
<div class="stat-label">Geräte zugewiesen</div>
</div>
<div class="stat">
<div class="stat-head"><div class="stat-icon s-lic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div></div>
<div class="stat-value">€ ${totalValue.toLocaleString('de-DE')}</div>
<div class="stat-label">Buchwert gesamt</div>
</div>
<div class="stat">
<div class="stat-head"><div class="stat-icon s-status"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div></div>
<div class="stat-value">${assets.filter(a => a.status === 'online').length}/<span style="color:var(--text-4);font-size:18px">${assets.length}</span></div>
<div class="stat-label">Aktuell online</div>
</div>
<div class="stat">
<div class="stat-head"><div class="stat-icon s-fido"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 8v4l3 3"/><circle cx="12" cy="12" r="10"/></svg></div></div>
<div class="stat-value">${assets[0]?.age || '—'}</div>
<div class="stat-label">Ältestes Gerät</div>
</div>
</div>
<div class="section-title">Zugewiesene Hardware</div>
<div class="device-grid">
${assets.map(a => `
<div class="device-card">
<div class="dc-top">
<div class="device-icon ${a.cls}">${a.icon}</div>
<div class="dc-meta">
<div class="dc-name">${esc(a.name)}</div>
<div class="dc-model">${esc(a.model)}</div>
</div>
<div class="dc-status-dot ${a.status === 'online' ? '' : 'off'}" title="${esc(a.status)}"></div>
</div>
<div class="dc-health">
${a.cpu > 0 ? `
<div class="dc-bar">
<div class="dc-bar-label"><span>CPU-Last</span><b>${a.cpu}%</b></div>
<div class="dc-bar-track"><div class="dc-bar-fill" style="width:${a.cpu}%;--bar-color:${a.cpu > 75 ? 'var(--red)' : a.cpu > 50 ? 'var(--amber)' : 'var(--teal)'}"></div></div>
</div>` : ''}
${a.disk > 0 ? `
<div class="dc-bar">
<div class="dc-bar-label"><span>Speicher</span><b>${a.disk}%</b></div>
<div class="dc-bar-track"><div class="dc-bar-fill" style="width:${a.disk}%;--bar-color:var(--blue)"></div></div>
</div>` : ''}
<div class="dc-bar">
<div class="dc-bar-label"><span>Garantie</span><b>${a.warranty}%</b></div>
<div class="dc-bar-track"><div class="dc-bar-fill" style="width:${a.warranty}%;--bar-color:${a.warranty < 25 ? 'var(--red)' : a.warranty < 50 ? 'var(--amber)' : 'var(--green)'}"></div></div>
</div>
</div>
<div class="dc-footer">
<span class="dc-tag">${esc(a.type)}</span>
<span>${esc(a.sn)}</span>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
function licensesHtml(u) {
const cats = {
'Produktivität': [
{ name: 'Microsoft 365 E3', plan: 'Office Apps, Teams, SharePoint', cost: '€ 32,40', cls: 'lc-m365', initials: 'M' },
{ name: 'Zoom Pro', plan: 'Bis 100 Teilnehmer · Cloud-Rec.', cost: '€ 13,99', cls: 'lc-zoom', initials: 'Z' },
{ name: 'Slack Pro', plan: 'Active Seat · 90 Tage History', cost: '€ 7,25', cls: 'lc-slack', initials: 'S' },
{ name: '1Password Business', plan: 'Persönlicher Tresor + Teams', cost: '€ 7,99', cls: 'lc-1pwd', initials: '1' }
],
'Entwicklung': [
{ name: 'GitHub Enterprise', plan: 'Cloud · Seat', cost: '€ 19,00', cls: 'lc-github', initials: 'G' },
{ name: 'JetBrains All Products', plan: 'Personal · IDE Suite', cost: '€ 24,90', cls: 'lc-jb', initials: 'J' },
{ name: 'AWS Developer Support', plan: 'Pro Account · Shared', cost: '€ 29,00', cls: 'lc-aws', initials: 'A' }
],
'Design & Tools': [
{ name: 'Adobe Creative Cloud', plan: 'All Apps · 100 GB', cost: '€ 67,99', cls: 'lc-adobe', initials: 'A' },
{ name: 'Figma Professional', plan: 'Editor Seat', cost: '€ 12,00', cls: 'lc-figma', initials: 'F' },
{ name: 'Notion Plus', plan: 'Team Member', cost: '€ 9,50', cls: 'lc-no', initials: 'N' },
{ name: 'Tailwind UI', plan: 'Team License', cost: '€ 8,30', cls: 'lc-tw', initials: 'T' },
{ name: 'Intercom Helpdesk', plan: 'Pro Seat', cost: '€ 39,00', cls: 'lc-int', initials: 'I' }
]
};
const flat = Object.values(cats).flat().slice(0, Math.max(u.stats.lic, 1));
const totalCost = flat.reduce((s, l) => s + parseFloat(l.cost.replace(/[^\d,]/g, '').replace(',', '.')), 0);
const expiringSoon = Math.min(2, flat.length);
// re-group filtered
let assigned = flat;
let catGroups = {};
Object.entries(cats).forEach(([k, list]) => {
const got = list.filter(l => assigned.includes(l));
if (got.length) catGroups[k] = got;
});
return `
<div class="tab-panel active">
<div class="lic-summary">
<div>
<div class="ls-label">Aktive Lizenzen</div>
<div class="ls-value">${assigned.length}</div>
<div class="ls-sub">Aus ${u.stats.lic} zugewiesenen Plätzen</div>
</div>
<div>
<div class="ls-label">Kosten pro Monat</div>
<div class="ls-value">€ ${totalCost.toFixed(2).replace('.', ',')}</div>
<div class="ls-sub">€ ${(totalCost * 12).toFixed(0)} jährlich</div>
</div>
<div>
<div class="ls-label">Verlängerung fällig</div>
<div class="ls-value" style="color:${expiringSoon ? 'var(--amber)' : 'var(--green)'}">${expiringSoon}</div>
<div class="ls-sub">${expiringSoon ? 'in den nächsten 60 Tagen' : 'Keine in Kürze'}</div>
</div>
</div>
${Object.entries(catGroups).map(([cat, list]) => `
<div class="lic-cat-title">
<span>${esc(cat)}</span>
<span class="ct-count">${list.length}</span>
</div>
<div class="lic-grid">
${list.map(l => `
<div class="lic-card">
<div class="lic-logo ${l.cls}">${esc(l.initials)}</div>
<div class="lic-body">
<div class="lic-name">${esc(l.name)}</div>
<div class="lic-plan">${esc(l.plan)}</div>
</div>
<div class="lic-cost">${esc(l.cost)}<span class="pm">/Monat</span></div>
</div>
`).join('')}
</div>
`).join('')}
</div>
`;
}
function entraHtml(u) {
const objId = '8f3a9e2b-4c1d-4f8a-9b2c-' + (u.id + '000000000000').slice(0, 12);
return `
<div class="tab-panel active">
<div class="entra-grid">
<div class="identity-card">
<div class="card-head" style="margin-bottom:14px">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M18 8h1a4 4 0 0 1 0 8h-1M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
Hybrid Identity
</div>
<span class="sec-status ok">Synchronisiert</span>
</div>
<div class="sync-diagram">
<div class="sync-node cloud">
<div class="sn-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg></div>
<div class="sn-label">Entra ID</div>
<div class="sn-id">cereda.onmicrosoft.com</div>
</div>
<div class="sync-arrow">
<div class="line"></div>
<div class="lbl">Connect Sync · 28 min</div>
<div class="line"></div>
</div>
<div class="sync-node ad">
<div class="sn-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6"/></svg></div>
<div class="sn-label">Active Directory</div>
<div class="sn-id">CEREDA.local</div>
</div>
</div>
<div class="info-list">
<div class="info-row">
<div class="k">Object ID</div>
<div class="v mono">${esc(objId)}<button class="copy-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button></div>
</div>
<div class="info-row">
<div class="k">UPN</div>
<div class="v mono">${esc(u.email)}</div>
</div>
<div class="info-row">
<div class="k">sAMAccountName</div>
<div class="v mono">CEREDA\\${esc(u.id)}</div>
</div>
<div class="info-row">
<div class="k">Distinguished Name</div>
<div class="v mono" style="font-size:11.5px">CN=${esc(u.first + ' ' + u.last)},OU=${esc(u.dept.split(' ')[0])},OU=Users,DC=cereda,DC=local</div>
</div>
<div class="info-row">
<div class="k">User Type</div>
<div class="v">${u.role === 'extern' ? '<span class="role-chip extern">Guest</span>' : '<span class="role-chip manager">Member</span>'}</div>
</div>
<div class="info-row">
<div class="k">Source of Authority</div>
<div class="v">Hybrid · Windows Server AD</div>
</div>
<div class="info-row">
<div class="k">Account Status</div>
<div class="v"><span class="sec-status ok">Enabled · Not locked</span></div>
</div>
</div>
<div class="token-preview">
<span class="tn">// Letztes ID-Token (decoded JWT)</span>
{
<span class="tk">"aud"</span>: <span class="tv">"api://it-nexus.cereda.de"</span>,
<span class="tk">"oid"</span>: <span class="tv">"${esc(objId)}"</span>,
<span class="tk">"preferred_username"</span>: <span class="tv">"${esc(u.email)}"</span>,
<span class="tk">"roles"</span>: [<span class="tv">"${esc(u.roleLabel)}"</span>],
<span class="tk">"groups"</span>: [<span class="tv">"SG-${esc(u.dept.split(' ')[0])}"</span>, <span class="tv">"SG-MFA-Required"</span>],
<span class="tk">"amr"</span>: [<span class="tv">"pwd"</span>, <span class="tv">"fido"</span>],
<span class="tk">"iat"</span>: <span class="tv">1748432104</span>
}
</div>
</div>
<div style="display:flex;flex-direction:column;gap:14px">
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Conditional Access
</div>
<span class="card-link">3 zugewiesen</span>
</div>
<div class="ca-list">
<div class="ca-policy">
<div class="cap-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="cap-body">
<div class="cap-name">MFA für alle Cloud-Apps</div>
<div class="cap-cond">Wenn: Außerhalb Firmennetz</div>
</div>
<span class="cap-mode">Grant</span>
</div>
<div class="ca-policy">
<div class="cap-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="cap-body">
<div class="cap-name">Compliant Device erforderlich</div>
<div class="cap-cond">Wenn: Zugriff auf SharePoint, Exchange</div>
</div>
<span class="cap-mode">Grant</span>
</div>
<div class="ca-policy blocked">
<div class="cap-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div>
<div class="cap-body">
<div class="cap-name">Legacy Auth blockieren</div>
<div class="cap-cond">Protokoll: IMAP, POP3, ActiveSync</div>
</div>
<span class="cap-mode" style="background:var(--red-soft);color:var(--red)">Block</span>
</div>
<div class="ca-policy report">
<div class="cap-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg></div>
<div class="cap-body">
<div class="cap-name">Risk-basiertes MFA</div>
<div class="cap-cond">Wenn: Sign-in Risk ≥ medium</div>
</div>
<span class="cap-mode">Report</span>
</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
Aktive Sessions
</div>
</div>
<div class="info-list">
<div class="info-row">
<div class="k">Geräte</div>
<div class="v">${u.stats.sessions} aktiv</div>
</div>
<div class="info-row">
<div class="k">Letzte Sign-in</div>
<div class="v">${esc(u.lastLogin)}</div>
</div>
<div class="info-row">
<div class="k">Sign-in Risk</div>
<div class="v"><span class="sec-status ok">${esc(u.risk)}</span></div>
</div>
<div class="info-row">
<div class="k">User Risk</div>
<div class="v"><span class="sec-status ok">Niedrig</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}
function fidoHtml(u) {
const keys = [
{ name: 'YubiKey 5C NFC', model: 'Yubico · USB-C / NFC', sn: 'YK-A7F3E2', added: '14. Mai 2026', last: 'vor 4 Min.', uses: 1247, primary: true },
{ name: 'YubiKey 5 NFC', model: 'Yubico · USB-A / NFC (Backup)', sn: 'YK-B92D11', added: '14. Mai 2026', last: 'vor 12 T.', uses: 38, primary: false }
].slice(0, u.stats.fido);
if (keys.length === 0) {
return `
<div class="tab-panel active">
<div class="empty-hint" style="padding:80px 24px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m15.5 7.5 3 3L22 7l-3-3M21 2l-9.6 9.6a5.5 5.5 0 1 1-2 2L19 4"/></svg>
<h3>Kein Sicherheitsschlüssel registriert</h3>
<p>Für diesen Benutzer wurde noch kein FIDO2-Schlüssel hinterlegt. Stark empfohlen für alle privilegierten Konten.</p>
<button class="btn primary" style="margin-top:16px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></svg>
Sicherheitsschlüssel hinzufügen
</button>
</div>
</div>`;
}
return `
<div class="tab-panel active">
<div class="fido-hero">
${keys.map(k => `
<div class="fido-key-card ${k.primary ? 'primary' : ''}">
<div class="fkc-head">
<div class="fkc-visual">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m15.5 7.5 3 3L22 7l-3-3M21 2l-9.6 9.6a5.5 5.5 0 1 1-2 2L19 4"/></svg>
</div>
<span class="fkc-status">${k.primary ? 'Primär' : 'Backup'}</span>
</div>
<div class="fkc-name">${esc(k.name)}</div>
<div class="fkc-sub">${esc(k.model)} · SN <span style="font-family:'JetBrains Mono',monospace;color:var(--text-2)">${esc(k.sn)}</span></div>
<div class="fkc-stats">
<div class="fkc-stat">
<div class="v">${k.uses.toLocaleString('de-DE')}</div>
<div class="l">Auth. gesamt</div>
</div>
<div class="fkc-stat">
<div class="v">${esc(k.last)}</div>
<div class="l">Letzte Nutzung</div>
</div>
</div>
</div>
`).join('')}
${keys.length < 3 ? `
<div class="add-key-card">
<div class="akc-plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></svg></div>
<div class="akc-title">Weiteren Schlüssel registrieren</div>
<div class="akc-sub">YubiKey, Titan, Feitian — WebAuthn</div>
</div>` : ''}
</div>
<div class="grid-2">
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
Authentifizierungs-Aktivität
</div>
<span class="card-link">Letzte 7 Tage</span>
</div>
<div class="info-list">
<div class="info-row">
<div class="k">Erfolgreiche Anmeldungen</div>
<div class="v"><b style="color:var(--green)">${42 + u.stats.fido * 3}</b> via FIDO2</div>
</div>
<div class="info-row">
<div class="k">Touch-Bestätigungen</div>
<div class="v">${38 + u.stats.fido * 3} · User Presence</div>
</div>
<div class="info-row">
<div class="k">PIN-Eingaben</div>
<div class="v">${4 + u.stats.fido} · User Verification</div>
</div>
<div class="info-row">
<div class="k">Fehlversuche</div>
<div class="v"><span style="color:var(--green)">0</span> blockiert · 0 Rate-Limit</div>
</div>
<div class="info-row">
<div class="k">Verwendete Browser</div>
<div class="v">Edge 124 · Safari 17 · Firefox 126</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Empfehlungen
</div>
</div>
<div class="sec-list">
<div class="sec-item">
<div class="sec-icon si-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="sec-body">
<div class="sec-label">Mindestens 2 Schlüssel registriert</div>
<div class="sec-sub">Primär &amp; Backup vorhanden</div>
</div>
<span class="sec-status ok">Erfüllt</span>
</div>
<div class="sec-item">
<div class="sec-icon si-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="sec-body">
<div class="sec-label">Schlüssel-Attestation aktiv</div>
<div class="sec-sub">Nur zertifizierte Authenticators</div>
</div>
<span class="sec-status ok">Erfüllt</span>
</div>
<div class="sec-item">
<div class="sec-icon si-warn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
<div class="sec-body">
<div class="sec-label">Passwortlose Anmeldung</div>
<div class="sec-sub">Optional — Passwort noch aktiv</div>
</div>
<span class="sec-status warn">Empfohlen</span>
</div>
</div>
</div>
</div>
</div>
`;
}
function lifecycleHtml(u) {
return `
<div class="tab-panel active">
<div class="lc-stage-bar">
<div class="lc-stage done">
<div class="lcs-num">01 · ABGESCHLOSSEN</div>
<div class="lcs-label">Pre-Onboarding</div>
<div class="lcs-date">— 7 Tage vor Start</div>
</div>
<div class="lc-stage done">
<div class="lcs-num">02 · ABGESCHLOSSEN</div>
<div class="lcs-label">Onboarding</div>
<div class="lcs-date">${esc(u.joined)}</div>
</div>
<div class="lc-stage current">
<div class="lcs-num">03 · LAUFEND</div>
<div class="lcs-label">Aktiv</div>
<div class="lcs-date">Seit ${esc(u.joined)}</div>
</div>
<div class="lc-stage">
<div class="lcs-num">04</div>
<div class="lcs-label">Rollenwechsel</div>
<div class="lcs-date">— optional</div>
</div>
<div class="lc-stage">
<div class="lcs-num">05</div>
<div class="lcs-label">Offboarding</div>
<div class="lcs-date">— offen</div>
</div>
</div>
<div class="lc-grid">
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Meilensteine &amp; Termine
</div>
<a class="card-link" href="#">Alle anzeigen</a>
</div>
<div class="milestone-list">
<div class="milestone">
<div class="ms-dot done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="ms-body">
<div class="ms-title">Vertrag unterzeichnet</div>
<div class="ms-sub">${esc(u.dept)} · ${esc(u.position)}</div>
</div>
<div class="ms-time">${esc(u.joined)}</div>
</div>
<div class="milestone">
<div class="ms-dot done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="ms-body">
<div class="ms-title">Hardware ausgehändigt</div>
<div class="ms-sub">${u.stats.assets} Geräte · Setup durch IT</div>
</div>
<div class="ms-time">${esc(u.joined)}</div>
</div>
<div class="milestone">
<div class="ms-dot done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="ms-body">
<div class="ms-title">IT-Schulung &amp; Security Awareness</div>
<div class="ms-sub">ISO 27001 · Phishing-Simulation bestanden</div>
</div>
<div class="ms-time">vor 90 T.</div>
</div>
<div class="milestone">
<div class="ms-dot done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><polyline points="20 6 9 17 4 12"/></svg></div>
<div class="ms-body">
<div class="ms-title">Probezeit abgeschlossen</div>
<div class="ms-sub">Übernahme durch ${esc(u.manager)}</div>
</div>
<div class="ms-time">vor 60 T.</div>
</div>
<div class="milestone">
<div class="ms-dot current"></div>
<div class="ms-body">
<div class="ms-title">Letzte Recertification der Berechtigungen</div>
<div class="ms-sub">Quartalsweise durch ${esc(u.manager)}</div>
</div>
<div class="ms-time">vor 14 T.</div>
</div>
<div class="milestone">
<div class="ms-dot upcoming"></div>
<div class="ms-body">
<div class="ms-title">Nächstes Mitarbeitergespräch</div>
<div class="ms-sub">Geplant mit ${esc(u.manager)}</div>
</div>
<div class="ms-time future">in 42 T.</div>
</div>
<div class="milestone">
<div class="ms-dot upcoming"></div>
<div class="ms-body">
<div class="ms-title">Hardware-Refresh fällig</div>
<div class="ms-sub">Notebook-Tausch nach 36 Monaten</div>
</div>
<div class="ms-time future">in 184 T.</div>
</div>
<div class="milestone">
<div class="ms-dot upcoming"></div>
<div class="ms-body">
<div class="ms-title">Security-Schulung Wiederholung</div>
<div class="ms-sub">Jährliche Pflichtschulung</div>
</div>
<div class="ms-time future">in 275 T.</div>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:14px">
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Vertrag &amp; Rolle
</div>
</div>
<div class="contract-grid">
<div class="kv-card">
<div class="kvc-label">Eintritt</div>
<div class="kvc-value">${esc(u.joined)}</div>
<div class="kvc-sub">Unbefristet</div>
</div>
<div class="kv-card">
<div class="kvc-label">Beschäftigung</div>
<div class="kvc-value">${u.role === 'extern' ? 'Externer Auftrag' : 'Vollzeit · 40h'}</div>
<div class="kvc-sub">${u.role === 'extern' ? 'Werkvertrag' : 'TVöD-äquiv.'}</div>
</div>
<div class="kv-card">
<div class="kvc-label">Standort</div>
<div class="kvc-value">${esc(u.location)}</div>
<div class="kvc-sub">${u.location === 'Remote' ? '100 % Homeoffice' : 'Hybrid 3:2'}</div>
</div>
<div class="kv-card">
<div class="kvc-label">Kostenstelle</div>
<div class="kvc-value">CC-${u.dept.split(' ')[0].toUpperCase().slice(0,3)}-${1000 + u.first.length * 7}</div>
<div class="kvc-sub">${esc(u.dept)}</div>
</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div class="card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Offboarding-Bereitschaft
</div>
<span class="sec-status off">Nicht aktiv</span>
</div>
<div class="info-list">
<div class="info-row">
<div class="k">Geplantes Austrittsdatum</div>
<div class="v" style="color:var(--text-4)">— Keines hinterlegt</div>
</div>
<div class="info-row">
<div class="k">Datenübergabe-Plan</div>
<div class="v">Automatisch · OneDrive, Mail-Archiv</div>
</div>
<div class="info-row">
<div class="k">Hardware-Rückgabe</div>
<div class="v">${u.stats.assets} Geräte registriert</div>
</div>
<div class="info-row">
<div class="k">Lizenzfreigabe</div>
<div class="v">${u.stats.lic} Lizenzen · ca. € ${(u.stats.lic * 24).toFixed(0)}/Monat</div>
</div>
<div class="info-row">
<div class="k">Wissenstransfer</div>
<div class="v" style="color:var(--text-4)">— Noch nicht initiiert</div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:14px">
<button class="btn" style="flex:1;justify-content:center">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
Austritt planen
</button>
<button class="btn" style="flex:1;justify-content:center">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
Lifecycle Export
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
/* ---- Init ---- */
$('#searchInput').addEventListener('input', (e) => {
state.search = e.target.value;
renderList();
});
$$('#filterRow .filter-pill').forEach(b => b.addEventListener('click', () => {
state.filter = b.dataset.role;
$$('#filterRow .filter-pill').forEach(x => x.classList.toggle('on', x === b));
renderList();
}));
renderList();
renderDetail();
</script>
</body>
</html>