dark mode, logging fixes to avoid agent traffic bloat
This commit is contained in:
1 parent
d97b1661c1
commit
f7cf22f970
10 files changed
+488
-36
No files matched your search
@@ -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()
|
||||
|
||||
@@ -6,37 +6,37 @@
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.ak-chart-card {
|
||||
border-color: #1f2937;
|
||||
background: radial-gradient(120% 140% at 0% 0%, #1e293b 0%, #111827 50%, #0b1220 100%);
|
||||
color: #e5e7eb;
|
||||
border-color: var(--kw-chart-card-border);
|
||||
background: var(--kw-chart-card-bg);
|
||||
color: var(--kw-chart-text);
|
||||
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.35);
|
||||
}
|
||||
|
||||
.ak-chart-title {
|
||||
color: #f3f4f6;
|
||||
color: var(--kw-chart-text);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.ak-chart-subtitle {
|
||||
color: #94a3b8;
|
||||
color: var(--kw-chart-subtext);
|
||||
}
|
||||
|
||||
.ak-chart-badge {
|
||||
border: 1px solid rgba(100, 116, 139, 0.45);
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
color: #e2e8f0;
|
||||
border: 1px solid var(--kw-chart-badge-border);
|
||||
background: var(--kw-chart-badge-bg);
|
||||
color: var(--kw-chart-badge-text);
|
||||
}
|
||||
|
||||
.ak-chart-canvas-wrap {
|
||||
border: 1px solid rgba(71, 85, 105, 0.35);
|
||||
background: rgba(2, 6, 23, 0.32);
|
||||
border: 1px solid var(--kw-chart-canvas-border);
|
||||
background: var(--kw-chart-canvas-bg);
|
||||
border-radius: 14px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
|
||||
.ak-chart-empty {
|
||||
color: #94a3b8;
|
||||
color: var(--kw-chart-empty-text);
|
||||
}
|
||||
</style>
|
||||
{% 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("");
|
||||
|
||||
@@ -100,14 +100,28 @@
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="mt-5 flex items-center justify-between border-t border-gray-100 pt-4 text-xs text-gray-500">
|
||||
<span>Certificates and access</span>
|
||||
<div class="mt-5 border-t border-gray-100 pt-4">
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
<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 %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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
|
||||
+323
-4
@@ -8,7 +8,7 @@
|
||||
<title>{% block title %}Keywarden{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="{% static 'branding/keywarden-favicon.svg' %}">
|
||||
<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 name="twitter:card" content="summary">
|
||||
<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">
|
||||
{% tailwind_css %}
|
||||
<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 {
|
||||
max-width: 80rem;
|
||||
}
|
||||
.kw-topnav-link {
|
||||
transition: color 150ms ease, background-color 150ms ease;
|
||||
}
|
||||
html[data-display-mode="full"] .kw-shell-width {
|
||||
max-width: min(1800px, calc(100vw - 1.5rem));
|
||||
}
|
||||
@@ -27,7 +56,196 @@
|
||||
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>
|
||||
<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>
|
||||
(function () {
|
||||
var key = "keywardenDisplayMode";
|
||||
@@ -61,6 +279,15 @@
|
||||
</span>
|
||||
</a>
|
||||
<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 %}
|
||||
<a
|
||||
href="{% url 'accounts:logout' %}"
|
||||
@@ -95,7 +322,7 @@
|
||||
<li>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@@ -103,7 +330,7 @@
|
||||
<li>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@@ -112,7 +339,7 @@
|
||||
<li>
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
@@ -147,6 +374,98 @@
|
||||
</footer>
|
||||
{% endif %}
|
||||
</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>
|
||||
(function () {
|
||||
var key = "keywardenDisplayMode";
|
||||
|
||||
+2
-1
@@ -22,6 +22,7 @@ services:
|
||||
- ./app:/app
|
||||
- ./nginx/certs:/etc/nginx/certs
|
||||
- ./nginx/logs:/var/log/nginx
|
||||
- ./app/logs:/var/log/keywarden
|
||||
ports:
|
||||
- "443:443"
|
||||
depends_on:
|
||||
@@ -100,4 +101,4 @@ services:
|
||||
volumes:
|
||||
pgdata:
|
||||
database:
|
||||
driver: local
|
||||
driver: local
|
||||
@@ -78,6 +78,22 @@ http {
|
||||
location ~* /(\\.git|\\.env|composer\\.(json|lock)|package(-lock)?\\.json|yarn\\.lock)$ {
|
||||
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 / {
|
||||
proxy_intercept_errors on;
|
||||
|
||||
@@ -95,6 +95,23 @@ http {
|
||||
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 / {
|
||||
proxy_intercept_errors on;
|
||||
error_page 404 = @masked_404;
|
||||
|
||||
Reference in new issue
Block a user