1087 lines
50 KiB
HTML
1087 lines
50 KiB
HTML
{{define "base"}}
|
||
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||
<title>{{.Title}} - {{appName}}</title>
|
||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||
<!-- Preload icon font to prevent re-decode lag on tab restore -->
|
||
<link rel="preload" href="/static/css/fonts/tabler-icons.woff2?v3.6.0" as="font" type="font/woff2" crossorigin>
|
||
<!-- Resolve theme BEFORE loading CSS to prevent FOUC (flash of unstyled content) -->
|
||
<script>
|
||
(function() {
|
||
var theme = '{{with .User}}{{.Theme}}{{end}}' || 'ocean-auto';
|
||
// Map legacy default values to ocean
|
||
if (theme === 'auto' || theme === 'light' || theme === 'dark') {
|
||
theme = 'ocean-' + theme;
|
||
}
|
||
window.__kwThemeRaw = theme;
|
||
var pair = 'default', mode = theme;
|
||
if (theme !== 'auto' && theme !== 'light' && theme !== 'dark') {
|
||
var idx = theme.lastIndexOf('-');
|
||
if (idx > 0) { pair = theme.substring(0, idx); mode = theme.substring(idx + 1); }
|
||
}
|
||
if (mode !== 'light' && mode !== 'dark') {
|
||
mode = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||
}
|
||
document.documentElement.setAttribute('data-bs-theme', mode);
|
||
document.documentElement.style.colorScheme = mode;
|
||
if (pair !== 'default') document.documentElement.setAttribute('data-theme-pair', pair);
|
||
})();
|
||
</script>
|
||
<style>
|
||
/* Critical inline styles: prevent white flash between page navigations */
|
||
html[data-bs-theme="dark"],
|
||
html[data-bs-theme="dark"] body { background-color: #0c1a2a; color-scheme: dark; }
|
||
html[data-bs-theme="light"],
|
||
html[data-bs-theme="light"] body { background-color: #ecfeff; color-scheme: light; }
|
||
.navbar-brand-image { height: 2rem; }
|
||
.keywarden-brand { font-weight: 700; font-size: 1.25rem; color: #206bc4; }
|
||
[data-bs-theme="dark"] .keywarden-brand { color: #4da3ff; }
|
||
/* Text selection colors */
|
||
[data-bs-theme="dark"] ::selection { background: #3d6098; color: #f0f4f8; }
|
||
[data-bs-theme="dark"] ::-moz-selection { background: #3d6098; color: #f0f4f8; }
|
||
[data-bs-theme="light"] ::selection { background: #b3d4fc; color: #1a1a1a; }
|
||
[data-bs-theme="light"] ::-moz-selection { background: #b3d4fc; color: #1a1a1a; }
|
||
|
||
/* Consistent spacing between Tabler icons and adjacent text */
|
||
i.ti { margin-right: 0.25em; }
|
||
.btn-icon > i.ti, .input-icon-addon > i.ti, .nav-link-icon > i.ti { margin-right: 0; }
|
||
|
||
/* ══ PAGE LAYOUT: full-width header on top, sidebar + content below ══ */
|
||
.page {
|
||
display: flex !important;
|
||
flex-direction: column !important;
|
||
min-height: 100vh;
|
||
}
|
||
.page-content-row {
|
||
display: flex;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ── Full-width top header (glass) ── */
|
||
header.navbar.keywarden-top-header {
|
||
background: rgba(29, 43, 56, 0.82) !important;
|
||
backdrop-filter: blur(20px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||
flex-shrink: 0;
|
||
z-index: 1030;
|
||
}
|
||
[data-bs-theme="light"] header.navbar.keywarden-top-header {
|
||
background: rgba(29, 43, 56, 0.82) !important;
|
||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||
}
|
||
header.navbar.keywarden-top-header .nav-link { color: #c8d6e5 !important; }
|
||
header.navbar.keywarden-top-header .nav-link .text-secondary { color: #8fa8c8 !important; }
|
||
header.navbar.keywarden-top-header .fw-bold { color: #e8eef5; }
|
||
|
||
/* ── Header brand area (left side, aligned with sidebar) ── */
|
||
.keywarden-header-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0 0.5rem;
|
||
white-space: nowrap;
|
||
}
|
||
.keywarden-header-brand .keywarden-brand { color: #c8d6e5; font-size: 1.15rem; }
|
||
.keywarden-header-brand .keywarden-brand:hover { color: #fff; text-decoration: none; }
|
||
.keywarden-header-brand .keywarden-brand i.ti { color: #4da3ff; font-size: 1.3rem; }
|
||
|
||
/* ── Angular header buttons ── */
|
||
header.navbar.keywarden-top-header .btn-header-modern {
|
||
color: #c8d6e5;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 2px solid rgba(255,255,255,0.18);
|
||
border-radius: 4px;
|
||
padding: 0.35rem 1rem;
|
||
font-size: 0.8125rem;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
header.navbar.keywarden-top-header .btn-header-modern:hover {
|
||
color: #fff;
|
||
background: rgba(255,255,255,0.14);
|
||
border-color: rgba(255,255,255,0.32);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||
}
|
||
header.navbar.keywarden-top-header .btn-header-modern.btn-icon {
|
||
width: 2.16875rem;
|
||
height: 2.16875rem;
|
||
padding: 0;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* ── Sidebar (vertical, below header – glass) ── */
|
||
.navbar-vertical {
|
||
background: rgba(29, 43, 56, 0.82) !important;
|
||
backdrop-filter: blur(20px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||
flex-shrink: 0;
|
||
overflow: hidden;
|
||
align-self: stretch;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
[data-bs-theme="dark"] .navbar-vertical { background: rgba(10, 17, 32, 0.82) !important; }
|
||
[data-bs-theme="dark"] header.navbar.keywarden-top-header { background: rgba(10, 17, 32, 0.82) !important; border-bottom-color: rgba(255,255,255,0.04); }
|
||
.navbar-vertical > .container-fluid {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.navbar-vertical .navbar-collapse {
|
||
flex: 1;
|
||
display: flex !important;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.navbar-vertical .navbar-nav {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
[data-bs-theme="light"] .navbar-vertical { background: rgba(29, 43, 56, 0.82) !important; }
|
||
|
||
/* ── Page content area ── */
|
||
.page-wrapper {
|
||
flex: 1;
|
||
min-width: 0;
|
||
overflow-y: auto;
|
||
margin-left: 0 !important;
|
||
}
|
||
[data-bs-theme="dark"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 10%, rgba(6, 182, 212, 0.07) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 85% 60%, rgba(99, 102, 241, 0.06) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 50% 90%, rgba(168, 85, 247, 0.04) 0%, transparent 50%),
|
||
#0F1829;
|
||
}
|
||
[data-bs-theme="dark"] .page-body {
|
||
background: transparent;
|
||
}
|
||
[data-bs-theme="light"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 10%, rgba(6, 182, 212, 0.10) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 85% 60%, rgba(99, 102, 241, 0.08) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 50% 90%, rgba(168, 85, 247, 0.06) 0%, transparent 50%),
|
||
#f1f5f9;
|
||
}
|
||
[data-bs-theme="light"] .page-body {
|
||
background: transparent;
|
||
}
|
||
html[data-bs-theme="dark"],
|
||
html[data-bs-theme="dark"] body { background-color: #0F1829 !important; }
|
||
/* content-visibility removed – causes Firefox to freeze/re-layout on tab hover */
|
||
|
||
/* ── Narrower dashboard stat cards ── */
|
||
.stat-card-narrow { max-width: 220px; }
|
||
|
||
/* ── Desktop: sidebar always visible ── */
|
||
@media (min-width: 992px) {
|
||
.navbar-vertical {
|
||
width: 256px !important;
|
||
min-width: 256px !important;
|
||
position: relative !important;
|
||
top: auto !important;
|
||
bottom: auto !important;
|
||
left: auto !important;
|
||
}
|
||
.navbar-vertical .container-fluid {
|
||
padding-left: 0.75rem;
|
||
padding-right: 0.75rem;
|
||
}
|
||
.navbar-vertical .nav-link-title {
|
||
opacity: 1;
|
||
white-space: nowrap;
|
||
pointer-events: auto;
|
||
width: auto;
|
||
}
|
||
.navbar-vertical .nav-link {
|
||
justify-content: start;
|
||
padding-left: 0.75rem;
|
||
padding-right: 0.75rem;
|
||
}
|
||
.navbar-vertical .nav-link-icon {
|
||
margin-right: 0.5rem;
|
||
width: 24px;
|
||
min-width: 24px;
|
||
text-align: center;
|
||
}
|
||
.navbar-vertical .nav-category {
|
||
opacity: 1;
|
||
height: auto;
|
||
padding: 0.6rem 0.75rem 0.25rem;
|
||
}
|
||
.navbar-vertical .nav-item + .nav-category,
|
||
.navbar-vertical .nav-category + .nav-category {
|
||
margin-top: 0.4rem;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
padding-top: 0.65rem;
|
||
}
|
||
.navbar-vertical .nav-category:first-child {
|
||
padding-top: 0;
|
||
}
|
||
/* Hide mobile burger button on desktop */
|
||
.mobile-menu-toggle {
|
||
display: none !important;
|
||
}
|
||
.page-wrapper {
|
||
padding-left: 0.75rem;
|
||
}
|
||
}
|
||
|
||
/* ── Mobile: off-canvas sidebar ── */
|
||
@media (max-width: 991.98px) {
|
||
aside.navbar-vertical {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
width: 280px !important;
|
||
max-width: 85vw;
|
||
z-index: 1050;
|
||
transform: translateX(-100%);
|
||
transition: transform 0.3s ease;
|
||
overflow-y: auto;
|
||
padding-top: 3.5rem;
|
||
}
|
||
aside.navbar-vertical.mobile-open {
|
||
transform: translateX(0);
|
||
}
|
||
aside.navbar-vertical .navbar-collapse {
|
||
display: flex !important;
|
||
flex-direction: column;
|
||
padding: 0.5rem 0;
|
||
}
|
||
aside.navbar-vertical .container-fluid {
|
||
padding-left: 0.75rem;
|
||
padding-right: 0.75rem;
|
||
}
|
||
aside.navbar-vertical .nav-link-title {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
width: auto;
|
||
}
|
||
aside.navbar-vertical .nav-link {
|
||
justify-content: start;
|
||
padding-left: 0.75rem;
|
||
padding-right: 0.75rem;
|
||
}
|
||
aside.navbar-vertical .nav-link-icon {
|
||
margin-right: 0.5rem;
|
||
}
|
||
aside.navbar-vertical .nav-category {
|
||
opacity: 1;
|
||
height: auto;
|
||
padding: 0.6rem 0.75rem 0.25rem;
|
||
}
|
||
/* Backdrop overlay */
|
||
.mobile-sidebar-backdrop {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1040;
|
||
}
|
||
.mobile-sidebar-backdrop.show {
|
||
display: block;
|
||
}
|
||
/* Mobile burger button */
|
||
.mobile-menu-toggle {
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.10);
|
||
color: #8fa8c8;
|
||
border-radius: 6px;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
padding: 0;
|
||
}
|
||
.mobile-menu-toggle:hover {
|
||
background: rgba(255,255,255,0.16);
|
||
color: #fff;
|
||
}
|
||
.mobile-menu-toggle .ti {
|
||
font-size: 1rem;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
/* ── Sidebar category headers ── */
|
||
.nav-category {
|
||
display: block;
|
||
padding: 0.6rem 0.75rem 0.25rem;
|
||
font-size: 0.65rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
color: rgba(138, 166, 200, 0.55);
|
||
white-space: nowrap;
|
||
user-select: none;
|
||
pointer-events: none;
|
||
}
|
||
.nav-category:first-child {
|
||
padding-top: 0;
|
||
}
|
||
/* Show a subtle divider line above categories (except the first) */
|
||
.nav-category + .nav-category,
|
||
.nav-item + .nav-category {
|
||
margin-top: 0.4rem;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||
padding-top: 0.65rem;
|
||
}
|
||
|
||
/* ── Avatar in dropdown ── */
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════ */
|
||
/* ADDITIONAL THEME PAIRS */
|
||
/* ═══════════════════════════════════════════════════════════ */
|
||
|
||
/* Shared themed overrides (active when any theme pair is set) */
|
||
html[data-theme-pair] .btn-primary {
|
||
--tblr-btn-bg: var(--kw-primary);
|
||
--tblr-btn-border-color: var(--kw-primary);
|
||
--tblr-btn-hover-bg: var(--kw-primary-hover);
|
||
--tblr-btn-hover-border-color: var(--kw-primary-hover);
|
||
--tblr-btn-active-bg: var(--kw-primary-active);
|
||
--tblr-btn-active-border-color: var(--kw-primary-active);
|
||
}
|
||
html[data-theme-pair] .btn-outline-primary {
|
||
--tblr-btn-color: var(--kw-primary);
|
||
--tblr-btn-border-color: var(--kw-primary);
|
||
--tblr-btn-hover-bg: var(--kw-primary);
|
||
--tblr-btn-hover-border-color: var(--kw-primary);
|
||
--tblr-btn-active-bg: var(--kw-primary-hover);
|
||
--tblr-btn-active-border-color: var(--kw-primary-hover);
|
||
}
|
||
html[data-theme-pair] {
|
||
--tblr-link-color: var(--kw-primary);
|
||
--tblr-link-hover-color: var(--kw-primary-hover);
|
||
}
|
||
html[data-theme-pair] .bg-primary-lt {
|
||
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
|
||
}
|
||
html[data-theme-pair] .alert-primary {
|
||
--tblr-alert-color: var(--kw-primary);
|
||
--tblr-alert-bg: rgba(var(--tblr-primary-rgb), 0.07);
|
||
--tblr-alert-border-color: rgba(var(--tblr-primary-rgb), 0.15);
|
||
}
|
||
html[data-theme-pair] .form-check-input:checked {
|
||
background-color: var(--kw-primary);
|
||
border-color: var(--kw-primary);
|
||
}
|
||
html[data-theme-pair] .form-select:focus,
|
||
html[data-theme-pair] .form-control:focus {
|
||
border-color: var(--kw-primary);
|
||
box-shadow: 0 0 0 0.25rem rgba(var(--tblr-primary-rgb), 0.25);
|
||
}
|
||
html[data-theme-pair] .nav-tabs .nav-link.active {
|
||
border-bottom-color: var(--kw-primary);
|
||
}
|
||
/* Override Tabler hardcoded blue badges to use theme accent */
|
||
html[data-theme-pair] .bg-blue-lt {
|
||
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
html[data-theme-pair] .bg-azure-lt {
|
||
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
html[data-theme-pair] .bg-cyan-lt {
|
||
background-color: rgba(var(--tblr-primary-rgb), 0.1) !important;
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
html[data-theme-pair] .text-primary {
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
html[data-theme-pair] .text-blue {
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
html[data-theme-pair] .text-azure {
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
html[data-theme-pair] .text-cyan {
|
||
color: var(--kw-primary) !important;
|
||
}
|
||
|
||
/* ── Ocean Theme ── */
|
||
html[data-theme-pair="ocean"] {
|
||
--kw-primary: #0891b2; --kw-primary-hover: #0e7490; --kw-primary-active: #155e75;
|
||
--tblr-primary: #0891b2; --tblr-primary-rgb: 8,145,178;
|
||
}
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] {
|
||
--kw-primary: #06b6d4; --kw-primary-hover: #0891b2; --kw-primary-active: #0e7490;
|
||
--tblr-primary: #06b6d4; --tblr-primary-rgb: 6,182,212;
|
||
--tblr-bg-surface: #0f2035; --tblr-bg-surface-secondary: #122840;
|
||
--tblr-bg-surface-tertiary: #0c1e30; --tblr-bg-surface-dark: #0c1a2a;
|
||
--tblr-bg-forms: #0c1e30; --tblr-body-bg: #0c1a2a; --tblr-body-bg-rgb: 12,26,42;
|
||
--tblr-border-color: #1a3555; --tblr-border-color-translucent: rgba(6, 182, 212, 0.12);
|
||
}
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"],
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"] body { background-color: #ecfeff !important; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"],
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] body { background-color: #0c1a2a !important; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"] .navbar-vertical,
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: rgba(21, 94, 117, 0.82) !important; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] .navbar-vertical,
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: rgba(7, 18, 32, 0.82) !important; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 10%, rgba(6, 182, 212, 0.12) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 85% 60%, rgba(14, 116, 144, 0.08) 0%, transparent 50%),
|
||
#ecfeff;
|
||
}
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 10%, rgba(6, 182, 212, 0.09) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 85% 60%, rgba(14, 116, 144, 0.06) 0%, transparent 50%),
|
||
#0c1a2a;
|
||
}
|
||
html[data-theme-pair="ocean"] .page-body { background: transparent; }
|
||
html[data-theme-pair="ocean"] .keywarden-header-brand .keywarden-brand i.ti { color: #22d3ee; }
|
||
html[data-theme-pair="ocean"] .nav-category { color: rgba(160, 220, 230, 0.6); }
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"] ::selection { background: #a5f3fc; color: #155e75; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="light"] ::-moz-selection { background: #a5f3fc; color: #155e75; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] ::selection { background: #164e63; color: #ecfeff; }
|
||
html[data-theme-pair="ocean"][data-bs-theme="dark"] ::-moz-selection { background: #164e63; color: #ecfeff; }
|
||
|
||
/* ── Forest Theme ── */
|
||
html[data-theme-pair="forest"] {
|
||
--kw-primary: #16a34a; --kw-primary-hover: #15803d; --kw-primary-active: #166534;
|
||
--tblr-primary: #16a34a; --tblr-primary-rgb: 22,163,74;
|
||
}
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] {
|
||
--kw-primary: #4ade80; --kw-primary-hover: #22c55e; --kw-primary-active: #16a34a;
|
||
--tblr-primary: #4ade80; --tblr-primary-rgb: 74,222,128;
|
||
--tblr-bg-surface: #0f2216; --tblr-bg-surface-secondary: #122a1b;
|
||
--tblr-bg-surface-tertiary: #0c1d12; --tblr-bg-surface-dark: #0a1a10;
|
||
--tblr-bg-forms: #0c1d12; --tblr-body-bg: #0a1a10; --tblr-body-bg-rgb: 10,26,16;
|
||
--tblr-border-color: #1a3524; --tblr-border-color-translucent: rgba(74, 222, 128, 0.10);
|
||
}
|
||
html[data-theme-pair="forest"][data-bs-theme="light"],
|
||
html[data-theme-pair="forest"][data-bs-theme="light"] body { background-color: #f0fdf4 !important; }
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"],
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] body { background-color: #0a1a10 !important; }
|
||
html[data-theme-pair="forest"][data-bs-theme="light"] .navbar-vertical,
|
||
html[data-theme-pair="forest"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: rgba(20, 83, 45, 0.82) !important; }
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] .navbar-vertical,
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: rgba(6, 18, 9, 0.82) !important; }
|
||
html[data-theme-pair="forest"][data-bs-theme="light"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 20% 15%, rgba(74, 222, 128, 0.12) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 70%, rgba(22, 163, 74, 0.08) 0%, transparent 50%),
|
||
#f0fdf4;
|
||
}
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 20% 15%, rgba(74, 222, 128, 0.08) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 70%, rgba(22, 163, 74, 0.05) 0%, transparent 50%),
|
||
#0a1a10;
|
||
}
|
||
html[data-theme-pair="forest"] .page-body { background: transparent; }
|
||
html[data-theme-pair="forest"] .keywarden-header-brand .keywarden-brand i.ti { color: #4ade80; }
|
||
html[data-theme-pair="forest"] .nav-category { color: rgba(160, 210, 170, 0.6); }
|
||
html[data-theme-pair="forest"][data-bs-theme="light"] ::selection { background: #bbf7d0; color: #14532d; }
|
||
html[data-theme-pair="forest"][data-bs-theme="light"] ::-moz-selection { background: #bbf7d0; color: #14532d; }
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] ::selection { background: #166534; color: #f0fdf4; }
|
||
html[data-theme-pair="forest"][data-bs-theme="dark"] ::-moz-selection { background: #166534; color: #f0fdf4; }
|
||
|
||
/* ── Sunset Theme ── */
|
||
html[data-theme-pair="sunset"] {
|
||
--kw-primary: #d97706; --kw-primary-hover: #b45309; --kw-primary-active: #92400e;
|
||
--tblr-primary: #d97706; --tblr-primary-rgb: 217,119,6;
|
||
}
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] {
|
||
--kw-primary: #f59e0b; --kw-primary-hover: #d97706; --kw-primary-active: #b45309;
|
||
--tblr-primary: #f59e0b; --tblr-primary-rgb: 245,158,11;
|
||
--tblr-bg-surface: #221a0e; --tblr-bg-surface-secondary: #281f12;
|
||
--tblr-bg-surface-tertiary: #1e170a; --tblr-bg-surface-dark: #1a1408;
|
||
--tblr-bg-forms: #1e170a; --tblr-body-bg: #1a1408; --tblr-body-bg-rgb: 26,20,8;
|
||
--tblr-border-color: #3a2c14; --tblr-border-color-translucent: rgba(245, 158, 11, 0.10);
|
||
}
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"],
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"] body { background-color: #fffbeb !important; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"],
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] body { background-color: #1a1408 !important; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"] .navbar-vertical,
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: rgba(120, 53, 15, 0.82) !important; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] .navbar-vertical,
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: rgba(17, 13, 4, 0.82) !important; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 20%, rgba(245, 158, 11, 0.12) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 60%, rgba(217, 119, 6, 0.08) 0%, transparent 50%),
|
||
#fffbeb;
|
||
}
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 20%, rgba(245, 158, 11, 0.08) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 60%, rgba(217, 119, 6, 0.05) 0%, transparent 50%),
|
||
#1a1408;
|
||
}
|
||
html[data-theme-pair="sunset"] .page-body { background: transparent; }
|
||
html[data-theme-pair="sunset"] .keywarden-header-brand .keywarden-brand i.ti { color: #fbbf24; }
|
||
html[data-theme-pair="sunset"] .nav-category { color: rgba(220, 190, 150, 0.6); }
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"] ::selection { background: #fde68a; color: #78350f; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="light"] ::-moz-selection { background: #fde68a; color: #78350f; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] ::selection { background: #92400e; color: #fffbeb; }
|
||
html[data-theme-pair="sunset"][data-bs-theme="dark"] ::-moz-selection { background: #92400e; color: #fffbeb; }
|
||
|
||
/* ── Rose Theme ── */
|
||
html[data-theme-pair="rose"] {
|
||
--kw-primary: #db2777; --kw-primary-hover: #be185d; --kw-primary-active: #9d174d;
|
||
--tblr-primary: #db2777; --tblr-primary-rgb: 219,39,119;
|
||
}
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] {
|
||
--kw-primary: #f472b6; --kw-primary-hover: #ec4899; --kw-primary-active: #db2777;
|
||
--tblr-primary: #f472b6; --tblr-primary-rgb: 244,114,182;
|
||
--tblr-bg-surface: #22101a; --tblr-bg-surface-secondary: #281420;
|
||
--tblr-bg-surface-tertiary: #1e0c16; --tblr-bg-surface-dark: #1a0a14;
|
||
--tblr-bg-forms: #1e0c16; --tblr-body-bg: #1a0a14; --tblr-body-bg-rgb: 26,10,20;
|
||
--tblr-border-color: #3a1a2c; --tblr-border-color-translucent: rgba(244, 114, 182, 0.10);
|
||
}
|
||
html[data-theme-pair="rose"][data-bs-theme="light"],
|
||
html[data-theme-pair="rose"][data-bs-theme="light"] body { background-color: #fdf2f8 !important; }
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"],
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] body { background-color: #1a0a14 !important; }
|
||
html[data-theme-pair="rose"][data-bs-theme="light"] .navbar-vertical,
|
||
html[data-theme-pair="rose"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: rgba(131, 24, 67, 0.82) !important; }
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] .navbar-vertical,
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: rgba(18, 6, 14, 0.82) !important; }
|
||
html[data-theme-pair="rose"][data-bs-theme="light"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 20% 15%, rgba(244, 114, 182, 0.12) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 70%, rgba(219, 39, 119, 0.08) 0%, transparent 50%),
|
||
#fdf2f8;
|
||
}
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 20% 15%, rgba(244, 114, 182, 0.08) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 70%, rgba(219, 39, 119, 0.05) 0%, transparent 50%),
|
||
#1a0a14;
|
||
}
|
||
html[data-theme-pair="rose"] .page-body { background: transparent; }
|
||
html[data-theme-pair="rose"] .keywarden-header-brand .keywarden-brand i.ti { color: #f472b6; }
|
||
html[data-theme-pair="rose"] .nav-category { color: rgba(220, 160, 190, 0.6); }
|
||
html[data-theme-pair="rose"][data-bs-theme="light"] ::selection { background: #fbcfe8; color: #831843; }
|
||
html[data-theme-pair="rose"][data-bs-theme="light"] ::-moz-selection { background: #fbcfe8; color: #831843; }
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] ::selection { background: #9f1239; color: #fdf2f8; }
|
||
html[data-theme-pair="rose"][data-bs-theme="dark"] ::-moz-selection { background: #9f1239; color: #fdf2f8; }
|
||
|
||
/* ── Nord Theme ── */
|
||
html[data-theme-pair="nord"] {
|
||
--kw-primary: #5e81ac; --kw-primary-hover: #4c6e96; --kw-primary-active: #3b5b80;
|
||
--tblr-primary: #5e81ac; --tblr-primary-rgb: 94,129,172;
|
||
}
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] {
|
||
--kw-primary: #88c0d0; --kw-primary-hover: #6eb0c2; --kw-primary-active: #5e9fb4;
|
||
--tblr-primary: #88c0d0; --tblr-primary-rgb: 136,192,208;
|
||
--tblr-bg-surface: #242830; --tblr-bg-surface-secondary: #2a2e36;
|
||
--tblr-bg-surface-tertiary: #21252c; --tblr-bg-surface-dark: #1e2128;
|
||
--tblr-bg-forms: #21252c; --tblr-body-bg: #1e2128; --tblr-body-bg-rgb: 30,33,40;
|
||
--tblr-border-color: #353a44; --tblr-border-color-translucent: rgba(136, 192, 208, 0.10);
|
||
}
|
||
html[data-theme-pair="nord"][data-bs-theme="light"],
|
||
html[data-theme-pair="nord"][data-bs-theme="light"] body { background-color: #eceff4 !important; }
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"],
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] body { background-color: #1e2128 !important; }
|
||
html[data-theme-pair="nord"][data-bs-theme="light"] .navbar-vertical,
|
||
html[data-theme-pair="nord"][data-bs-theme="light"] header.navbar.keywarden-top-header { background: rgba(46, 52, 64, 0.82) !important; }
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] .navbar-vertical,
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] header.navbar.keywarden-top-header { background: rgba(20, 23, 28, 0.82) !important; }
|
||
html[data-theme-pair="nord"][data-bs-theme="light"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 10%, rgba(136, 192, 208, 0.12) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 60%, rgba(94, 129, 172, 0.08) 0%, transparent 50%),
|
||
#eceff4;
|
||
}
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] .page-wrapper {
|
||
background:
|
||
radial-gradient(ellipse at 15% 10%, rgba(136, 192, 208, 0.08) 0%, transparent 50%),
|
||
radial-gradient(ellipse at 80% 60%, rgba(94, 129, 172, 0.05) 0%, transparent 50%),
|
||
#1e2128;
|
||
}
|
||
html[data-theme-pair="nord"] .page-body { background: transparent; }
|
||
html[data-theme-pair="nord"] .keywarden-header-brand .keywarden-brand i.ti { color: #88c0d0; }
|
||
html[data-theme-pair="nord"] .nav-category { color: rgba(160, 180, 200, 0.6); }
|
||
html[data-theme-pair="nord"][data-bs-theme="light"] ::selection { background: #d8dee9; color: #2e3440; }
|
||
html[data-theme-pair="nord"][data-bs-theme="light"] ::-moz-selection { background: #d8dee9; color: #2e3440; }
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] ::selection { background: #434c5e; color: #eceff4; }
|
||
html[data-theme-pair="nord"][data-bs-theme="dark"] ::-moz-selection { background: #434c5e; color: #eceff4; }
|
||
|
||
/* ═══════════════════════════════════════════════════════════ */
|
||
/* GLASSMORPHISM */
|
||
/* ═══════════════════════════════════════════════════════════ */
|
||
|
||
/* ── Glass Cards ── */
|
||
.page-wrapper .card {
|
||
background: rgba(255, 255, 255, 0.45) !important;
|
||
backdrop-filter: blur(20px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||
border: 1px solid rgba(255, 255, 255, 0.5) !important;
|
||
box-shadow:
|
||
0 4px 24px rgba(0, 0, 0, 0.06),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||
transition: box-shadow 0.25s ease, border-color 0.25s ease;
|
||
}
|
||
.page-wrapper .card:hover {
|
||
box-shadow:
|
||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||
border-color: rgba(255, 255, 255, 0.6) !important;
|
||
}
|
||
[data-bs-theme="dark"] .page-wrapper .card {
|
||
background: rgba(15, 24, 41, 0.45) !important;
|
||
border: 1px solid rgba(255, 255, 255, 0.10) !important;
|
||
box-shadow:
|
||
0 4px 24px rgba(0, 0, 0, 0.3),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||
}
|
||
[data-bs-theme="dark"] .page-wrapper .card:hover {
|
||
box-shadow:
|
||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
border-color: rgba(255, 255, 255, 0.15) !important;
|
||
}
|
||
.page-wrapper .card .form-control,
|
||
.page-wrapper .card .form-select {
|
||
background: rgba(255, 255, 255, 0.5);
|
||
border-color: rgba(0, 0, 0, 0.1);
|
||
}
|
||
[data-bs-theme="dark"] .page-wrapper .card .form-control,
|
||
[data-bs-theme="dark"] .page-wrapper .card .form-select {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-color: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
/* ── Glass Dropdown Menus ── */
|
||
.dropdown-menu {
|
||
backdrop-filter: blur(16px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||
background: rgba(255, 255, 255, 0.88) !important;
|
||
border: 1px solid rgba(255, 255, 255, 0.4) !important;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
}
|
||
[data-bs-theme="dark"] .dropdown-menu {
|
||
background: rgba(15, 24, 41, 0.88) !important;
|
||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
/* ── Glass Modal ── */
|
||
.modal-content {
|
||
backdrop-filter: blur(16px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||
background: rgba(255, 255, 255, 0.88) !important;
|
||
border: 1px solid rgba(255, 255, 255, 0.4) !important;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
}
|
||
[data-bs-theme="dark"] .modal-content {
|
||
background: rgba(15, 24, 41, 0.88) !important;
|
||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||
}
|
||
</style>
|
||
<!-- Tabler CSS (self-hosted to prevent FOUC) -->
|
||
<link rel="stylesheet" href="/static/css/tabler.min.css">
|
||
<link rel="stylesheet" href="/static/css/tabler-icons.min.css">
|
||
</head>
|
||
<body class="layout-fluid">
|
||
<div class="page">
|
||
<!-- ═══ FULL-WIDTH TOP HEADER ═══ -->
|
||
<header class="navbar d-print-none keywarden-top-header" data-bs-theme="dark">
|
||
<div class="container-fluid px-3">
|
||
<div class="d-flex align-items-center w-100">
|
||
<!-- Logo + Brand (left side, aligned with sidebar) -->
|
||
<div class="keywarden-header-brand">
|
||
<button class="mobile-menu-toggle d-lg-none" onclick="toggleMobileMenu()" title="Menü" id="mobile-menu-btn">
|
||
<i class="ti ti-menu-2" id="mobile-menu-icon"></i>
|
||
</button>
|
||
<a href="/dashboard" class="keywarden-brand text-decoration-none d-flex align-items-center">
|
||
<i class="ti ti-key"></i> {{appName}}
|
||
</a>
|
||
</div>
|
||
<!-- Spacer -->
|
||
<div class="flex-grow-1"></div>
|
||
<!-- Update Available Badge (Admin/Owner only) -->
|
||
{{with .User}}{{if and (updateAvailable) (or (eq .Role "admin") (eq .Role "owner"))}}
|
||
<div class="nav-item d-none d-md-flex me-2">
|
||
<a href="{{releaseURL}}" class="btn btn-header-modern btn-sm" target="_blank" rel="noopener noreferrer" title="Update verfügbar: {{latestVersion}} (aktuell: {{appVersion}})">
|
||
<i class="ti ti-download" style="color: #fbbf24;"></i>
|
||
<span style="color: #fbbf24;">Update {{latestVersion}}</span>
|
||
</a>
|
||
</div>
|
||
{{end}}{{end}}
|
||
<!-- Repository Link -->
|
||
<div class="nav-item d-none d-md-flex me-2">
|
||
<a href="https://git.techniverse.net/scriptos/keywarden" class="btn btn-header-modern btn-sm" target="_blank" rel="noopener noreferrer" title="Source code on Gitea">
|
||
<i class="ti ti-brand-git"></i> Repository
|
||
</a>
|
||
</div>
|
||
<!-- Documentation -->
|
||
<div class="nav-item d-none d-md-flex me-2">
|
||
<a href="https://git.techniverse.net/scriptos/keywarden/src/branch/master/docs" class="btn btn-header-modern btn-sm" target="_blank" rel="noopener noreferrer" title="Documentation">
|
||
<i class="ti ti-book"></i> Docs
|
||
</a>
|
||
</div>
|
||
<!-- Theme Toggle -->
|
||
<div class="nav-item d-flex me-2">
|
||
<button id="theme-toggle" class="btn btn-header-modern btn-sm btn-icon" title="Toggle theme" onclick="toggleTheme()">
|
||
<i class="ti ti-sun" id="theme-icon"></i>
|
||
</button>
|
||
</div>
|
||
<!-- User Badge -->
|
||
{{with .User}}
|
||
<div class="nav-item dropdown">
|
||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||
<span class="avatar avatar-sm rounded-circle bg-primary-lt">
|
||
{{if .AvatarBase64}}
|
||
<img src="/avatar/{{.ID}}" class="avatar-img" alt="Avatar">
|
||
{{else}}
|
||
<i class="ti ti-user" style="font-size: 1.1rem;"></i>
|
||
{{end}}
|
||
</span>
|
||
<div class="d-none d-xl-block ps-2">
|
||
<div class="fw-bold">{{.Username}}</div>
|
||
<div class="mt-1 small text-secondary">{{if eq .Role "owner"}}Owner{{else if eq .Role "admin"}}Administrator{{else}}User{{end}}</div>
|
||
</div>
|
||
</a>
|
||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||
<a class="dropdown-item text-danger" href="/logout">
|
||
<i class="ti ti-logout"></i> Logout
|
||
</a>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</header>
|
||
<!-- ═══ CONTENT ROW: sidebar + main ═══ -->
|
||
<div class="page-content-row">
|
||
<!-- Mobile sidebar backdrop -->
|
||
<div class="mobile-sidebar-backdrop" id="mobile-sidebar-backdrop" onclick="closeMobileMenu()"></div>
|
||
<!-- Sidebar -->
|
||
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark" id="keywarden-sidebar">
|
||
<div class="container-fluid">
|
||
<div class="collapse navbar-collapse" id="sidebar-menu">
|
||
<ul class="navbar-nav pt-lg-3">
|
||
<!-- ── Overview ── -->
|
||
<li class="nav-category">Overview</li>
|
||
<li class="nav-item{{if eq .Active "dashboard"}} active{{end}}">
|
||
<a class="nav-link" href="/dashboard">
|
||
<span class="nav-link-icon"><i class="ti ti-dashboard"></i></span>
|
||
<span class="nav-link-title">Dashboard</span>
|
||
</a>
|
||
</li>
|
||
<!-- ── Infrastructure (Admin/Owner only) ── -->
|
||
{{with .User}}
|
||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||
<li class="nav-category">Infrastructure</li>
|
||
<li class="nav-item{{if eq $.Active "servers"}} active{{end}}">
|
||
<a class="nav-link" href="/servers">
|
||
<span class="nav-link-icon"><i class="ti ti-server"></i></span>
|
||
<span class="nav-link-title">Hosts</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item{{if eq $.Active "groups"}} active{{end}}">
|
||
<a class="nav-link" href="/groups">
|
||
<span class="nav-link-icon"><i class="ti ti-folders"></i></span>
|
||
<span class="nav-link-title">Groups</span>
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
<!-- ── Key Management ── -->
|
||
<li class="nav-category">Key Management</li>
|
||
<li class="nav-item{{if eq .Active "keys"}} active{{end}}">
|
||
<a class="nav-link" href="/keys">
|
||
<span class="nav-link-icon"><i class="ti ti-key"></i></span>
|
||
<span class="nav-link-title">SSH Keys</span>
|
||
</a>
|
||
</li>
|
||
<!-- My Access (visible to all users) -->
|
||
<li class="nav-item{{if eq .Active "my_access"}} active{{end}}">
|
||
<a class="nav-link" href="/my/access">
|
||
<span class="nav-link-icon"><i class="ti ti-shield-check"></i></span>
|
||
<span class="nav-link-title">My Access</span>
|
||
</a>
|
||
</li>
|
||
{{with .User}}
|
||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||
<li class="nav-item{{if eq $.Active "deploy"}} active{{end}}">
|
||
<a class="nav-link" href="/deploy">
|
||
<span class="nav-link-icon"><i class="ti ti-send"></i></span>
|
||
<span class="nav-link-title">Deploy Keys</span>
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
<!-- ── Operations (Admin/Owner only) ── -->
|
||
{{with .User}}
|
||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||
<li class="nav-category">Operations</li>
|
||
<li class="nav-item{{if eq $.Active "cron"}} active{{end}}">
|
||
<a class="nav-link" href="/cron">
|
||
<span class="nav-link-icon"><i class="ti ti-clock"></i></span>
|
||
<span class="nav-link-title">Temporary Access</span>
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
<li class="nav-item{{if eq .Active "audit"}} active{{end}}">
|
||
<a class="nav-link" href="/audit">
|
||
<span class="nav-link-icon"><i class="ti ti-list-details"></i></span>
|
||
<span class="nav-link-title">Audit Log</span>
|
||
</a>
|
||
</li>
|
||
<!-- ── Administration ── -->
|
||
<li class="nav-category">Administration</li>
|
||
{{with .User}}
|
||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||
<li class="nav-item{{if eq $.Active "users"}} active{{end}}">
|
||
<a class="nav-link" href="/users">
|
||
<span class="nav-link-icon"><i class="ti ti-users"></i></span>
|
||
<span class="nav-link-title">Users</span>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item{{if eq $.Active "assignments"}} active{{end}}">
|
||
<a class="nav-link" href="/assignments">
|
||
<span class="nav-link-icon"><i class="ti ti-shield-lock"></i></span>
|
||
<span class="nav-link-title">Access Assignments</span>
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
<li class="nav-item{{if eq .Active "settings"}} active{{end}}">
|
||
<a class="nav-link" href="/settings">
|
||
<span class="nav-link-icon"><i class="ti ti-settings"></i></span>
|
||
<span class="nav-link-title">Settings</span>
|
||
</a>
|
||
</li>
|
||
{{with .User}}
|
||
{{if eq .Role "owner"}}
|
||
<li class="nav-item{{if eq $.Active "admin_settings"}} active{{end}}">
|
||
<a class="nav-link" href="/admin/settings">
|
||
<span class="nav-link-icon"><i class="ti ti-shield-cog"></i></span>
|
||
<span class="nav-link-title">Admin Settings</span>
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
<!-- ── System (Admin/Owner only) ── -->
|
||
{{with .User}}
|
||
{{if or (eq .Role "admin") (eq .Role "owner")}}
|
||
<li class="nav-category">System</li>
|
||
<li class="nav-item{{if eq $.Active "system_info"}} active{{end}}">
|
||
<a class="nav-link" href="/system">
|
||
<span class="nav-link-icon"><i class="ti ti-info-circle"></i></span>
|
||
<span class="nav-link-title">System Information</span>
|
||
</a>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<!-- Main content -->
|
||
<div class="page-wrapper">
|
||
<div class="page-header d-print-none">
|
||
<div class="container-xl">
|
||
<div class="page-pretitle">Keywarden – Centralized SSH Key Management and Deployment</div>
|
||
<h2 class="page-title">{{.Title}}</h2>
|
||
</div>
|
||
</div>
|
||
<div class="page-body">
|
||
<div class="container-xl">
|
||
{{if .Flash}}
|
||
<div class="alert alert-{{.Flash.Type}} alert-dismissible" role="alert">
|
||
<div class="d-flex">
|
||
<div>
|
||
{{if eq .Flash.Type "success"}}<i class="ti ti-check icon alert-icon"></i>{{end}}
|
||
{{if eq .Flash.Type "danger"}}<i class="ti ti-alert-circle icon alert-icon"></i>{{end}}
|
||
{{if eq .Flash.Type "warning"}}<i class="ti ti-alert-triangle icon alert-icon"></i>{{end}}
|
||
</div>
|
||
<div>{{.Flash.Message}}</div>
|
||
</div>
|
||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
|
||
</div>
|
||
{{end}}
|
||
{{template "content" .}}
|
||
</div>
|
||
</div>
|
||
<footer class="footer footer-transparent d-print-none">
|
||
<div class="container-xl">
|
||
<div class="row text-center align-items-center">
|
||
<div class="col-12">
|
||
<span class="text-secondary">© 2026 <a href="https://keywarden.app" target="_blank" rel="noopener noreferrer" class="text-secondary text-decoration-none">Keywarden</a> – Centralized SSH Key Management and Deployment · <a href="{{releasesPageURL}}" target="_blank" rel="noopener noreferrer" class="text-secondary text-decoration-none">{{appVersion}}</a>{{if updateAvailable}} · <a href="{{releaseURL}}" target="_blank" rel="noopener noreferrer" class="text-warning" title="Update verfügbar"><i class="ti ti-download"></i> {{latestVersion}} verfügbar</a>{{end}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
</div><!-- /page-content-row -->
|
||
</div><!-- /page -->
|
||
<!-- Tabler JS (self-hosted) -->
|
||
<script src="/static/js/tabler.min.js"></script>
|
||
<script>
|
||
// --- Theme Toggle ---
|
||
function parseTheme(theme) {
|
||
if (!theme || theme === 'auto' || theme === 'light' || theme === 'dark') {
|
||
return { pair: 'default', mode: theme || 'auto' };
|
||
}
|
||
var idx = theme.lastIndexOf('-');
|
||
if (idx > 0) {
|
||
return { pair: theme.substring(0, idx), mode: theme.substring(idx + 1) };
|
||
}
|
||
return { pair: 'default', mode: 'auto' };
|
||
}
|
||
function resolveMode(mode) {
|
||
if (mode !== 'light' && mode !== 'dark') {
|
||
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||
}
|
||
return mode;
|
||
}
|
||
function getResolvedTheme() {
|
||
return document.documentElement.getAttribute('data-bs-theme') || 'light';
|
||
}
|
||
function applyFullTheme(themeStr) {
|
||
var parts = parseTheme(themeStr);
|
||
var resolved = resolveMode(parts.mode);
|
||
document.documentElement.setAttribute('data-bs-theme', resolved);
|
||
document.documentElement.style.colorScheme = resolved;
|
||
if (parts.pair !== 'default') {
|
||
document.documentElement.setAttribute('data-theme-pair', parts.pair);
|
||
} else {
|
||
document.documentElement.removeAttribute('data-theme-pair');
|
||
}
|
||
updateThemeIcon(resolved);
|
||
}
|
||
function updateThemeIcon(theme) {
|
||
var icon = document.getElementById('theme-icon');
|
||
if (!icon) return;
|
||
icon.className = theme === 'dark' ? 'ti ti-moon' : 'ti ti-sun';
|
||
}
|
||
function toggleTheme() {
|
||
var raw = window.__kwThemeRaw || 'auto';
|
||
var parts = parseTheme(raw);
|
||
var currentMode = getResolvedTheme();
|
||
var nextMode = currentMode === 'dark' ? 'light' : 'dark';
|
||
var nextTheme = parts.pair === 'default' ? nextMode : parts.pair + '-' + nextMode;
|
||
window.__kwThemeRaw = nextTheme;
|
||
applyFullTheme(nextTheme);
|
||
var csrf = (document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/) || [])[1] || '';
|
||
fetch('/settings/theme', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||
body: 'theme=' + encodeURIComponent(nextTheme) + '&_csrf=' + encodeURIComponent(csrf)
|
||
});
|
||
}
|
||
// Set initial icon on page load
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
updateThemeIcon(getResolvedTheme());
|
||
});
|
||
|
||
// --- Mobile Sidebar Toggle ---
|
||
function toggleMobileMenu() {
|
||
var sidebar = document.getElementById('keywarden-sidebar');
|
||
var backdrop = document.getElementById('mobile-sidebar-backdrop');
|
||
var icon = document.getElementById('mobile-menu-icon');
|
||
if (!sidebar) return;
|
||
var isOpen = sidebar.classList.toggle('mobile-open');
|
||
if (backdrop) backdrop.classList.toggle('show', isOpen);
|
||
if (icon) icon.className = isOpen ? 'ti ti-x' : 'ti ti-menu-2';
|
||
}
|
||
function closeMobileMenu() {
|
||
var sidebar = document.getElementById('keywarden-sidebar');
|
||
var backdrop = document.getElementById('mobile-sidebar-backdrop');
|
||
var icon = document.getElementById('mobile-menu-icon');
|
||
if (sidebar) sidebar.classList.remove('mobile-open');
|
||
if (backdrop) backdrop.classList.remove('show');
|
||
if (icon) icon.className = 'ti ti-menu-2';
|
||
}
|
||
|
||
function copyToClipboard(elementId, btn) {
|
||
var el = document.getElementById(elementId);
|
||
var text = el.value || el.textContent;
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
navigator.clipboard.writeText(text).then(function() {
|
||
showCopyFeedback(btn);
|
||
});
|
||
} else {
|
||
// Fallback for non-HTTPS: use temporary textarea (password inputs block select/copy)
|
||
var tmp = document.createElement('textarea');
|
||
tmp.value = text;
|
||
tmp.style.position = 'fixed';
|
||
tmp.style.opacity = '0';
|
||
document.body.appendChild(tmp);
|
||
tmp.focus();
|
||
tmp.select();
|
||
tmp.setSelectionRange(0, 99999);
|
||
document.execCommand('copy');
|
||
document.body.removeChild(tmp);
|
||
showCopyFeedback(btn);
|
||
}
|
||
}
|
||
function showCopyFeedback(btn) {
|
||
var orig = btn.innerHTML;
|
||
btn.innerHTML = '<i class="ti ti-check"></i>';
|
||
btn.classList.add('btn-success');
|
||
btn.classList.remove('btn-outline-primary');
|
||
setTimeout(function() {
|
||
btn.innerHTML = orig;
|
||
btn.classList.remove('btn-success');
|
||
btn.classList.add('btn-outline-primary');
|
||
}, 2000);
|
||
}
|
||
|
||
// --- CSRF Protection ---
|
||
// Reads the _csrf cookie and injects a hidden field into every POST form.
|
||
// Also provides a helper for fetch/AJAX calls.
|
||
(function() {
|
||
function getCsrfToken() {
|
||
var m = document.cookie.match(/(?:^|;\s*)_csrf=([^;]*)/);
|
||
return m ? decodeURIComponent(m[1]) : '';
|
||
}
|
||
// Inject hidden _csrf field into all POST forms
|
||
document.querySelectorAll('form').forEach(function(form) {
|
||
if ((form.method || 'get').toLowerCase() === 'post' && !form.querySelector('input[name="_csrf"]')) {
|
||
var input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = '_csrf';
|
||
input.value = getCsrfToken();
|
||
form.prepend(input);
|
||
}
|
||
});
|
||
// Expose globally for fetch/AJAX calls
|
||
window._csrfToken = getCsrfToken;
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}}
|