From e1abef4194610c834bb8d6a430704715c1eb3f27 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Mon, 27 Apr 2026 13:31:25 +0200 Subject: [PATCH] =?UTF-8?q?F=20websockets:watch=20=E2=80=94=20live-updatin?= =?UTF-8?q?g=20stats=20display,=20refreshes=20every=201s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders the same Live Stats / channels table that `websockets:info` shows at the bottom, but loops indefinitely so it can be left open as a quick status pane. 1-second poll against the existing WebsocketService cache reads — no pub/sub plumbing because there is no "stats changed" event emitted today and a 1s tick is fast enough for the granularity humans care about. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Console/Commands/WatchStats.php | 105 ++++++++++++++++++++++++++++ src/WebSocketsServiceProvider.php | 1 + 2 files changed, 106 insertions(+) create mode 100644 src/Console/Commands/WatchStats.php diff --git a/src/Console/Commands/WatchStats.php b/src/Console/Commands/WatchStats.php new file mode 100644 index 0000000..3e850b5 --- /dev/null +++ b/src/Console/Commands/WatchStats.php @@ -0,0 +1,105 @@ +option('interval')); + + // Restore cursor visibility on Ctrl+C / kill so the terminal isn't + // left in a broken state if the user interrupts mid-render. + if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { + pcntl_async_signals(true); + $restore = function () { + $this->output->write("\033[?25h"); + $this->newLine(); + exit(0); + }; + pcntl_signal(SIGINT, $restore); + pcntl_signal(SIGTERM, $restore); + } + + $this->output->write("\033[?25l"); // hide cursor for cleaner refresh + + try { + while (true) { + $this->output->write("\033[2J\033[H"); // clear screen + home cursor + + $this->line('WebSocket Server — Live Stats'); + $this->line('' . now()->format('Y-m-d H:i:s') . ' — refreshing every ' . $interval . 's (Ctrl+C to exit)'); + $this->newLine(); + + $this->renderStats(); + + sleep($interval); + } + } finally { + $this->output->write("\033[?25h"); // always restore cursor + } + + return 0; // @phpstan-ignore-line + } + + /** + * Identical to ServerInfo::renderStats — duplicated rather than shared via + * trait so that the two commands can evolve independently if needed (e.g. + * the live view may eventually want sparkline-style deltas that the + * one-shot view doesn't). + */ + protected function renderStats(): void + { + $channels = WebsocketService::getActiveChannels(); + $authedUsers = WebsocketService::getAuthedUsers(); + + $totalConnections = 0; + $channelData = []; + + foreach ($channels as $channel) { + $connections = WebsocketService::getChannelConnections($channel); + $count = count($connections); + $totalConnections += $count; + $channelData[] = [$channel, $count]; + } + + $uniqueUsers = count(array_unique(array_values($authedUsers))); + + $this->components->twoColumnDetail('Live Stats'); + $this->components->twoColumnDetail('Total connections', "{$totalConnections}"); + $this->components->twoColumnDetail('Authenticated users', "{$uniqueUsers}"); + $this->components->twoColumnDetail('Active channels', '' . count($channels) . ''); + + if (count($channelData) > 0) { + $this->newLine(); + $this->table(['Channel', 'Connections'], $channelData); + } else { + $this->newLine(); + $this->line(' No active channels.'); + } + } +} diff --git a/src/WebSocketsServiceProvider.php b/src/WebSocketsServiceProvider.php index f771d6a..1600de6 100644 --- a/src/WebSocketsServiceProvider.php +++ b/src/WebSocketsServiceProvider.php @@ -61,6 +61,7 @@ class WebSocketsServiceProvider extends ServiceProvider Console\Commands\RestartHard::class, Console\Commands\SteerServer::class, Console\Commands\ServerInfo::class, + Console\Commands\WatchStats::class, ]); }