refactor(wireguard): remove dead code + DRY the WG surface (no behavior change)

Verified dead-code + duplication cleanup over the WireGuard feature surface
(found via an audit workflow, every item adversarially re-verified by grep):

Dead code removed:
- lang keys not_configured_title / not_configured_body (both locales) — orphaned
  when the unconfigured empty state became the in-page setup form (setup_title /
  setup_intro). 0 references anywhere. de/en parity kept (83/83).
- WgTraffic::series() 'buckets' return key — written, never read by blade/route/test.
- blade $fmtB byte-formatter — byte-identical duplicate of the top-level $fmtBytes
  already in scope; collapsed onto $fmtBytes.

DRY / readability (behavior-preserving):
- Index: PEER_NAME_RE constant (was the same regex literal at 5 sites), portInRange()
  + clampWindow() helpers (window default now self::WINDOWS[0]), openConfirm() helper
  (5 confirm-modal openers shared one dispatch), onFlag() helper (2 apply handlers).
- WgBridge: validPort() helper (port check was duplicated verbatim).
- clusev-wg.sh: _write_server_conf() shared by both setup paths (the byte-identical
  server bring-up was copy-pasted); single CIDR_RE constant for the 3 subnet checks;
  fixed a stale header comment that still claimed SSH is "NEVER matched" (the v0.9.40
  ssh-lock can match 22); documented the defence-in-depth re-validation in _set_*.
- WgStatus: documented server.pubkey is part of the /wg-status.json contract (kept).

No DB/migration changes (peer_name write-only column left intact — needs a
coordinated migration). 348 tests pass, shellcheck clean, Pint clean, R12 verified
in both the configured + unconfigured states (200, no console errors, no leaked
keys, traffic formatter renders 1.5 MB / 200 KB correctly).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-21 16:33:41 +02:00
parent 2b245c3d3a
commit 9c0d390365
8 changed files with 110 additions and 119 deletions

View File

