diff --git a/app/Livewire/Wireguard/Index.php b/app/Livewire/Wireguard/Index.php index 40315aa..d01f417 100644 --- a/app/Livewire/Wireguard/Index.php +++ b/app/Livewire/Wireguard/Index.php @@ -3,20 +3,36 @@ namespace App\Livewire\Wireguard; use App\Services\WgStatus; +use App\Services\WgTraffic; use Livewire\Attributes\Layout; use Livewire\Component; /** - * WireGuard dashboard — live status (read-only, Phase 1). Renders the host-collected - * run/wg-status.json (via WgStatus) and wire:polls every 5s. No writes here (P3/P4). + * WireGuard dashboard — live status (P1) + traffic history (P2). Renders the host-collected + * status (WgStatus) and a bucketed throughput series (WgTraffic), wire:polls every 5s. Read-only. */ #[Layout('layouts.app')] class Index extends Component { - public function render(WgStatus $wg) + /** Selected traffic window in seconds. One of WINDOWS. */ + public int $window = 3600; + + /** Allowed windows (seconds): 1h / 24h / 7d. */ + public const WINDOWS = [3600, 86400, 604800]; + + public function setWindow(int $seconds): void { + $this->window = in_array($seconds, self::WINDOWS, true) ? $seconds : 3600; + } + + public function render(WgStatus $wg, WgTraffic $traffic) + { + $window = in_array($this->window, self::WINDOWS, true) ? $this->window : 3600; + return view('livewire.wireguard.index', [ 'status' => $wg->read(), + 'traffic' => $traffic->series($window), + 'windows' => self::WINDOWS, ])->title(__('wireguard.title')); } } diff --git a/lang/de/wireguard.php b/lang/de/wireguard.php index 47e759c..1fdc86c 100644 --- a/lang/de/wireguard.php +++ b/lang/de/wireguard.php @@ -26,4 +26,11 @@ return [ 'last_handshake' => 'letzter Handshake', 'never' => 'nie', 'down_up' => '↓ :rx · ↑ :tx', + 'traffic_title' => 'Traffic', + 'window_1h' => '1 Std', + 'window_24h' => '24 Std', + 'window_7d' => '7 Tage', + 'total_down' => 'Empfangen', + 'total_up' => 'Gesendet', + 'no_traffic' => 'Noch keine Verlaufsdaten — sie sammeln sich, sobald Peers verbunden sind.', ]; diff --git a/lang/en/wireguard.php b/lang/en/wireguard.php index ed2a3a4..2ab4467 100644 --- a/lang/en/wireguard.php +++ b/lang/en/wireguard.php @@ -26,4 +26,11 @@ return [ 'last_handshake' => 'last handshake', 'never' => 'never', 'down_up' => '↓ :rx · ↑ :tx', + 'traffic_title' => 'Traffic', + 'window_1h' => '1h', + 'window_24h' => '24h', + 'window_7d' => '7d', + 'total_down' => 'Received', + 'total_up' => 'Sent', + 'no_traffic' => 'No history yet — it accrues once peers are connected.', ]; diff --git a/resources/views/livewire/wireguard/index.blade.php b/resources/views/livewire/wireguard/index.blade.php index e032627..19f818c 100644 --- a/resources/views/livewire/wireguard/index.blade.php +++ b/resources/views/livewire/wireguard/index.blade.php @@ -36,6 +36,59 @@ @endif + @php + $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']); + $line = function (string $key) use ($pts, $n, $peak) { + $out = []; + foreach ($pts as $i => $p) { + $x = round($i / $n * 600, 1); + $y = round(140 - ($p[$key] / $peak) * 132, 1); + $out[] = $x.','.$y; + } + return implode(' ', $out); + }; + $hasTraffic = $traffic['total_down'] > 0 || $traffic['total_up'] > 0; + @endphp + + +
+
+ {{ __('wireguard.total_down') }}: {{ $fmtB($traffic['total_down']) }} + {{ __('wireguard.total_up') }}: {{ $fmtB($traffic['total_up']) }} +
+
+ @foreach ($windows as $w) + + @endforeach +
+
+
+ @if ($hasTraffic) + + + + + @else +

{{ __('wireguard.no_traffic') }}

+ @endif +
+
+
diff --git a/tests/Feature/WireguardPageTest.php b/tests/Feature/WireguardPageTest.php index 7aa7b5a..11b5299 100644 --- a/tests/Feature/WireguardPageTest.php +++ b/tests/Feature/WireguardPageTest.php @@ -53,4 +53,23 @@ class WireguardPageTest extends TestCase ->assertSee('laptop') ->assertSee('10.99.0.0/24'); } + + public function test_window_selector_switches_and_passes_traffic(): void + { + \App\Models\WgTrafficSample::create(['peer_pubkey' => 'p1', 'peer_name' => 'a', 'rx' => 0, 'tx' => 0, 'sampled_at' => now()->subMinutes(10)]); + \App\Models\WgTrafficSample::create(['peer_pubkey' => 'p1', 'peer_name' => 'a', 'rx' => 500, 'tx' => 100, 'sampled_at' => now()->subMinutes(1)]); + + \Livewire\Livewire::test(\App\Livewire\Wireguard\Index::class) + ->assertViewHas('traffic', fn ($t) => $t['total_down'] === 500) + ->call('setWindow', 86400) + ->assertSet('window', 86400) + ->assertViewHas('traffic', fn ($t) => $t['window'] === 86400); + } + + public function test_window_clamps_to_allowed_values(): void + { + \Livewire\Livewire::test(\App\Livewire\Wireguard\Index::class) + ->call('setWindow', 999) + ->assertSet('window', 3600); + } }