From f7cf22f9709a978a9cb80e727b085d71e55d61d3 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 16 Apr 2026 20:10:23 +0100 Subject: [PATCH] dark mode, logging fixes to avoid agent traffic bloat --- app/apps/servers/admin.py | 16 +- app/apps/servers/templates/servers/audit.html | 108 ++++-- .../servers/templates/servers/dashboard.html | 24 +- app/entrypoint.sh | 4 + app/logs/.gitkeep | 1 + app/scripts/gunicorn.sh | 8 +- app/templates/base.html | 327 +++++++++++++++++- docker-compose.yml | 3 +- nginx/configs/nginx.conf | 16 + nginx/configs/nginx.conf.template | 17 + 10 files changed, 488 insertions(+), 36 deletions(-) create mode 100644 app/logs/.gitkeep diff --git a/app/apps/servers/admin.py b/app/apps/servers/admin.py index 3555557..fe2141b 100644 --- a/app/apps/servers/admin.py +++ b/app/apps/servers/admin.py @@ -1,10 +1,15 @@ from django.contrib import admin from django.utils.html import format_html + +try: + from unfold.admin import ModelAdmin as UnfoldModelAdmin +except ImportError: + UnfoldModelAdmin = admin.ModelAdmin + try: from unfold.contrib.guardian.admin import GuardedModelAdmin except ImportError: # Fallback for older Unfold builds without guardian admin shim. from guardian.admin import GuardedModelAdmin as GuardianGuardedModelAdmin - from unfold.admin import ModelAdmin as UnfoldModelAdmin class GuardedModelAdmin(GuardianGuardedModelAdmin, UnfoldModelAdmin): pass @@ -65,13 +70,20 @@ class ServerAdmin(GuardedModelAdmin): @admin.register(EnrollmentToken) -class EnrollmentTokenAdmin(admin.ModelAdmin): +class EnrollmentTokenAdmin(UnfoldModelAdmin): list_display = ("token", "created_at", "expires_at", "used_at", "server") list_filter = ("created_at", "used_at") search_fields = ("token", "server__display_name", "server__hostname") readonly_fields = ("token", "created_at", "used_at", "server", "created_by") 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: if not obj.pk: obj.ensure_token() diff --git a/app/apps/servers/templates/servers/audit.html b/app/apps/servers/templates/servers/audit.html index 72a8d6c..c2eeb3f 100644 --- a/app/apps/servers/templates/servers/audit.html +++ b/app/apps/servers/templates/servers/audit.html @@ -6,37 +6,37 @@ {% block extra_head %} {% endblock %} @@ -230,6 +230,7 @@ var panelUrl = "{{ audit_panel_url }}"; var lineLimitSelect = document.getElementById("js-audit-line-limit"); var chartsInitialized = false; + var chartInstances = []; var auditPath = "{{ request.path }}"; 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) { var canvas = document.getElementById(canvasId); var emptyNode = document.getElementById(emptyId); var chartOpts = options || {}; + var palette = getChartPalette(); if (!canvas) { return; } + if (canvas._kwChart) { + canvas._kwChart.destroy(); + canvas._kwChart = null; + } if (!window.Chart || !data || !Array.isArray(data.labels) || data.labels.length === 0) { canvas.classList.add("hidden"); if (emptyNode) { @@ -275,7 +312,7 @@ } ] }; - new window.Chart(canvas.getContext("2d"), { + var chart = new window.Chart(canvas.getContext("2d"), { type: "bar", data: chartData, options: { @@ -288,7 +325,7 @@ position: "top", align: "center", labels: { - color: "#e2e8f0", + color: palette.legend, boxWidth: 18, boxHeight: 8, font: { @@ -298,11 +335,11 @@ } }, tooltip: { - backgroundColor: "rgba(15, 23, 42, 0.94)", - borderColor: "rgba(148, 163, 184, 0.35)", + backgroundColor: palette.tooltipBg, + borderColor: palette.tooltipBorder, borderWidth: 1, - titleColor: "#e2e8f0", - bodyColor: "#cbd5e1", + titleColor: palette.tooltipTitle, + bodyColor: palette.tooltipBody, callbacks: { label: function (ctx) { var isHorizontal = chartOpts.indexAxis === "y"; @@ -317,19 +354,19 @@ beginAtZero: true, ticks: { precision: 0, - color: "#94a3b8" + color: palette.tickPrimary }, grid: { - color: "rgba(148, 163, 184, 0.12)", + color: palette.gridStrong, drawBorder: false } }, y: { ticks: { - color: "#cbd5e1" + color: palette.tickSecondary }, grid: { - color: "rgba(148, 163, 184, 0.08)", + color: palette.gridSoft, drawBorder: false } } @@ -354,12 +391,28 @@ } } }); + canvas._kwChart = chart; + chartInstances.push(chart); } - function initializeCharts() { - if (chartsInitialized) { + function destroyCharts() { + for (var i = 0; i < chartInstances.length; i += 1) { + try { + chartInstances[i].destroy(); + } catch (err) { + // noop + } + } + chartInstances = []; + } + + function initializeCharts(force) { + if (chartsInitialized && !force) { return; } + if (force) { + destroyCharts(); + } chartsInitialized = true; renderBarChart("js-login-chart", "js-login-chart-empty", parseChartData("js-login-chart-data"), { datasetLabel: "Logins" @@ -512,6 +565,15 @@ } setSubpage("{{ audit_subpage }}", false); + window.addEventListener("keywarden:theme-mode", function () { + if (chartsSection && !chartsSection.classList.contains("hidden")) { + initializeCharts(true); + return; + } + destroyCharts(); + chartsInitialized = false; + }); + if (addButton) { addButton.addEventListener("click", function () { createPanel(""); diff --git a/app/apps/servers/templates/servers/dashboard.html b/app/apps/servers/templates/servers/dashboard.html index 1956266..3660f03 100644 --- a/app/apps/servers/templates/servers/dashboard.html +++ b/app/apps/servers/templates/servers/dashboard.html @@ -100,14 +100,28 @@ -
- Certificates and access +
{% if item.access.can_users %} - View details + + Open details + {% elif item.access.can_logs %} - Open audit + + Open audit + {% elif item.access.can_shell %} - Open shell + + Open shell + {% endif %}
{% else %} diff --git a/app/entrypoint.sh b/app/entrypoint.sh index a5f344f..5b05a34 100755 --- a/app/entrypoint.sh +++ b/app/entrypoint.sh @@ -36,4 +36,8 @@ python manage.py collectstatic --noinput python manage.py migrate --noinput 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 diff --git a/app/logs/.gitkeep b/app/logs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/app/scripts/gunicorn.sh b/app/scripts/gunicorn.sh index 29101d2..e8d5025 100755 --- a/app/scripts/gunicorn.sh +++ b/app/scripts/gunicorn.sh @@ -1,4 +1,10 @@ #!/bin/sh 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 diff --git a/app/templates/base.html b/app/templates/base.html index d45fb4e..bc9448c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -8,7 +8,7 @@ {% block title %}Keywarden{% endblock %} - + @@ -16,9 +16,38 @@ {% tailwind_css %} +