Files
keywarden/web/templates/layout/base.html

1087 lines
50 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?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">&copy; 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}}