HTTPD performance improvements

This commit is contained in:
2026-05-17 23:38:54 -04:00
parent 94cb2a98c4
commit 823ee61a07
20 changed files with 367 additions and 188 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ COMMANDS
</section>
<section class="rise rise-1 mt-6 flex flex-wrap items-center gap-3">
<label class="ring-card relative flex h-10 flex-1 items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30">
<label class="ring-card relative flex h-10 w-full items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-md">
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-search"/></svg>
<input id="command-filter"
type="text"
@@ -19,7 +19,7 @@ COMMANDS
Collapse all
</button>
</section>
<p class="rise rise-1 mt-2 hidden font-mono text-[11px] text-destructive" id="command-empty">No commands match that filter.</p>
<p class="rise rise-1 mt-2 hidden text-sm text-destructive" id="command-empty">No commands match that filter.</p>
<section class="rise rise-2 mt-2">
${commands}
+13
View File
@@ -7,6 +7,19 @@ server:
file-path: "httpd.log" # relative to the module's data folder
console: false # also mirror to the Bukkit console
# Jetty thread pool. Bounded so a flood of HTTP requests can't starve the
# Minecraft tick thread or consume unbounded memory.
threads:
max: 16
min: 2
idle-timeout-ms: 30000
# Per-connection limits that close slow/abusive clients quickly.
limits:
idle-timeout-ms: 15000 # drop conns with no progress for this long
accept-queue: 32 # OS-level backlog of pending TCP connects
request-header-bytes: 8192 # cap header size; oversized requests get 431
# Token-bucket rate limiting. Defaults are sized for a small sized server.
# capacity = burst, per-second = sustained rate. Disable globally with enabled: false.
rate-limit:
+1 -1
View File
@@ -2,7 +2,7 @@ Indefinite Bans
INDEFBANS
<section class="rise flex flex-wrap items-end justify-between gap-3">
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Indefinite bans</h1>
<div class="flex items-center gap-4 font-mono text-[11px] text-muted-foreground tabular">
<div class="flex items-center gap-4 text-sm text-muted-foreground tabular">
<span><span class="text-foreground">${group_count}</span> groups</span>
<span><span class="text-foreground">${total_users}</span> users</span>
<span><span class="text-foreground">${total_uuids}</span> uuids</span>
+31 -31
View File
@@ -2,26 +2,26 @@ Overview
HOME
<section class="rise flex flex-wrap items-end justify-between gap-3">
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Overview</h1>
<span class="font-mono text-xs text-muted-foreground">
Minecraft version <span data-stat="version"></span>
<span class="text-sm text-muted-foreground">
Minecraft version <span data-stat="version" class="text-foreground"></span>
</span>
</section>
<section class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<article class="rise rise-1 ring-card relative overflow-hidden rounded-2xl bg-card p-5">
<article class="rise rise-1 ring-card relative flex flex-col overflow-hidden rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">Players</span>
<span class="text-sm text-muted-foreground">Players</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-users"/></svg>
</div>
<div class="mt-4 flex items-baseline gap-2">
<span data-stat="players-online" class="tabular text-4xl font-medium tracking-tight"></span>
<span class="font-mono text-sm text-muted-foreground">/ <span data-stat="players-max" class="tabular"></span></span>
<span class="text-sm text-muted-foreground">/ <span data-stat="players-max" class="tabular"></span></span>
</div>
<div class="mt-5 h-1 overflow-hidden rounded-full bg-muted">
<div data-stat="players-bar" class="h-full rounded-full bg-primary transition-[width] duration-500" style="width:0"></div>
</div>
<div class="mt-3 flex items-center justify-between font-mono text-[11px] text-muted-foreground">
<div class="mt-auto flex items-center justify-between pt-3 text-xs text-muted-foreground">
<span>online</span>
<a href="/players/" class="inline-flex items-center gap-1 text-foreground/80 hover:text-foreground">
view list <svg class="size-3" aria-hidden="true"><use href="#i-arrow-right"/></svg>
@@ -29,9 +29,9 @@ HOME
</div>
</article>
<article class="rise rise-2 ring-card relative overflow-hidden rounded-2xl bg-card p-5">
<article class="rise rise-2 ring-card relative flex flex-col overflow-hidden rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">CPU</span>
<span class="text-sm text-muted-foreground">CPU</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-chip"/></svg>
</div>
<div class="mt-4 flex items-baseline gap-1.5">
@@ -40,38 +40,38 @@ HOME
<div class="mt-5 h-1 overflow-hidden rounded-full bg-muted">
<div data-stat="cpu-bar" class="h-full rounded-full bg-primary transition-[width] duration-500" style="width:0"></div>
</div>
<div class="mt-3 flex items-center justify-between font-mono text-[11px] text-muted-foreground">
<div class="mt-auto flex items-center justify-between pt-3 text-xs text-muted-foreground">
<span>process · <span data-stat="cpu-cores" class="tabular text-foreground/80"></span> cores</span>
<span>sys <span data-stat="cpu-system-value" class="tabular text-foreground/80"></span></span>
</div>
</article>
<article class="rise rise-3 ring-card relative overflow-hidden rounded-2xl bg-card p-5">
<article class="rise rise-3 ring-card relative flex flex-col overflow-hidden rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">Memory</span>
<span class="text-sm text-muted-foreground">Memory</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-database"/></svg>
</div>
<div class="mt-4 flex items-baseline gap-1.5">
<span data-stat="mem-value" class="tabular text-4xl font-medium tracking-tight"></span>
<span data-stat="mem-unit" class="font-mono text-sm text-muted-foreground"></span>
<span data-stat="mem-unit" class="text-sm text-muted-foreground"></span>
</div>
<div class="mt-5 h-1 overflow-hidden rounded-full bg-muted">
<div data-stat="mem-bar" class="h-full rounded-full bg-primary transition-[width] duration-500" style="width:0"></div>
</div>
<div class="mt-3 flex items-center justify-between font-mono text-[11px] text-muted-foreground">
<div class="mt-auto flex items-center justify-between pt-3 text-xs text-muted-foreground">
<span>heap · <span data-stat="mem-percent" class="tabular text-foreground/80"></span></span>
<span>max <span data-stat="mem-max" class="tabular text-foreground/80"></span></span>
</div>
</article>
<article class="rise rise-4 ring-card relative overflow-hidden rounded-2xl bg-card p-5">
<article class="rise rise-4 ring-card relative flex flex-col overflow-hidden rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">Ticks per second</span>
<span class="text-sm text-muted-foreground">Ticks per second</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-chart"/></svg>
</div>
<div class="mt-4 flex items-baseline gap-1.5">
<span data-stat="tps-1m" data-tps-state class="tabular text-4xl font-medium tracking-tight text-success"></span>
<span class="font-mono text-sm text-muted-foreground">/ 20.00</span>
<span class="text-sm text-muted-foreground">/ 20.00</span>
</div>
<svg data-spark="tps" viewBox="0 0 600 60" preserveAspectRatio="none" class="mt-3 h-12 w-full overflow-visible">
<defs>
@@ -83,7 +83,7 @@ HOME
<polygon data-spark-area class="text-primary" fill="url(#spark-fill)" points=""/>
<polyline data-spark-line fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" class="text-primary" points=""/>
</svg>
<div class="mt-1 flex items-center justify-between font-mono text-[11px] text-muted-foreground">
<div class="mt-1 flex items-center justify-between text-xs text-muted-foreground">
<span>5m <span data-stat="tps-5m" class="tabular text-foreground/80"></span></span>
<span>15m <span data-stat="tps-15m" class="tabular text-foreground/80"></span></span>
</div>
@@ -93,51 +93,51 @@ HOME
<section class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<article class="rise rise-5 ring-card rounded-2xl bg-card p-5">
<article class="rise rise-5 ring-card flex flex-col rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">Uptime</span>
<span class="text-sm text-muted-foreground">Uptime</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-clock"/></svg>
</div>
<div class="mt-3 font-mono text-2xl tracking-tight">
<div class="my-auto font-mono text-2xl tracking-tight">
<span data-stat="uptime"></span>
</div>
</article>
<article class="rise rise-5 ring-card rounded-2xl bg-card p-5">
<article class="rise rise-5 ring-card flex flex-col rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">World</span>
<span class="text-sm text-muted-foreground">World</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-package"/></svg>
</div>
<dl class="mt-3 grid grid-cols-3 gap-2 font-mono text-sm">
<dl class="my-auto grid grid-cols-3 gap-2 text-sm">
<div>
<dt class="text-[10px] uppercase tracking-wider text-muted-foreground">Worlds</dt>
<dt class="text-xs text-muted-foreground">Worlds</dt>
<dd data-stat="worlds" class="mt-1 tabular text-lg text-foreground"></dd>
</div>
<div>
<dt class="text-[10px] uppercase tracking-wider text-muted-foreground">Chunks</dt>
<dt class="text-xs text-muted-foreground">Chunks</dt>
<dd data-stat="chunks" class="mt-1 tabular text-lg text-foreground"></dd>
</div>
<div>
<dt class="text-[10px] uppercase tracking-wider text-muted-foreground">Entities</dt>
<dt class="text-xs text-muted-foreground">Entities</dt>
<dd data-stat="entities" class="mt-1 tabular text-lg text-foreground"></dd>
</div>
</dl>
</article>
<article class="rise rise-6 ring-card rounded-2xl bg-card p-5">
<article class="rise rise-6 ring-card flex flex-col rounded-2xl bg-card p-5">
<div class="flex items-center justify-between">
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">Plugins</span>
<span class="text-sm text-muted-foreground">Plugins</span>
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-code"/></svg>
</div>
<div class="mt-3 flex items-baseline gap-2">
<span data-stat="plugins" class="tabular text-2xl font-medium tracking-tight"></span>
<span class="text-sm text-muted-foreground">active</span>
</div>
<div class="mt-3 flex gap-2">
<a href="/api/commands/" class="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 font-mono text-[11px] text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground">
<div class="mt-auto flex gap-2 pt-3">
<a href="/api/commands/" class="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground">
<svg class="size-3" aria-hidden="true"><use href="#i-arrow-right"/></svg> commands
</a>
<a href="/api/schematics/download/" class="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 font-mono text-[11px] text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground">
<a href="/api/schematics/download/" class="inline-flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground">
<svg class="size-3" aria-hidden="true"><use href="#i-arrow-right"/></svg> schematics
</a>
</div>
+2 -2
View File
@@ -2,13 +2,13 @@ Players
PLAYERS
<section class="rise flex flex-wrap items-end justify-between gap-3">
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Players</h1>
<span class="font-mono text-xs text-muted-foreground tabular">
<span class="text-sm text-muted-foreground tabular">
<span class="text-foreground">${player_count}</span> / <span>${player_max}</span> online
</span>
</section>
<section class="rise rise-1 mt-6 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center">
<label class="ring-card relative flex h-10 flex-1 items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30">
<label class="ring-card relative flex h-10 w-full items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-md">
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-search"/></svg>
<input id="player-filter"
type="text"
+3 -3
View File
@@ -4,9 +4,9 @@ PUNISHMENTS
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">Punishments</h1>
</section>
<section class="rise rise-1 mt-6 max-w-2xl">
<form onsubmit="event.preventDefault(); redirect();" class="flex flex-col gap-3 sm:flex-row">
<label class="ring-card relative flex h-10 flex-1 items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30">
<section class="rise rise-1 mt-6">
<form onsubmit="event.preventDefault(); redirect();" class="flex flex-col gap-3 sm:flex-row sm:items-center">
<label class="ring-card relative flex h-10 w-full items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-md">
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-search"/></svg>
<input id="uuid"
type="text"
@@ -7,16 +7,16 @@ PUNISHMENTS
alt="" loading="lazy" width="48" height="48">
<div>
<h1 class="text-3xl font-medium tracking-tight md:text-4xl">${player_name}</h1>
<p class="mt-1 font-mono text-[11px] text-muted-foreground break-all">${player_uuid}</p>
<p class="mt-1 font-mono text-xs text-muted-foreground break-all">${player_uuid}</p>
</div>
</div>
<span class="font-mono text-xs text-muted-foreground tabular">
<span class="text-sm text-muted-foreground tabular">
<span class="text-foreground">${punishment_count}</span> ${punishment_label}
</span>
</section>
<section class="rise rise-1 mt-4 flex flex-wrap items-center gap-3">
<label class="ring-card relative flex h-10 flex-1 items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-md">
<label class="ring-card relative flex h-10 w-full items-center gap-2 rounded-full bg-card px-3 transition-colors focus-within:bg-background focus-within:ring-2 focus-within:ring-ring/30 sm:max-w-md">
<svg class="size-4 text-muted-foreground" aria-hidden="true"><use href="#i-search"/></svg>
<input id="punish-filter"
type="text"
@@ -37,7 +37,7 @@ PUNISHMENTS
${punishments}
</section>
<p id="punish-empty" class="rise rise-2 mt-4 hidden font-mono text-[11px] text-muted-foreground">No punishments match those filters.</p>
<p id="punish-empty" class="rise rise-2 mt-4 hidden text-sm text-muted-foreground">No punishments match those filters.</p>
<script>
(function () {
@@ -57,18 +57,22 @@ ${punishments}
? 'bg-primary text-primary-foreground'
: 'bg-card ring-card text-muted-foreground hover:bg-muted hover:text-foreground';
return `<button type="button" data-group="${group}" data-value="${value}"
class="inline-flex h-7 items-center rounded-full px-3 font-mono text-[11px] uppercase tracking-wider transition-colors ${cls}">${label}</button>`;
class="inline-flex h-7 items-center rounded-full px-3 text-xs transition-colors ${cls}">${label}</button>`;
}
function titleCase(s) {
return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : s;
}
function renderChips() {
const parts = [];
parts.push(chip('all', 'type', 'all', state.type === 'all'));
types.forEach(t => parts.push(chip(t.toLowerCase(), 'type', t, state.type === t)));
parts.push(chip('All', 'type', 'all', state.type === 'all'));
types.forEach(t => parts.push(chip(titleCase(t), 'type', t, state.type === t)));
if (hasStatus) {
parts.push('<span class="mx-1 h-4 w-px bg-border"></span>');
parts.push(chip('any', 'status', 'all', state.status === 'all'));
parts.push(chip('active', 'status', 'active', state.status === 'active'));
parts.push(chip('expired', 'status', 'expired', state.status === 'expired'));
parts.push(chip('Any', 'status', 'all', state.status === 'all'));
parts.push(chip('Active', 'status', 'active', state.status === 'active'));
parts.push(chip('Expired', 'status', 'expired', state.status === 'expired'));
}
chips.innerHTML = parts.join('');
}
@@ -25,8 +25,8 @@ SCHEMATICS
<table id="schemList" class="w-full text-sm">
<thead class="border-b border-border/60 bg-muted/40">
<tr>
<th scope="col" class="px-4 py-2.5 text-left font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Name</th>
<th scope="col" class="px-4 py-2.5 text-right font-mono text-[11px] font-medium uppercase tracking-wider text-muted-foreground">Size</th>
<th scope="col" class="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Name</th>
<th scope="col" class="px-4 py-2.5 text-right text-xs font-medium text-muted-foreground">Size</th>
<th scope="col" class="w-8"></th>
</tr>
</thead>
@@ -21,7 +21,7 @@ SCHEMATICS
<svg class="size-3.5" aria-hidden="true"><use href="#i-arrow-right"/></svg>
</button>
</form>
<p id="picked-name" class="mt-3 font-mono text-[11px] text-muted-foreground"></p>
<p id="picked-name" class="mt-3 text-xs text-muted-foreground"></p>
</section>
<script>
+98 -60
View File
@@ -5,18 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${TITLE} &middot; Plex HTTPD</title>
<script>
(function () {
try {
const stored = localStorage.getItem('plex-theme');
const dark = stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
if (dark) document.documentElement.classList.add('dark');
} catch (e) {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) document.documentElement.classList.add('dark');
}
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
@@ -25,7 +13,7 @@
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant dark (@media (prefers-color-scheme: dark));
@theme {
--font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
@@ -67,29 +55,31 @@
</style>
<style>
.dark {
--color-background: oklch(0.145 0 0);
--color-foreground: oklch(0.985 0 0);
--color-card: oklch(0.205 0 0);
--color-card-foreground: oklch(0.985 0 0);
--color-popover: oklch(0.205 0 0);
--color-popover-foreground: oklch(0.985 0 0);
--color-primary: oklch(0.62 0.235 264);
--color-primary-foreground: oklch(0.985 0 0);
--color-secondary: oklch(0.269 0 0);
--color-secondary-foreground: oklch(0.985 0 0);
--color-muted: oklch(0.269 0 0);
--color-muted-foreground: oklch(0.708 0 0);
--color-accent: oklch(0.371 0 0);
--color-accent-foreground: oklch(0.985 0 0);
--color-destructive: oklch(0.704 0.191 22);
--color-success: oklch(0.74 0.18 145);
--color-warning: oklch(0.82 0.16 75);
--color-border: oklch(1 0 0 / 10%);
--color-input: oklch(1 0 0 / 15%);
--color-ring: oklch(0.62 0.235 264);
--color-surface: oklch(0.2 0 0);
--color-surface-foreground: oklch(0.708 0 0);
@media (prefers-color-scheme: dark) {
:root {
--color-background: oklch(0.145 0 0);
--color-foreground: oklch(0.985 0 0);
--color-card: oklch(0.205 0 0);
--color-card-foreground: oklch(0.985 0 0);
--color-popover: oklch(0.205 0 0);
--color-popover-foreground: oklch(0.985 0 0);
--color-primary: oklch(0.62 0.235 264);
--color-primary-foreground: oklch(0.985 0 0);
--color-secondary: oklch(0.269 0 0);
--color-secondary-foreground: oklch(0.985 0 0);
--color-muted: oklch(0.269 0 0);
--color-muted-foreground: oklch(0.708 0 0);
--color-accent: oklch(0.371 0 0);
--color-accent-foreground: oklch(0.985 0 0);
--color-destructive: oklch(0.704 0.191 22);
--color-success: oklch(0.74 0.18 145);
--color-warning: oklch(0.82 0.16 75);
--color-border: oklch(1 0 0 / 10%);
--color-input: oklch(1 0 0 / 15%);
--color-ring: oklch(0.62 0.235 264);
--color-surface: oklch(0.2 0 0);
--color-surface-foreground: oklch(0.708 0 0);
}
}
</style>
@@ -186,10 +176,6 @@
/* Maia: subtle ring on cards, no shadow */
.ring-card { box-shadow: inset 0 0 0 1px oklch(from var(--color-foreground) l c h / 0.08); }
/* Hide scrollbar but keep functionality */
.nav-scroll::-webkit-scrollbar { display: none; }
.nav-scroll { scrollbar-width: none; }
</style>
</head>
<body class="bg-background text-foreground min-h-screen antialiased">
@@ -291,13 +277,13 @@
<div class="layer-content flex min-h-screen flex-col">
<header class="sticky top-0 z-50 border-b border-border/60 bg-background/75 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
<div class="mx-auto flex h-14 max-w-7xl items-center gap-6 px-6">
<div class="mx-auto flex h-14 max-w-7xl items-center gap-4 px-6">
<a href="/" class="flex items-center gap-2.5 text-foreground transition-opacity hover:opacity-80">
<img src="/assets/plexlogo.webp" alt="" class="size-7 rounded-md" width="28" height="28">
<span class="text-sm font-semibold tracking-tight">Plex HTTPD</span>
</a>
<nav class="nav-scroll flex flex-1 items-center gap-1 overflow-x-auto">
<nav class="hidden flex-1 items-center gap-1 md:flex">
<a class="nav-link ${ACTIVE_HOME} group inline-flex h-8 items-center gap-1.5 rounded-full px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/">
<svg class="size-3.5 opacity-70 group-hover:opacity-100 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-dashboard"/></svg>
Overview
@@ -324,14 +310,48 @@
</a>
</nav>
<div class="flex items-center gap-2">
<div id="plex-auth" class="hidden md:flex items-center gap-2"></div>
<button type="button" onclick="window.plexToggleTheme()" class="ring-card inline-flex size-8 items-center justify-center rounded-full bg-card text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" aria-label="Toggle theme">
<svg class="size-4 hidden dark:block" aria-hidden="true"><use href="#i-sun"/></svg>
<svg class="size-4 block dark:hidden" aria-hidden="true"><use href="#i-moon"/></svg>
<div class="flex flex-1 items-center justify-end gap-2 md:flex-initial">
<div data-plex-auth class="hidden items-center gap-2 md:flex"></div>
<button id="plex-nav-toggle" type="button" class="ring-card inline-flex size-8 items-center justify-center rounded-full bg-card text-muted-foreground transition-colors hover:bg-muted hover:text-foreground md:hidden" aria-label="Toggle menu" aria-expanded="false" aria-controls="plex-mobile-menu">
<svg class="size-4 block" data-icon="open" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M4 7h16M4 12h16M4 17h16"/>
</svg>
<svg class="size-4 hidden" data-icon="close" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
</div>
<div id="plex-mobile-menu" class="hidden border-t border-border/60 md:hidden">
<nav class="mx-auto flex max-w-7xl flex-col gap-1 px-4 py-3">
<a class="nav-link ${ACTIVE_HOME} group flex h-10 items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/">
<svg class="size-4 opacity-70 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-dashboard"/></svg>
Overview
</a>
<a class="nav-link ${ACTIVE_PLAYERS} group flex h-10 items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/players/">
<svg class="size-4 opacity-70 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-users"/></svg>
Players
</a>
<a class="nav-link ${ACTIVE_COMMANDS} group flex h-10 items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/api/commands/">
<svg class="size-4 opacity-70 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-code"/></svg>
Commands
</a>
<a class="nav-link ${ACTIVE_PUNISHMENTS} group flex h-10 items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/punishments/">
<svg class="size-4 opacity-70 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-gavel"/></svg>
Punishments
</a>
<a class="nav-link ${ACTIVE_INDEFBANS} group flex h-10 items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/indefbans/">
<svg class="size-4 opacity-70 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-lock"/></svg>
Indef Bans
</a>
<a class="nav-link ${ACTIVE_SCHEMATICS} group flex h-10 items-center gap-2.5 rounded-xl px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[active=true]:bg-muted data-[active=true]:text-foreground" href="/api/schematics/download/">
<svg class="size-4 opacity-70 group-data-[active=true]:text-primary group-data-[active=true]:opacity-100" aria-hidden="true"><use href="#i-package"/></svg>
Schematics
</a>
<div data-plex-auth class="mt-2 flex flex-col gap-2 border-t border-border/60 pt-3"></div>
</nav>
</div>
</header>
<main class="mx-auto w-full max-w-7xl flex-1 px-6 py-10 md:py-14">
@@ -341,28 +361,46 @@
</div>
<script>
window.plexToggleTheme = function () {
const isDark = document.documentElement.classList.toggle('dark');
try { localStorage.setItem('plex-theme', isDark ? 'dark' : 'light'); } catch (e) {}
};
document.querySelectorAll('.nav-link').forEach(a => {
if (a.classList.contains('active')) a.setAttribute('data-active', 'true');
});
(function () {
const mount = document.getElementById('plex-auth');
if (!mount) return;
const linkClasses = 'ring-card inline-flex h-8 items-center gap-1.5 rounded-full bg-card px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground';
const toggle = document.getElementById('plex-nav-toggle');
const menu = document.getElementById('plex-mobile-menu');
if (!toggle || !menu) return;
const setOpen = (open) => {
menu.classList.toggle('hidden', !open);
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
toggle.querySelector('[data-icon="open"]').classList.toggle('hidden', open);
toggle.querySelector('[data-icon="close"]').classList.toggle('hidden', !open);
};
toggle.addEventListener('click', () => setOpen(menu.classList.contains('hidden')));
window.matchMedia('(min-width: 768px)').addEventListener('change', e => { if (e.matches) setOpen(false); });
})();
(function () {
const mounts = document.querySelectorAll('[data-plex-auth]');
if (!mounts.length) return;
const inlineLink = 'ring-card inline-flex h-8 items-center gap-1.5 rounded-full bg-card px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground';
const blockLink = 'flex h-10 items-center gap-2.5 rounded-xl bg-card px-3 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground';
const escape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const here = window.location.pathname + window.location.search;
const loginHref = '/oauth2/login?return_to=' + encodeURIComponent(here);
fetch('/oauth2/me', { credentials: 'same-origin', headers: { 'Accept': 'application/json' } })
.then(r => r.json().catch(() => ({})).then(j => ({ status: r.status, body: j })))
.then(({ status, body }) => {
if (body && body.authenticated === false && body.reason === 'disabled') return;
if (status === 200 && body.authenticated) {
mount.innerHTML = '<span class="text-xs text-muted-foreground">' + escape(body.username) + '</span>'
+ '<a href="/oauth2/logout" class="' + linkClasses + '">Sign out</a>';
} else {
mount.innerHTML = '<a href="/oauth2/login" class="' + linkClasses + '">Sign in</a>';
}
mounts.forEach(mount => {
const block = mount.classList.contains('flex-col');
const linkClasses = block ? blockLink : inlineLink;
if (status === 200 && body.authenticated) {
const label = block
? '<span class="px-3 text-xs text-muted-foreground">Signed in as ' + escape(body.username) + '</span>'
: '<span class="text-xs text-muted-foreground">' + escape(body.username) + '</span>';
mount.innerHTML = label + '<a href="/oauth2/logout" class="' + linkClasses + '">Sign out</a>';
} else {
mount.innerHTML = '<a href="' + loginHref + '" class="' + linkClasses + '">Sign in</a>';
}
});
})
.catch(() => {});
})();