From e52a49b5e9d70ade80b0f98529bf47b3793197de Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:21:05 +0100 Subject: [PATCH] feat(server): add server metadata collection and display Add ability to gather and display server system information including OS, architecture, kernel version, CPU count, memory, and uptime. Includes: - New gatherServerMetadata() method to collect system details via remote commands - New refreshServerMetadata() Livewire action with authorization and error handling - Server Details UI section showing collected metadata with refresh capability - Database migration to add server_metadata JSON column - Comprehensive test suite for metadata collection and persistence --- app/Livewire/Server/Show.php | 16 ++++ app/Models/Server.php | 52 ++++++++++ ...0_add_server_metadata_to_servers_table.php | 32 +++++++ .../views/livewire/server/show.blade.php | 52 ++++++++++ tests/Feature/ServerMetadataTest.php | 96 +++++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 database/migrations/2026_03_11_000000_add_server_metadata_to_servers_table.php create mode 100644 tests/Feature/ServerMetadataTest.php diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index edc17004c..84cb65ee6 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -483,6 +483,22 @@ public function startHetznerServer() } } + public function refreshServerMetadata(): void + { + try { + $this->authorize('update', $this->server); + $result = $this->server->gatherServerMetadata(); + if ($result) { + $this->server->refresh(); + $this->dispatch('success', 'Server details refreshed.'); + } else { + $this->dispatch('error', 'Could not fetch server details. Is the server reachable?'); + } + } catch (\Throwable $e) { + handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Models/Server.php b/app/Models/Server.php index 5099a9fec..508b9833b 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -25,6 +25,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Stringable; use OpenApi\Attributes as OA; @@ -231,6 +232,7 @@ public static function flushIdentityMap(): void protected $casts = [ 'proxy' => SchemalessAttributes::class, 'traefik_outdated_info' => 'array', + 'server_metadata' => 'array', 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', 'delete_unused_volumes' => 'boolean', @@ -258,6 +260,7 @@ public static function flushIdentityMap(): void 'is_validating', 'detected_traefik_version', 'traefik_outdated_info', + 'server_metadata', ]; protected $guarded = []; @@ -1074,6 +1077,55 @@ public function validateOS(): bool|Stringable } } + public function gatherServerMetadata(): ?array + { + if (! $this->isFunctional()) { + return null; + } + + try { + $output = instant_remote_process([ + 'echo "---PRETTY_NAME---" && grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d \'"\' && echo "---ARCH---" && uname -m && echo "---KERNEL---" && uname -r && echo "---CPUS---" && nproc && echo "---MEMORY---" && free -b | awk \'/Mem:/{print $2}\' && echo "---UPTIME_SINCE---" && uptime -s', + ], $this, false); + + if (! $output) { + return null; + } + + $sections = []; + $currentKey = null; + foreach (explode("\n", trim($output)) as $line) { + $line = trim($line); + if (preg_match('/^---(\w+)---$/', $line, $m)) { + $currentKey = $m[1]; + } elseif ($currentKey) { + $sections[$currentKey] = $line; + } + } + + $metadata = [ + 'os' => $sections['PRETTY_NAME'] ?? 'Unknown', + 'arch' => $sections['ARCH'] ?? 'Unknown', + 'kernel' => $sections['KERNEL'] ?? 'Unknown', + 'cpus' => (int) ($sections['CPUS'] ?? 0), + 'memory_bytes' => (int) ($sections['MEMORY'] ?? 0), + 'uptime_since' => $sections['UPTIME_SINCE'] ?? null, + 'collected_at' => now()->toIso8601String(), + ]; + + $this->update(['server_metadata' => $metadata]); + + return $metadata; + } catch (\Throwable $e) { + Log::debug('Failed to gather server metadata', [ + 'server_id' => $this->id, + 'error' => $e->getMessage(), + ]); + + return null; + } + } + public function isTerminalEnabled() { return $this->settings->is_terminal_enabled ?? false; diff --git a/database/migrations/2026_03_11_000000_add_server_metadata_to_servers_table.php b/database/migrations/2026_03_11_000000_add_server_metadata_to_servers_table.php new file mode 100644 index 000000000..cea25c3ba --- /dev/null +++ b/database/migrations/2026_03_11_000000_add_server_metadata_to_servers_table.php @@ -0,0 +1,32 @@ +json('server_metadata')->nullable(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasColumn('servers', 'server_metadata')) { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('server_metadata'); + }); + } + } +}; diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index f58dc058b..7017d7104 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -289,6 +289,58 @@ class="w-full input opacity-50 cursor-not-allowed" + @if ($server->isFunctional()) +
Last updated: + {{ \Carbon\Carbon::parse($meta['collected_at'])->diffForHumans() }}
+ @endif + @else +