Files
keywarden/web/templates/layout/base.html
scriptos 653592e68f
Some checks failed
PR Tests / Lint, Build & Test (pull_request) Successful in 5m45s
Security Scan / Go Vulnerability Check (pull_request) Failing after 5m42s
feat: add automatic update checker with version injection
- Add internal/updater package (queries Gitea releases API every 6h)

- Inject version at build time via -ldflags (-X main.Version)

- Show update badge in header for admin/owner users

- Show version on system info page

- Add VERSION build arg to Dockerfile

- Update docs (deployment, architecture, admin-guide, contributing, README)
2026-04-07 23:13:26 +02:00

942 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{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" 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 ── */
header.navbar.keywarden-top-header {
background: #1D2B38 !important;
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: #1D2B38 !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) ── */
.navbar-vertical {
background: #1D2B38 !important;
flex-shrink: 0;
overflow: hidden;
align-self: stretch;
display: flex;
flex-direction: column;
}
[data-bs-theme="dark"] .navbar-vertical { background: #0a1120 !important; }
[data-bs-theme="dark"] header.navbar.keywarden-top-header { background: #0a1120 !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: #1D2B38 !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: #0F1829; }
[data-bs-theme="dark"] .page-body { background: #0F1829; }
html[data-bs-theme="dark"],
html[data-bs-theme="dark"] body { background-color: #0F1829 !important; }
.page-body { content-visibility: auto; contain-intrinsic-size: auto 500px; }
/* ── 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: #155e75 !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: #071220 !important; }
html[data-theme-pair="ocean"][data-bs-theme="light"] .page-wrapper { background: #ecfeff; }
html[data-theme-pair="ocean"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="ocean"][data-bs-theme="dark"] .page-body { background: #0c1a2a; }
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: #14532d !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: #061209 !important; }
html[data-theme-pair="forest"][data-bs-theme="light"] .page-wrapper { background: #f0fdf4; }
html[data-theme-pair="forest"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="forest"][data-bs-theme="dark"] .page-body { background: #0a1a10; }
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: #78350f !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: #110d04 !important; }
html[data-theme-pair="sunset"][data-bs-theme="light"] .page-wrapper { background: #fffbeb; }
html[data-theme-pair="sunset"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="sunset"][data-bs-theme="dark"] .page-body { background: #1a1408; }
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: #831843 !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: #12060e !important; }
html[data-theme-pair="rose"][data-bs-theme="light"] .page-wrapper { background: #fdf2f8; }
html[data-theme-pair="rose"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="rose"][data-bs-theme="dark"] .page-body { background: #1a0a14; }
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: #2e3440 !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: #14171c !important; }
html[data-theme-pair="nord"][data-bs-theme="light"] .page-wrapper { background: #eceff4; }
html[data-theme-pair="nord"][data-bs-theme="dark"] .page-wrapper,
html[data-theme-pair="nord"][data-bs-theme="dark"] .page-body { background: #1e2128; }
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; }
</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">&copy; 2026 Keywarden Centralized SSH Key Management and Deployment | AGPLv3</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}}