dark mode, logging fixes to avoid agent traffic bloat

This commit is contained in:
boris committed 2026-04-16 20:10:23 +01:00
1 parent d97b1661c1
commit f7cf22f970
10 files changed
+488 -36

No files matched your search

+14 -2
View File
@@ -1,10 +1,15 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
try:
from unfold.admin import ModelAdmin as UnfoldModelAdmin
except ImportError:
UnfoldModelAdmin = admin.ModelAdmin
try: try:
from unfold.contrib.guardian.admin import GuardedModelAdmin from unfold.contrib.guardian.admin import GuardedModelAdmin
except ImportError: # Fallback for older Unfold builds without guardian admin shim. except ImportError: # Fallback for older Unfold builds without guardian admin shim.
from guardian.admin import GuardedModelAdmin as GuardianGuardedModelAdmin from guardian.admin import GuardedModelAdmin as GuardianGuardedModelAdmin
from unfold.admin import ModelAdmin as UnfoldModelAdmin
class GuardedModelAdmin(GuardianGuardedModelAdmin, UnfoldModelAdmin): class GuardedModelAdmin(GuardianGuardedModelAdmin, UnfoldModelAdmin):
pass pass
@@ -65,13 +70,20 @@ class ServerAdmin(GuardedModelAdmin):
@admin.register(EnrollmentToken) @admin.register(EnrollmentToken)
class EnrollmentTokenAdmin(admin.ModelAdmin): class EnrollmentTokenAdmin(UnfoldModelAdmin):
list_display = ("token", "created_at", "expires_at", "used_at", "server") list_display = ("token", "created_at", "expires_at", "used_at", "server")
list_filter = ("created_at", "used_at") list_filter = ("created_at", "used_at")
search_fields = ("token", "server__display_name", "server__hostname") search_fields = ("token", "server__display_name", "server__hostname")
readonly_fields = ("token", "created_at", "used_at", "server", "created_by") readonly_fields = ("token", "created_at", "used_at", "server", "created_by")
fields = ("token", "expires_at", "created_by", "created_at", "used_at", "server") fields = ("token", "expires_at", "created_by", "created_at", "used_at", "server")
def get_fields(self, request, obj=None):
# Keep the add form minimal and deterministic in Unfold: token and
# metadata are generated server-side and shown on the change view.
if obj is None:
return ("expires_at",)
return self.fields
def save_model(self, request, obj, form, change) -> None: def save_model(self, request, obj, form, change) -> None:
if not obj.pk: if not obj.pk:
obj.ensure_token() obj.ensure_token()
+85 -23
View File
@@ -6,37 +6,37 @@
{% block extra_head %} {% block extra_head %}
<style> <style>
.ak-chart-card { .ak-chart-card {
border-color: #1f2937; border-color: var(--kw-chart-card-border);
background: radial-gradient(120% 140% at 0% 0%, #1e293b 0%, #111827 50%, #0b1220 100%); background: var(--kw-chart-card-bg);
color: #e5e7eb; color: var(--kw-chart-text);
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.35); box-shadow: 0 10px 30px rgba(2, 6, 23, 0.35);
} }
.ak-chart-title { .ak-chart-title {
color: #f3f4f6; color: var(--kw-chart-text);
font-weight: 700; font-weight: 700;
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
.ak-chart-subtitle { .ak-chart-subtitle {
color: #94a3b8; color: var(--kw-chart-subtext);
} }
.ak-chart-badge { .ak-chart-badge {
border: 1px solid rgba(100, 116, 139, 0.45); border: 1px solid var(--kw-chart-badge-border);
background: rgba(15, 23, 42, 0.65); background: var(--kw-chart-badge-bg);
color: #e2e8f0; color: var(--kw-chart-badge-text);
} }
.ak-chart-canvas-wrap { .ak-chart-canvas-wrap {
border: 1px solid rgba(71, 85, 105, 0.35); border: 1px solid var(--kw-chart-canvas-border);
background: rgba(2, 6, 23, 0.32); background: var(--kw-chart-canvas-bg);
border-radius: 14px; border-radius: 14px;
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
} }
.ak-chart-empty { .ak-chart-empty {
color: #94a3b8; color: var(--kw-chart-empty-text);
} }
</style> </style>
{% endblock %} {% endblock %}
@@ -230,6 +230,7 @@
var panelUrl = "{{ audit_panel_url }}"; var panelUrl = "{{ audit_panel_url }}";
var lineLimitSelect = document.getElementById("js-audit-line-limit"); var lineLimitSelect = document.getElementById("js-audit-line-limit");
var chartsInitialized = false; var chartsInitialized = false;
var chartInstances = [];
var auditPath = "{{ request.path }}"; var auditPath = "{{ request.path }}";
function parseChartData(scriptId) { function parseChartData(scriptId) {
@@ -244,13 +245,49 @@
} }
} }
function getResolvedTheme() {
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
}
function getChartPalette() {
if (getResolvedTheme() === "dark") {
return {
legend: "#e2e8f0",
tickPrimary: "#94a3b8",
tickSecondary: "#cbd5e1",
gridStrong: "rgba(148, 163, 184, 0.12)",
gridSoft: "rgba(148, 163, 184, 0.08)",
tooltipBg: "rgba(15, 23, 42, 0.94)",
tooltipBorder: "rgba(148, 163, 184, 0.35)",
tooltipTitle: "#e2e8f0",
tooltipBody: "#cbd5e1"
};
}
return {
legend: "#0f172a",
tickPrimary: "#64748b",
tickSecondary: "#334155",
gridStrong: "rgba(148, 163, 184, 0.24)",
gridSoft: "rgba(148, 163, 184, 0.16)",
tooltipBg: "rgba(248, 250, 252, 0.98)",
tooltipBorder: "rgba(100, 116, 139, 0.35)",
tooltipTitle: "#0f172a",
tooltipBody: "#334155"
};
}
function renderBarChart(canvasId, emptyId, data, options) { function renderBarChart(canvasId, emptyId, data, options) {
var canvas = document.getElementById(canvasId); var canvas = document.getElementById(canvasId);
var emptyNode = document.getElementById(emptyId); var emptyNode = document.getElementById(emptyId);
var chartOpts = options || {}; var chartOpts = options || {};
var palette = getChartPalette();
if (!canvas) { if (!canvas) {
return; return;
} }
if (canvas._kwChart) {
canvas._kwChart.destroy();
canvas._kwChart = null;
}
if (!window.Chart || !data || !Array.isArray(data.labels) || data.labels.length === 0) { if (!window.Chart || !data || !Array.isArray(data.labels) || data.labels.length === 0) {
canvas.classList.add("hidden"); canvas.classList.add("hidden");
if (emptyNode) { if (emptyNode) {
@@ -275,7 +312,7 @@
} }
] ]
}; };
new window.Chart(canvas.getContext("2d"), { var chart = new window.Chart(canvas.getContext("2d"), {
type: "bar", type: "bar",
data: chartData, data: chartData,
options: { options: {
@@ -288,7 +325,7 @@
position: "top", position: "top",
align: "center", align: "center",
labels: { labels: {
color: "#e2e8f0", color: palette.legend,
boxWidth: 18, boxWidth: 18,
boxHeight: 8, boxHeight: 8,
font: { font: {
@@ -298,11 +335,11 @@
} }
}, },
tooltip: { tooltip: {
backgroundColor: "rgba(15, 23, 42, 0.94)", backgroundColor: palette.tooltipBg,
borderColor: "rgba(148, 163, 184, 0.35)", borderColor: palette.tooltipBorder,
borderWidth: 1, borderWidth: 1,
titleColor: "#e2e8f0", titleColor: palette.tooltipTitle,
bodyColor: "#cbd5e1", bodyColor: palette.tooltipBody,
callbacks: { callbacks: {
label: function (ctx) { label: function (ctx) {
var isHorizontal = chartOpts.indexAxis === "y"; var isHorizontal = chartOpts.indexAxis === "y";
@@ -317,19 +354,19 @@
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
precision: 0, precision: 0,
color: "#94a3b8" color: palette.tickPrimary
}, },
grid: { grid: {
color: "rgba(148, 163, 184, 0.12)", color: palette.gridStrong,
drawBorder: false drawBorder: false
} }
}, },
y: { y: {
ticks: { ticks: {
color: "#cbd5e1" color: palette.tickSecondary
}, },
grid: { grid: {
color: "rgba(148, 163, 184, 0.08)", color: palette.gridSoft,
drawBorder: false drawBorder: false
} }
} }
@@ -354,12 +391,28 @@
} }
} }
}); });
canvas._kwChart = chart;
chartInstances.push(chart);
} }
function initializeCharts() { function destroyCharts() {
if (chartsInitialized) { for (var i = 0; i < chartInstances.length; i += 1) {
try {
chartInstances[i].destroy();
} catch (err) {
// noop
}
}
chartInstances = [];
}
function initializeCharts(force) {
if (chartsInitialized && !force) {
return; return;
} }
if (force) {
destroyCharts();
}
chartsInitialized = true; chartsInitialized = true;
renderBarChart("js-login-chart", "js-login-chart-empty", parseChartData("js-login-chart-data"), { renderBarChart("js-login-chart", "js-login-chart-empty", parseChartData("js-login-chart-data"), {
datasetLabel: "Logins" datasetLabel: "Logins"
@@ -512,6 +565,15 @@
} }
setSubpage("{{ audit_subpage }}", false); setSubpage("{{ audit_subpage }}", false);
window.addEventListener("keywarden:theme-mode", function () {
if (chartsSection && !chartsSection.classList.contains("hidden")) {
initializeCharts(true);
return;
}
destroyCharts();
chartsInitialized = false;
});
if (addButton) { if (addButton) {
addButton.addEventListener("click", function () { addButton.addEventListener("click", function () {
createPanel(""); createPanel("");
@@ -100,14 +100,28 @@
</dd> </dd>
</div> </div>
</dl> </dl>
<div class="mt-5 flex items-center justify-between border-t border-gray-100 pt-4 text-xs text-gray-500"> <div class="mt-5 border-t border-gray-100 pt-4">
<span>Certificates and access</span>
{% if item.access.can_users %} {% if item.access.can_users %}
<a href="{% url 'servers:detail' item.server.id %}" class="font-semibold text-blue-700 hover:underline">View details</a> <a
href="{% url 'servers:detail' item.server.id %}"
class="inline-flex w-full items-center justify-center rounded-full bg-blue-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-800"
>
Open details
</a>
{% elif item.access.can_logs %} {% elif item.access.can_logs %}
<a href="{% url 'servers:audit' item.server.id %}" class="font-semibold text-blue-700 hover:underline">Open audit</a> <a
href="{% url 'servers:audit' item.server.id %}"
class="inline-flex w-full items-center justify-center rounded-full bg-blue-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-800"
>
Open audit
</a>
{% elif item.access.can_shell %} {% elif item.access.can_shell %}
<a href="{% url 'servers:shell' item.server.id %}" class="font-semibold text-blue-700 hover:underline">Open shell</a> <a
href="{% url 'servers:shell' item.server.id %}"
class="inline-flex w-full items-center justify-center rounded-full bg-blue-700 px-4 py-2 text-xs font-semibold text-white transition hover:bg-blue-800"
>
Open shell
</a>
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
+4
View File
@@ -36,4 +36,8 @@ python manage.py collectstatic --noinput
python manage.py migrate --noinput python manage.py migrate --noinput
python manage.py ensure_admin python manage.py ensure_admin
# Ensure application log directory exists and is writable by the Django runtime user.
mkdir -p /var/log/keywarden
chown -R djangouser:djangouser /var/log/keywarden || true
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
+1
View File
@@ -0,0 +1 @@
+7 -1
View File
@@ -1,4 +1,10 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
exec gunicorn keywarden.wsgi:application --bind 0.0.0.0:8000 --workers 3 exec gunicorn keywarden.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 3 \
--access-logfile /var/log/keywarden/gunicorn-access.log \
--error-logfile /var/log/keywarden/gunicorn-error.log \
--capture-output \
--log-level info
+323 -4
View File
@@ -8,7 +8,7 @@
<title>{% block title %}Keywarden{% endblock %}</title> <title>{% block title %}Keywarden{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{% static 'branding/keywarden-favicon.svg' %}"> <link rel="icon" type="image/svg+xml" href="{% static 'branding/keywarden-favicon.svg' %}">
<link rel="icon" type="image/png" href="{% static 'ninja/favicon.png' %}"> <link rel="icon" type="image/png" href="{% static 'ninja/favicon.png' %}">
<meta name="theme-color" content="#0f172a"> <meta id="kw-theme-color" name="theme-color" content="#0f172a">
<meta property="og:title" content="Keywarden"> <meta property="og:title" content="Keywarden">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@@ -16,9 +16,38 @@
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
{% tailwind_css %} {% tailwind_css %}
<style> <style>
:root {
color-scheme: light;
--kw-chart-card-border: #d6e0ee;
--kw-chart-card-bg: radial-gradient(120% 140% at 0% 0%, #f8fbff 0%, #eef4ff 50%, #e6eefb 100%);
--kw-chart-text: #0f172a;
--kw-chart-subtext: #475569;
--kw-chart-badge-border: rgba(148, 163, 184, 0.45);
--kw-chart-badge-bg: rgba(255, 255, 255, 0.85);
--kw-chart-badge-text: #1e293b;
--kw-chart-canvas-border: rgba(148, 163, 184, 0.35);
--kw-chart-canvas-bg: rgba(248, 250, 252, 0.75);
--kw-chart-empty-text: #64748b;
}
html[data-theme="dark"] {
color-scheme: dark;
--kw-chart-card-border: #1f2937;
--kw-chart-card-bg: radial-gradient(120% 140% at 0% 0%, #1e293b 0%, #111827 50%, #0b1220 100%);
--kw-chart-text: #f3f4f6;
--kw-chart-subtext: #94a3b8;
--kw-chart-badge-border: rgba(100, 116, 139, 0.45);
--kw-chart-badge-bg: rgba(15, 23, 42, 0.65);
--kw-chart-badge-text: #e2e8f0;
--kw-chart-canvas-border: rgba(71, 85, 105, 0.35);
--kw-chart-canvas-bg: rgba(2, 6, 23, 0.32);
--kw-chart-empty-text: #94a3b8;
}
.kw-shell-width { .kw-shell-width {
max-width: 80rem; max-width: 80rem;
} }
.kw-topnav-link {
transition: color 150ms ease, background-color 150ms ease;
}
html[data-display-mode="full"] .kw-shell-width { html[data-display-mode="full"] .kw-shell-width {
max-width: min(1800px, calc(100vw - 1.5rem)); max-width: min(1800px, calc(100vw - 1.5rem));
} }
@@ -27,7 +56,196 @@
max-width: min(1880px, calc(100vw - 3rem)); max-width: min(1880px, calc(100vw - 3rem));
} }
} }
html[data-theme="dark"] body {
background: linear-gradient(135deg, #020617, #0f172a 45%, #111827);
color: #e2e8f0;
}
html[data-theme="dark"] header,
html[data-theme="dark"] footer {
border-color: #334155 !important;
background-color: rgba(15, 23, 42, 0.92) !important;
}
html[data-theme="dark"] .bg-white {
background-color: #0f172a !important;
}
html[data-theme="dark"] .bg-gray-50 {
background-color: #111827 !important;
}
html[data-theme="dark"] .bg-gray-100 {
background-color: #1f2937 !important;
}
html[data-theme="dark"] .border-gray-100 {
border-color: #1e293b !important;
}
html[data-theme="dark"] .border-gray-200 {
border-color: #334155 !important;
}
html[data-theme="dark"] .border-gray-300 {
border-color: #475569 !important;
}
html[data-theme="dark"] .divide-gray-100 > :not([hidden]) ~ :not([hidden]) {
border-color: #1e293b !important;
}
html[data-theme="dark"] .divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
border-color: #334155 !important;
}
html[data-theme="dark"] .text-gray-900 {
color: #f1f5f9 !important;
}
html[data-theme="dark"] .text-gray-800 {
color: #e2e8f0 !important;
}
html[data-theme="dark"] .text-gray-700 {
color: #e2e8f0 !important;
}
html[data-theme="dark"] .text-gray-600 {
color: #cbd5e1 !important;
}
html[data-theme="dark"] .text-gray-500 {
color: #94a3b8 !important;
}
html[data-theme="dark"] .text-gray-400 {
color: #64748b !important;
}
html[data-theme="dark"] .text-blue-700 {
color: #60a5fa !important;
}
html[data-theme="dark"] .text-emerald-700 {
color: #34d399 !important;
}
html[data-theme="dark"] .text-emerald-800 {
color: #6ee7b7 !important;
}
html[data-theme="dark"] .text-indigo-700 {
color: #818cf8 !important;
}
html[data-theme="dark"] .text-rose-700 {
color: #fb7185 !important;
}
html[data-theme="dark"] .text-red-600 {
color: #f87171 !important;
}
html[data-theme="dark"] .text-red-800 {
color: #fca5a5 !important;
}
html[data-theme="dark"] .text-amber-700 {
color: #fbbf24 !important;
}
html[data-theme="dark"] .text-amber-800 {
color: #fcd34d !important;
}
html[data-theme="dark"] .ring-blue-100 {
--tw-ring-color: rgba(59, 130, 246, 0.35) !important;
}
html[data-theme="dark"] .hover\:bg-gray-50:hover {
background-color: #1f2937 !important;
}
html[data-theme="dark"] .hover\:bg-gray-100:hover {
background-color: #334155 !important;
}
html[data-theme="dark"] .hover\:bg-white:hover {
background-color: #1e293b !important;
}
html[data-theme="dark"] .hover\:text-gray-900:hover {
color: #f1f5f9 !important;
}
html[data-theme="dark"] .bg-blue-50 {
background-color: rgba(37, 99, 235, 0.2) !important;
}
html[data-theme="dark"] .bg-blue-100 {
background-color: rgba(37, 99, 235, 0.3) !important;
}
html[data-theme="dark"] .bg-emerald-50 {
background-color: rgba(5, 150, 105, 0.2) !important;
}
html[data-theme="dark"] .bg-emerald-100 {
background-color: rgba(5, 150, 105, 0.28) !important;
}
html[data-theme="dark"] .bg-indigo-50 {
background-color: rgba(79, 70, 229, 0.22) !important;
}
html[data-theme="dark"] .bg-rose-50 {
background-color: rgba(225, 29, 72, 0.2) !important;
}
html[data-theme="dark"] .bg-rose-100 {
background-color: rgba(225, 29, 72, 0.28) !important;
}
html[data-theme="dark"] .bg-amber-100 {
background-color: rgba(180, 83, 9, 0.35) !important;
}
html[data-theme="dark"] .bg-red-50 {
background-color: rgba(127, 29, 29, 0.32) !important;
}
html[data-theme="dark"] .bg-gray-200 {
background-color: #334155 !important;
}
html[data-theme="dark"] .hover\:bg-blue-50:hover {
background-color: rgba(37, 99, 235, 0.28) !important;
}
html[data-theme="dark"] .hover\:bg-blue-100:hover {
background-color: rgba(37, 99, 235, 0.35) !important;
}
html[data-theme="dark"] tr.border-t {
border-color: #334155 !important;
}
html[data-theme="dark"] hr {
border-color: #334155 !important;
}
html[data-theme="dark"] input[type="text"],
html[data-theme="dark"] input[type="password"],
html[data-theme="dark"] input[type="email"],
html[data-theme="dark"] input[type="search"],
html[data-theme="dark"] input[type="datetime-local"],
html[data-theme="dark"] input[type="number"],
html[data-theme="dark"] input[type="url"],
html[data-theme="dark"] input[type="tel"],
html[data-theme="dark"] textarea,
html[data-theme="dark"] select {
background-color: #0b1220 !important;
border-color: #475569 !important;
color: #e2e8f0 !important;
}
html[data-theme="dark"] input::placeholder,
html[data-theme="dark"] textarea::placeholder {
color: #94a3b8 !important;
}
html[data-theme="dark"] .kw-topnav-link {
color: #cbd5e1 !important;
}
html[data-theme="dark"] .kw-topnav-link:hover {
background-color: #1f2937 !important;
color: #bfdbfe !important;
}
@media (min-width: 768px) {
html[data-theme="dark"] .kw-topnav-link:hover {
background-color: transparent !important;
color: #93c5fd !important;
}
}
</style> </style>
<script>
(function () {
var key = "keywardenThemeMode";
function normalize(mode) {
return mode === "light" || mode === "dark" || mode === "system" ? mode : "system";
}
function resolve(mode) {
if (mode === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return mode;
}
var mode = "system";
try {
mode = normalize(window.localStorage.getItem(key));
} catch (err) {
mode = "system";
}
var resolved = resolve(mode);
document.documentElement.setAttribute("data-theme-mode", mode);
document.documentElement.setAttribute("data-theme", resolved);
})();
</script>
<script> <script>
(function () { (function () {
var key = "keywardenDisplayMode"; var key = "keywardenDisplayMode";
@@ -61,6 +279,15 @@
</span> </span>
</a> </a>
<div class="flex items-center gap-3 md:order-2"> <div class="flex items-center gap-3 md:order-2">
<button
type="button"
data-theme-toggle
class="inline-flex items-center gap-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-300"
aria-label="Toggle theme"
>
<span data-theme-icon aria-hidden="true">◐</span>
<span data-theme-label>Theme</span>
</button>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a <a
href="{% url 'accounts:logout' %}" href="{% url 'accounts:logout' %}"
@@ -95,7 +322,7 @@
<li> <li>
<a <a
href="{% url 'servers:dashboard' %}" href="{% url 'servers:dashboard' %}"
class="block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700" class="kw-topnav-link block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700"
> >
Servers Servers
</a> </a>
@@ -103,7 +330,7 @@
<li> <li>
<a <a
href="{% url 'accounts:profile' %}" href="{% url 'accounts:profile' %}"
class="block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700" class="kw-topnav-link block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700"
> >
Profile Profile
</a> </a>
@@ -112,7 +339,7 @@
<li> <li>
<a <a
href="{% url 'servers:admin_dashboard' %}" href="{% url 'servers:admin_dashboard' %}"
class="block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700" class="kw-topnav-link block rounded px-3 py-2 hover:bg-gray-100 md:px-0 md:py-0 md:hover:bg-transparent md:hover:text-blue-700"
> >
Admin Admin
</a> </a>
@@ -147,6 +374,98 @@
</footer> </footer>
{% endif %} {% endif %}
</div> </div>
<script>
(function () {
var key = "keywardenThemeMode";
var root = document.documentElement;
var toggles = document.querySelectorAll("[data-theme-toggle]");
var labels = document.querySelectorAll("[data-theme-label]");
var icons = document.querySelectorAll("[data-theme-icon]");
var themeMeta = document.getElementById("kw-theme-color");
function normalize(mode) {
return mode === "light" || mode === "dark" || mode === "system" ? mode : "system";
}
function resolve(mode) {
if (mode === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return mode;
}
function readMode() {
try {
return normalize(window.localStorage.getItem(key));
} catch (err) {
return "system";
}
}
function writeMode(mode) {
try {
window.localStorage.setItem(key, mode);
} catch (err) {
// Ignore storage failures.
}
}
function apply(mode) {
var resolved = resolve(mode);
root.setAttribute("data-theme-mode", mode);
root.setAttribute("data-theme", resolved);
var labelText = mode === "system" ? "Theme: System" : (mode === "dark" ? "Theme: Dark" : "Theme: Light");
var iconText = resolved === "dark" ? "☾" : "☀";
if (themeMeta) {
themeMeta.setAttribute("content", resolved === "dark" ? "#020617" : "#f8fafc");
}
for (var i = 0; i < labels.length; i += 1) {
labels[i].textContent = labelText;
}
for (var j = 0; j < icons.length; j += 1) {
icons[j].textContent = iconText;
}
if (typeof window.CustomEvent === "function") {
window.dispatchEvent(new CustomEvent("keywarden:theme-mode", { detail: { mode: mode, resolved: resolved } }));
}
return mode;
}
function cycle(mode) {
if (mode === "light") {
return "dark";
}
if (mode === "dark") {
return "system";
}
return "light";
}
var currentMode = apply(readMode());
for (var k = 0; k < toggles.length; k += 1) {
toggles[k].addEventListener("click", function () {
currentMode = apply(cycle(currentMode));
writeMode(currentMode);
});
}
var media = window.matchMedia("(prefers-color-scheme: dark)");
var mediaListener = function () {
if (currentMode === "system") {
apply(currentMode);
}
};
if (typeof media.addEventListener === "function") {
media.addEventListener("change", mediaListener);
} else if (typeof media.addListener === "function") {
media.addListener(mediaListener);
}
window.keywardenGetThemeMode = function () {
return currentMode;
};
window.keywardenSetThemeMode = function (mode) {
currentMode = apply(normalize(mode));
writeMode(currentMode);
return currentMode;
};
})();
</script>
<script> <script>
(function () { (function () {
var key = "keywardenDisplayMode"; var key = "keywardenDisplayMode";
+2 -1
View File
@@ -22,6 +22,7 @@ services:
- ./app:/app - ./app:/app
- ./nginx/certs:/etc/nginx/certs - ./nginx/certs:/etc/nginx/certs
- ./nginx/logs:/var/log/nginx - ./nginx/logs:/var/log/nginx
- ./app/logs:/var/log/keywarden
ports: ports:
- "443:443" - "443:443"
depends_on: depends_on:
@@ -100,4 +101,4 @@ services:
volumes: volumes:
pgdata: pgdata:
database: database:
driver: local driver: local
+16
View File
@@ -78,6 +78,22 @@ http {
location ~* /(\\.git|\\.env|composer\\.(json|lock)|package(-lock)?\\.json|yarn\\.lock)$ { location ~* /(\\.git|\\.env|composer\\.(json|lock)|package(-lock)?\\.json|yarn\\.lock)$ {
return 404; return 404;
} }
# Route agent control-plane requests to a dedicated access log to keep
# the primary access log focused on user-facing traffic.
location ^~ /api/v1/agent/ {
access_log /var/log/nginx/agent-access.log main;
proxy_intercept_errors on;
error_page 404 = @masked_404;
error_page 401 = @masked_401;
proxy_pass http://127.0.0.1:8000;
include options-https-headers.conf;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
location / { location / {
proxy_intercept_errors on; proxy_intercept_errors on;
+17
View File
@@ -95,6 +95,23 @@ http {
proxy_read_timeout 1h; proxy_read_timeout 1h;
} }
# Route agent control-plane requests to a dedicated access log to keep
# the primary access log focused on user-facing traffic.
location ^~ /api/v1/agent/ {
access_log /var/log/nginx/agent-access.log main;
proxy_intercept_errors on;
error_page 404 = @masked_404;
error_page 401 = @masked_401;
proxy_pass http://127.0.0.1:8000;
include options-https-headers.conf;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 1h;
}
location / { location / {
proxy_intercept_errors on; proxy_intercept_errors on;
error_page 404 = @masked_404; error_page 404 = @masked_404;