@ -22,10 +22,13 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
#[Layout('layouts.app')]
class Index extends Component
{
public int $window = 3600;
public const WINDOWS = [3600, 86400, 604800];
/** Peer-name charset, shared by every app-side validation site (the host re-validates too). */
private const PEER_NAME_RE = '/^[A-Za-z0-9._-]{1,64}$/';
public int $window = self::WINDOWS[0];
public string $newPeer = '';
public string $newEndpoint = '';
@ -58,12 +61,12 @@ class Index extends Component
public function setWindow(int $seconds): void
{
$this->window = in_array($seconds, self::WINDOWS, true) ? $seconds : 3600;
$this->window = $this->clampWindow($seconds);
}
public function addPeer(WgBridge $bridge): void
{
$this->validate(['newPeer' => ['required', 'regex:/^[A-Za-z0-9._-]{1,64}$/']], [
$this->validate(['newPeer' => ['required', 'regex:'.self::PEER_NAME_RE]], [
'newPeer.regex' => __('wireguard.peer_name_invalid'),
'newPeer.required' => __('wireguard.peer_name_invalid'),
]);
@ -83,14 +86,14 @@ class Index extends Component
'setupSubnet' => ['required', 'regex:#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#'],
'setupPort' => ['required', 'regex:/^\d{1,5}$/'],
'setupEndpoint' => ['nullable', 'regex:/^[A-Za-z0-9.:_-]{1,128}$/'],
'setupPeer' => ['required', 'regex:/^[A-Za-z0-9._-]{1,64}$/'],
'setupPeer' => ['required', 'regex:'.self::PEER_NAME_RE],
], [
'setupSubnet.regex' => __('wireguard.subnet_invalid'),
'setupPort.regex' => __('wireguard.port_invalid'),
'setupEndpoint.regex' => __('wireguard.endpoint_invalid'),
'setupPeer.regex' => __('wireguard.peer_name_invalid'),
]);
if ((int) $this->setupPort < 1 || (int) $this->setupPort > 65535) {
if (! $this->portInRange($this->setupPort)) {
$this->addError('setupPort', __('wireguard.port_invalid'));
return;
@ -109,24 +112,13 @@ class Index extends Component
/** Opens the wire-elements/modal confirm dialog for peer removal. */
public function confirmRemovePeer(string $name): void
{
if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
if (preg_match(self::PEER_NAME_RE, $name) !== 1) {
return;
}
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => __('wireguard.remove_confirm_title'),
'body' => __('wireguard.remove_confirm_body'),
'confirmLabel' => __('wireguard.remove'),
'danger' => true,
'icon' => 'trash',
'notify' => '',
// No audit descriptor — the apply path (removePeer) audits, so the confirm modal
// must NOT also audit (avoids a double audit row for one action).
'token' => ConfirmToken::issue('wgPeerRemoved', ['name' => $name]),
],
);
$this->openConfirm('wgPeerRemoved', ['name' => $name],
__('wireguard.remove_confirm_title'), __('wireguard.remove_confirm_body'),
__('wireguard.remove'), danger: true, icon: 'trash');
}
/** Apply handler — called after the ConfirmAction modal confirms the removal. */
@ -140,7 +132,7 @@ class Index extends Component
}
$name = $payload['params']['name'] ?? '';
if ($name === '' || preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1) {
if ($name === '' || preg_match(self::PEER_NAME_RE, $name) !== 1) {
return;
}
@ -149,7 +141,7 @@ class Index extends Component
public function removePeer(WgBridge $bridge, string $name): void
{
if (preg_match('/^[A-Za-z0-9._-]{1,64}$/', $name) !== 1 || ! $this->throttle()) {
if (preg_match(self::PEER_NAME_RE, $name) !== 1 || ! $this->throttle()) {
return;
}
$this->pendingId = $bridge->request('remove-peer', ['name' => $name]);
@ -174,18 +166,11 @@ class Index extends Component
public function confirmGate(bool $on): void
{
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => $on ? __('wireguard.gate_on_title') : __('wireguard.gate_off_title'),
'body' => $on ? __('wireguard.gate_on_body') : __('wireguard.gate_off_body'),
'confirmLabel' => $on ? __('wireguard.gate_turn_on') : __('wireguard.gate_turn_off'),
'danger' => ! $on,
'icon' => 'shield',
'notify' => '',
'token' => ConfirmToken::issue('wgGateToggle', ['on' => $on ? '1' : '0']),
],
);
$this->openConfirm('wgGateToggle', ['on' => $on ? '1' : '0'],
$on ? __('wireguard.gate_on_title') : __('wireguard.gate_off_title'),
$on ? __('wireguard.gate_on_body') : __('wireguard.gate_off_body'),
$on ? __('wireguard.gate_turn_on') : __('wireguard.gate_turn_off'),
danger: ! $on, icon: 'shield');
}
#[On('wgGateToggle')]
@ -197,7 +182,7 @@ class Index extends Component
return;
}
$this->runGate(($payload['params']['on'] ?? '0') === '1', $bridge);
$this->runGate($this->onFlag($payload), $bridge);
}
public function runGate(bool $on, ?WgBridge $bridge = null): void
@ -218,18 +203,11 @@ class Index extends Component
*/
public function confirmSshGate(bool $on): void
{
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => $on ? __('wireguard.ssh_lock_on_title') : __('wireguard.ssh_lock_off_title'),
'body' => $on ? __('wireguard.ssh_lock_on_body') : __('wireguard.ssh_lock_off_body'),
'confirmLabel' => $on ? __('wireguard.ssh_lock_turn_on') : __('wireguard.ssh_lock_turn_off'),
'danger' => $on,
'icon' => 'alert',
'notify' => '',
'token' => ConfirmToken::issue('wgSshGate', ['on' => $on ? '1' : '0']),
],
);
$this->openConfirm('wgSshGate', ['on' => $on ? '1' : '0'],
$on ? __('wireguard.ssh_lock_on_title') : __('wireguard.ssh_lock_off_title'),
$on ? __('wireguard.ssh_lock_on_body') : __('wireguard.ssh_lock_off_body'),
$on ? __('wireguard.ssh_lock_turn_on') : __('wireguard.ssh_lock_turn_off'),
danger: $on, icon: 'alert');
}
#[On('wgSshGate')]
@ -241,7 +219,7 @@ class Index extends Component
return;
}
$this->runSshGate(($payload['params']['on'] ?? '0') === '1', $bridge);
$this->runSshGate($this->onFlag($payload), $bridge);
}
public function runSshGate(bool $on, ?WgBridge $bridge = null): void
@ -261,24 +239,15 @@ class Index extends Component
['newPort' => ['required', 'regex:/^\d{1,5}$/']],
['newPort.regex' => __('wireguard.port_invalid'), 'newPort.required' => __('wireguard.port_invalid')],
);
if ((int) $this->newPort < 1 || (int) $this->newPort > 65535) {
if (! $this->portInRange($this->newPort)) {
$this->addError('newPort', __('wireguard.port_invalid'));
return;
}
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => __('wireguard.port_confirm_title'),
'body' => __('wireguard.port_confirm_body'),
'confirmLabel' => __('wireguard.apply'),
'danger' => true,
'icon' => 'alert',
'notify' => '',
'token' => ConfirmToken::issue('wgSetPort', ['port' => $this->newPort]),
],
);
$this->openConfirm('wgSetPort', ['port' => $this->newPort],
__('wireguard.port_confirm_title'), __('wireguard.port_confirm_body'),
__('wireguard.apply'), danger: true, icon: 'alert');
}
#[On('wgSetPort')]
@ -307,18 +276,9 @@ class Index extends Component
['newSubnet.regex' => __('wireguard.subnet_invalid'), 'newSubnet.required' => __('wireguard.subnet_invalid')],
);
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => __('wireguard.subnet_confirm_title'),
'body' => __('wireguard.subnet_confirm_body'),
'confirmLabel' => __('wireguard.apply'),
'danger' => true,
'icon' => 'alert',
'notify' => '',
'token' => ConfirmToken::issue('wgSetSubnet', ['subnet' => $this->newSubnet]),
],
);
$this->openConfirm('wgSetSubnet', ['subnet' => $this->newSubnet],
__('wireguard.subnet_confirm_title'), __('wireguard.subnet_confirm_body'),
__('wireguard.apply'), danger: true, icon: 'alert');
}
#[On('wgSetSubnet')]
@ -392,6 +352,47 @@ class Index extends Component
}, 'clusev-wireguard.conf', ['Content-Type' => 'text/plain; charset=UTF-8']);
}
/** Clamp a traffic window to one of the allowed values, falling back to the shortest. */
private function clampWindow(int $seconds): int
{
return in_array($seconds, self::WINDOWS, true) ? $seconds : self::WINDOWS[0];
}
/** True when a numeric-string port is within the valid UDP range. */
private function portInRange(string $port): bool
{
return (int) $port >= 1 && (int) $port <= 65535;
}
/** Decode the sealed on/off flag a gate-toggle confirm token carries. */
private function onFlag(array $payload): bool
{
return ($payload['params']['on'] ?? '0') === '1';
}
/**
* Open the shared ConfirmAction modal (R5) for one WG action. The issued token carries NO audit
* descriptor on purpose each apply handler audits exactly once itself, so auditing here too
* would double-count. Heading/body/label/danger/icon/params are the only per-action differences.
*
* @param array<string, string> $params
*/
private function openConfirm(string $event, array $params, string $heading, string $body, string $confirmLabel, bool $danger, string $icon): void
{
$this->dispatch('openModal',
component: 'modals.confirm-action',
arguments: [
'heading' => $heading,
'body' => $body,
'confirmLabel' => $confirmLabel,
'danger' => $danger,
'icon' => $icon,
'notify' => '',
'token' => ConfirmToken::issue($event, $params),
],
);
}
private function throttle(): bool
{
$key = 'wg-request:'.Auth::id();
@ -418,7 +419,7 @@ class Index extends Component
public function render(WgStatus $wg, WgTraffic $traffic)
{
$window = in_array($this->window, self::WINDOWS, true) ? $this->window : 3600;
$window = $this->clampWindow($this->window);
return view('livewire.wireguard.index', [
'status' => $wg->read(),

View File

@ -100,7 +100,7 @@ class WgBridge
return ['endpoint' => $ep];
case 'set-port':
$port = (string) ($args['port'] ?? '');
if (preg_match('/^\d{1,5}$/', $port) !== 1 || (int) $port < 1 || (int) $port > 65535) {
if (! $this->validPort($port)) {
throw new \InvalidArgumentException('invalid port');
}
@ -120,7 +120,7 @@ class WgBridge
if (preg_match('#^\d{1,3}(\.\d{1,3}){3}/\d{1,2}$#', $subnet) !== 1) {
throw new \InvalidArgumentException('invalid subnet');
}
if (preg_match('/^\d{1,5}$/', $port) !== 1 || (int) $port < 1 || (int) $port > 65535) {
if (! $this->validPort($port)) {
throw new \InvalidArgumentException('invalid port');
}
if ($endpoint !== '' && preg_match('/^[A-Za-z0-9.:_-]{1,128}$/', $endpoint) !== 1) {
@ -135,4 +135,10 @@ class WgBridge
return [];
}
/** A valid 165535 UDP port given as a numeric string. */
private function validPort(string $port): bool
{
return preg_match('/^\d{1,5}$/', $port) === 1 && (int) $port >= 1 && (int) $port <= 65535;
}
}

View File

@ -71,6 +71,7 @@ class WgStatus
'server_ip' => (string) ($data['server']['server_ip'] ?? ''),
'port' => (int) ($data['server']['port'] ?? 0),
'endpoint' => (string) ($data['server']['endpoint'] ?? ''),
// Not rendered by the page, but part of the /wg-status.json response contract (API consumers).
'pubkey' => (string) ($data['server']['pubkey'] ?? ''),
],
'peers' => $peers,

View File

@ -12,7 +12,7 @@ use App\Models\WgTrafficSample;
class WgTraffic
{
/**
* @return array{window:int, buckets:int, points:array<int,array{t:int,down:int,up:int}>,
* @return array{window:int, points:array<int,array{t:int,down:int,up:int}>,
* total_down:int, total_up:int, peak_down:int, peak_up:int}
*/
public function series(int $windowSeconds, int $buckets = 60): array
@ -59,7 +59,6 @@ class WgTraffic
return [
'window' => $windowSeconds,
'buckets' => $buckets,
'points' => $points,
'total_down' => array_sum($down),
'total_up' => array_sum($up),

View File

@ -2,8 +2,9 @@
# Clusev WireGuard gate + peer CLI (HOST-side, root). Invoked via `clusev wg <cmd>` (the host
# wrapper execs this from the deployed repo tree). Puts the panel behind a WG tunnel: the VM is the
# WG server, operator devices peer in, and TCP 80/443 is filtered to the WG subnet by a dedicated
# iptables chain hung off DOCKER-USER. SSH (22) and the WG UDP port are NEVER matched — you can
# always SSH in and run `clusev wg down`. The gate is OFF until `clusev wg up`.
# iptables chain hung off DOCKER-USER. The WG UDP port is NEVER matched. SSH (22) is only filtered if
# the operator opts into the SEPARATE ssh-lock (default off); the server console + `clusev wg down`
# are always a way back in. The panel gate is OFF until `clusev wg up`.
set -euo pipefail
WG_IF="wg0"
@ -20,6 +21,8 @@ PANEL_PORTS="80,443" # see spec §3 post-DNAT note: matches the published cont
# current SSH session, only NEW non-tunnel SSH. Off unless the operator explicitly enables it.
SSH_MARKER="${STATE_DIR}/wg-ssh-gate.enabled"
SSH_CHAIN="CLUSEV-WG-SSH"
# Shape-check for a /N CIDR (single source for the subnet validators below). Single-quoted = literal.
readonly CIDR_RE='^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$'
# Project root (for the run/ bridge dir). The collect timer sets CLUSEV_DIR; otherwise derive it
# from this script's location (docker/wg/clusev-wg.sh -> ../..).
@ -102,7 +105,7 @@ ssh_gate_apply() {
[ -n "${WG_SUBNET:-}" ] || return 0
# Re-validate the subnet shape before it reaches `iptables -s`; a malformed value fails open
# (no DROP) rather than aborting mid-chain and leaving SSH half-filtered.
printf '%s' "$WG_SUBNET" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' || return 0
printf '%s' "$WG_SUBNET" | grep -qE "$CIDR_RE" || return 0
iptables -N "$SSH_CHAIN" 2>/dev/null || iptables -F "$SSH_CHAIN"
iptables -A "$SSH_CHAIN" -i lo -j RETURN
iptables -A "$SSH_CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN # never cut a live session
@ -297,7 +300,7 @@ _setup_noninteractive() {
local subnet="$1" server_ip="$2" port="$3" endpoint="$4" peer1="$5"
have wg || { echo "wireguard-tools fehlt" >&2; return 1; }
[ -f "$WG_CONF" ] && { echo "WireGuard ist bereits eingerichtet" >&2; return 1; }
printf '%s' "$subnet" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' || { echo "Ungueltiges Subnetz" >&2; return 1; }
printf '%s' "$subnet" | grep -qE "$CIDR_RE" || { echo "Ungueltiges Subnetz" >&2; return 1; }
case "$server_ip" in ''|*[!0-9.]*) echo "Ungueltige Server-IP" >&2; return 1 ;; esac
case "$port" in ''|*[!0-9]*) echo "Ungueltiger Port" >&2; return 1 ;; esac
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || { echo "Port ausserhalb 1-65535" >&2; return 1; }
@ -305,6 +308,16 @@ _setup_noninteractive() {
valid_name "$peer1" || { echo "Ungueltiger Peer-Name" >&2; return 1; }
subnet_collides "$subnet" && { echo "Subnetz kollidiert mit einer bestehenden Route/Adresse" >&2; return 1; }
_write_server_conf "$subnet" "$server_ip" "$port" "$endpoint"
systemctl enable --now "wg-quick@${WG_IF}" || { echo "wg-quick@${WG_IF} konnte nicht gestartet werden" >&2; return 1; }
_peer_add "$peer1"
}
# Generate the server keypair and write wg0.conf + wg.env (both 0600). Shared by both setup paths
# (dashboard _setup_noninteractive + interactive cmd_setup); the caller does `systemctl enable --now`
# and the first-peer add, since those differ (error style return-vs-die, _peer_add vs cmd_add_peer).
_write_server_conf() {
local subnet="$1" server_ip="$2" port="$3" endpoint="$4"
umask 077; mkdir -p "$WG_DIR" "$STATE_DIR"
local srv_priv srv_pub prefix
srv_priv="$(wg genkey)"; srv_pub="$(printf '%s' "$srv_priv" | wg pubkey)"
@ -324,8 +337,6 @@ WG_ENDPOINT=${endpoint}
WG_SERVER_PUBKEY=${srv_pub}
EOF
chmod 600 "$WG_ENV"
systemctl enable --now "wg-quick@${WG_IF}" || { echo "wg-quick@${WG_IF} konnte nicht gestartet werden" >&2; return 1; }
_peer_add "$peer1"
}
cmd_setup() {
@ -368,28 +379,7 @@ cmd_setup() {
[ -n "$endpoint" ] || die "Endpoint erforderlich."
read -rp " Name des ersten Peers [client-1]: " peer1 || true; peer1="${peer1:-client-1}"
umask 077; mkdir -p "$WG_DIR" "$STATE_DIR"
local srv_priv srv_pub prefix
srv_priv="$(wg genkey)"; srv_pub="$(printf '%s' "$srv_priv" | wg pubkey)"
prefix="${subnet##*/}"
cat > "$WG_CONF" <<EOF
[Interface]
Address = ${server_ip}/${prefix}
ListenPort = ${port}
PrivateKey = ${srv_priv}
EOF
chmod 600 "$WG_CONF"
cat > "$WG_ENV" <<EOF
WG_SUBNET=${subnet}
WG_SERVER_IP=${server_ip}
WG_PORT=${port}
WG_ENDPOINT=${endpoint}
WG_SERVER_PUBKEY=${srv_pub}
EOF
chmod 600 "$WG_ENV"
_write_server_conf "$subnet" "$server_ip" "$port" "$endpoint"
systemctl enable --now "wg-quick@${WG_IF}" || die "wg-quick@${WG_IF} konnte nicht gestartet werden — 'journalctl -u wg-quick@${WG_IF}' pruefen."
cmd_add_peer "$peer1"
echo
@ -491,12 +481,14 @@ cmd_serve_request() {
# ── settings mutators (cores: no prints; errors to stderr; non-zero on fail) ──────────────────
_set_endpoint() {
local ep="$1"; require_setup
# Re-validated here (defence-in-depth): the bare CLI path cmd_set_endpoint passes $1 unchecked.
case "$ep" in *[!A-Za-z0-9.:_-]*|'') echo "bad endpoint" >&2; return 1 ;; esac
sed -i "s#^WG_ENDPOINT=.*#WG_ENDPOINT=${ep}#" "$WG_ENV"
}
_set_port() {
local port="$1"; require_setup
# Re-validated here: cmd_set_port passes $1 unchecked; also adds the 1-65535 range the bridge regex omits.
case "$port" in ''|*[!0-9]*) echo "bad port" >&2; return 1 ;; esac
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || { echo "port out of range" >&2; return 1; }
sed -i "s#^ListenPort = .*#ListenPort = ${port}#" "$WG_CONF"
@ -506,8 +498,9 @@ _set_port() {
_set_subnet() {
local subnet="$1"; require_setup
# Re-validated here (defence-in-depth): the bare CLI path cmd_set_subnet passes $1 unchecked.
case "$subnet" in *[!0-9./]*|'') echo "bad subnet" >&2; return 1 ;; esac
printf '%s' "$subnet" | grep -qE '^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$' || { echo "bad subnet" >&2; return 1; }
printf '%s' "$subnet" | grep -qE "$CIDR_RE" || { echo "bad subnet" >&2; return 1; }
subnet_collides "$subnet" && { echo "subnet collides with an existing route/address" >&2; return 1; }
local prefix server_ip
prefix="${subnet##*/}"

View File

@ -5,8 +5,6 @@ return [
'eyebrow' => 'Netzwerk',
'heading' => 'WireGuard',
'subtitle' => 'Tunnel-Zugang zum Panel — Live-Status.',
'not_configured_title' => 'WireGuard ist nicht eingerichtet',
'not_configured_body' => 'Auf dem Host einrichten: per SSH "sudo clusev wg setup". Danach erscheint hier der Live-Status.',
'stale' => 'Status veraltet — der Host-Collector antwortet nicht. Läuft der Stack?',
'gate' => 'Gate (Panel-Sperre)',
'gate_on' => 'aktiv — Panel nur über den Tunnel',

View File

@ -5,8 +5,6 @@ return [
'eyebrow' => 'Network',
'heading' => 'WireGuard',
'subtitle' => 'Tunnel access to the panel — live status.',
'not_configured_title' => 'WireGuard is not set up',
'not_configured_body' => 'Set it up on the host: over SSH run "sudo clusev wg setup". The live status appears here afterwards.',
'stale' => 'Status is stale — the host collector is not responding. Is the stack running?',
'gate' => 'Gate (panel lock)',
'gate_on' => 'on — panel only via the tunnel',

View File

@ -79,11 +79,6 @@
$win = collect($windows)->mapWithKeys(fn ($s) => [$s => match ($s) {
3600 => __('wireguard.window_1h'), 86400 => __('wireguard.window_24h'), default => __('wireguard.window_7d'),
}])->all();
$fmtB = function (int $n): string {
$u = ['B', 'KB', 'MB', 'GB', 'TB']; $i = 0; $v = (float) $n;
while ($v >= 1024 && $i < count($u) - 1) { $v /= 1024; $i++; }
return ($i === 0 ? (string) $n : number_format($v, 1)).' '.$u[$i];
};
$pts = $traffic['points'];
$n = max(1, count($pts) - 1);
$peak = max(1, $traffic['peak_down'], $traffic['peak_up']);
@ -102,8 +97,8 @@
<x-panel :title="__('wireguard.traffic_title')" :padded="false">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-line px-4 py-3 sm:px-5">
<div class="flex items-center gap-4 font-mono text-[11px]">
<span class="text-accent-text">{{ __('wireguard.total_down') }}: <span class="text-ink-2">{{ $fmtB($traffic['total_down']) }}</span></span>
<span class="text-cyan">{{ __('wireguard.total_up') }}: <span class="text-ink-2">{{ $fmtB($traffic['total_up']) }}</span></span>
<span class="text-accent-text">{{ __('wireguard.total_down') }}: <span class="text-ink-2">{{ $fmtBytes($traffic['total_down']) }}</span></span>
<span class="text-cyan">{{ __('wireguard.total_up') }}: <span class="text-ink-2">{{ $fmtBytes($traffic['total_up']) }}</span></span>
</div>
<div class="flex items-center gap-1">
@foreach ($windows as $w)