From 059e58dc0cfd16a6d25a3dd3c9036653438a0af8 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry Date: Sun, 8 Feb 2026 22:11:23 +0100 Subject: [PATCH] Show installation logs in services --- app/Actions/Server/InstallServer.php | 3 + .../Controllers/API/ServiceController.php | 4 +- app/Http/Controllers/ServiceController.php | 2 +- app/Http/Resources/ServiceResource.php | 1 + app/Jobs/Service/InstallJob.php | 3 + app/Models/Service.php | 26 ++++++++ app/Services/Database/AbstractDatabase.php | 4 +- app/Services/Firewall/Ufw.php | 10 +-- .../Monitoring/VitoAgent/VitoAgent.php | 18 ++--- app/Services/NodeJS/NodeJS.php | 16 +++-- app/Services/PHP/PHP.php | 16 +++-- app/Services/ProcessManager/Supervisor.php | 10 +-- app/Services/Redis/Redis.php | 10 +-- app/Services/Valkey/Valkey.php | 10 +-- app/Services/Webserver/Caddy.php | 10 +-- app/Services/Webserver/Nginx.php | 10 +-- ...01_232849_add_log_id_to_services_table.php | 28 ++++++++ .../js/pages/services/components/columns.tsx | 7 ++ .../services/components/installation-log.tsx | 66 +++++++++++++++++++ resources/js/types/service.d.ts | 3 + tests/Feature/ServicesTest.php | 37 +++++++++++ 21 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 database/migrations/2026_02_01_232849_add_log_id_to_services_table.php create mode 100644 resources/js/pages/services/components/installation-log.tsx diff --git a/app/Actions/Server/InstallServer.php b/app/Actions/Server/InstallServer.php index 58175e619..03ca51392 100644 --- a/app/Actions/Server/InstallServer.php +++ b/app/Actions/Server/InstallServer.php @@ -60,6 +60,9 @@ public function install(): void foreach ($services as $service) { $currentProgress += $progressPerService; $this->progress($currentProgress, 'installing- '.$service->name); + + $service->newLog(); + $service->handler()->install(); $service->update(['status' => ServiceStatus::READY]); if ($service->type == 'php') { diff --git a/app/Http/Controllers/API/ServiceController.php b/app/Http/Controllers/API/ServiceController.php index ad90a7de2..359033f78 100644 --- a/app/Http/Controllers/API/ServiceController.php +++ b/app/Http/Controllers/API/ServiceController.php @@ -27,7 +27,7 @@ public function index(Project $project, Server $server): ResourceCollection $this->validateRoute($project, $server); - return ServiceResource::collection($server->services()->simplePaginate(25)); + return ServiceResource::collection($server->services()->with('log')->simplePaginate(25)); } #[Get('{service}', name: 'api.projects.servers.services.show', middleware: 'ability:read')] @@ -37,6 +37,8 @@ public function show(Project $project, Server $server, Service $service): Servic $this->validateRoute($project, $server, $service); + $service->load('log'); + return new ServiceResource($service); } diff --git a/app/Http/Controllers/ServiceController.php b/app/Http/Controllers/ServiceController.php index ac3c00838..bfc29fac7 100644 --- a/app/Http/Controllers/ServiceController.php +++ b/app/Http/Controllers/ServiceController.php @@ -31,7 +31,7 @@ public function index(Server $server): Response { $this->authorize('viewAny', [Service::class, $server]); - $services = $server->services()->simplePaginate(config('web.pagination_size')); + $services = $server->services()->with('log')->simplePaginate(config('web.pagination_size')); return Inertia::render('services/index', [ 'services' => ServiceResource::collection($services), diff --git a/app/Http/Resources/ServiceResource.php b/app/Http/Resources/ServiceResource.php index 4e78b7877..27a8aa8c1 100644 --- a/app/Http/Resources/ServiceResource.php +++ b/app/Http/Resources/ServiceResource.php @@ -28,6 +28,7 @@ public function toArray(Request $request): array 'status_color' => $this->status->getColor(), 'icon' => config('core.service_icons')[$this->name] ?? '', 'is_default' => $this->is_default, + 'log' => $this->log ? new ServerLogResource($this->log) : null, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; diff --git a/app/Jobs/Service/InstallJob.php b/app/Jobs/Service/InstallJob.php index c38de7192..5355ab7fe 100644 --- a/app/Jobs/Service/InstallJob.php +++ b/app/Jobs/Service/InstallJob.php @@ -22,6 +22,9 @@ public function handle(): void { $this->run("server-{$this->service->server_id}", function () { Log::info("Installing service ID {$this->service->id} on server ID {$this->service->server_id}"); + + $this->service->newLog(); + $this->service->handler()->install(); $this->service->status = ServiceStatus::READY; $this->service->installed_version = $this->service->handler()->version(); diff --git a/app/Models/Service.php b/app/Models/Service.php index 13ae39c79..1fbaac38b 100755 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -18,6 +18,7 @@ /** * @property int $server_id + * @property ?int $log_id * @property string $type * @property array $type_data * @property string $name @@ -28,6 +29,7 @@ * @property ServiceStatus $status * @property bool $is_default * @property Server $server + * @property ?ServerLog $log */ class Service extends AbstractModel { @@ -36,6 +38,7 @@ class Service extends AbstractModel protected $fillable = [ 'server_id', + 'log_id', 'type', 'type_data', 'name', @@ -49,6 +52,7 @@ class Service extends AbstractModel protected $casts = [ 'server_id' => 'integer', + 'log_id' => 'integer', 'type_data' => 'json', 'is_default' => 'boolean', 'status' => ServiceStatus::class, @@ -62,6 +66,14 @@ public function server(): BelongsTo return $this->belongsTo(Server::class); } + /** + * @return BelongsTo + */ + public function log(): BelongsTo + { + return $this->belongsTo(ServerLog::class, 'log_id'); + } + public function handler(): ServiceInterface|Webserver|PHP|Firewall|\App\Services\Database\Database|ProcessManager { $name = $this->name; @@ -116,4 +128,18 @@ public function disable(): void { $this->handler()->unit() && app(Manage::class)->disable($this); } + + public function newLog(): ServerLog + { + + $serverLog = ServerLog::newLog( + $this->server, + 'install-'.$this->name.'-'.$this->version + ); + $serverLog->save(); + $this->log_id = $serverLog->id; + $this->save(); + + return $serverLog; + } } diff --git a/app/Services/Database/AbstractDatabase.php b/app/Services/Database/AbstractDatabase.php index f4a9cecb7..ba12ab67b 100755 --- a/app/Services/Database/AbstractDatabase.php +++ b/app/Services/Database/AbstractDatabase.php @@ -57,7 +57,9 @@ public function install(): void { $version = str_replace('.', '', $this->service->version); $command = view($this->getScriptView('install-'.$version)); - $this->service->server->ssh()->exec($command, 'install-'.$this->service->name.'-'.$version); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec($command, 'install-'.$this->service->name.'-'.$version); $status = $this->service->server->systemd()->status($this->unit()); $this->service->validateInstall($status); $this->service->server->os()->cleanup(); diff --git a/app/Services/Firewall/Ufw.php b/app/Services/Firewall/Ufw.php index 4f371a5b2..8892e5c60 100755 --- a/app/Services/Firewall/Ufw.php +++ b/app/Services/Firewall/Ufw.php @@ -29,10 +29,12 @@ public function install(): void { $this->createBasicFirewallRules(); - $this->service->server->ssh()->exec( - view('ssh.services.firewall.ufw.install-ufw'), - 'install-ufw' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.firewall.ufw.install-ufw'), + 'install-ufw' + ); event('service.installed', $this->service); $this->service->server->os()->cleanup(); } diff --git a/app/Services/Monitoring/VitoAgent/VitoAgent.php b/app/Services/Monitoring/VitoAgent/VitoAgent.php index 65cae386e..d3cbf4be5 100644 --- a/app/Services/Monitoring/VitoAgent/VitoAgent.php +++ b/app/Services/Monitoring/VitoAgent/VitoAgent.php @@ -90,14 +90,16 @@ public function install(): void $this->service->save(); $this->service->refresh(); - $this->service->server->ssh()->exec( - view('ssh.services.monitoring.vito-agent.install', [ - 'downloadUrl' => $downloadUrl, - 'configUrl' => $this->data()['url'], - 'configSecret' => $this->data()['secret'], - ]), - 'install-vito-agent' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.monitoring.vito-agent.install', [ + 'downloadUrl' => $downloadUrl, + 'configUrl' => $this->data()['url'], + 'configSecret' => $this->data()['secret'], + ]), + 'install-vito-agent' + ); $status = $this->service->server->systemd()->status($this->unit()); event('service.installed', $this->service); $this->service->validateInstall($status); diff --git a/app/Services/NodeJS/NodeJS.php b/app/Services/NodeJS/NodeJS.php index f5c9185d8..58cfb9f1e 100644 --- a/app/Services/NodeJS/NodeJS.php +++ b/app/Services/NodeJS/NodeJS.php @@ -87,13 +87,15 @@ public function uninstall(): void if ($this->service->server->services()->where('type', 'nodejs')->count() > 1) { return; } - $this->service->server->ssh()->exec( - view('ssh.services.nodejs.uninstall-nodejs', [ - 'version' => $this->service->version, - 'default' => $this->service->is_default, - ]), - 'uninstall-nodejs-'.$this->service->version - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.nodejs.uninstall-nodejs', [ + 'version' => $this->service->version, + 'default' => $this->service->is_default, + ]), + 'uninstall-nodejs-'.$this->service->version + ); event('service.uninstalled', $this->service); $this->service->server->os()->cleanup(); } diff --git a/app/Services/PHP/PHP.php b/app/Services/PHP/PHP.php index 6ce9a5a00..04a84004d 100644 --- a/app/Services/PHP/PHP.php +++ b/app/Services/PHP/PHP.php @@ -61,13 +61,15 @@ function (string $attribute, mixed $value, Closure $fail): void { public function install(): void { $server = $this->service->server; - $server->ssh()->exec( - view('ssh.services.php.install-php', [ - 'version' => $this->service->version, - 'user' => $server->getSshUser(), - ]), - 'install-php-'.$this->service->version - ); + $server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.php.install-php', [ + 'version' => $this->service->version, + 'user' => $server->getSshUser(), + ]), + 'install-php-'.$this->service->version + ); $this->installComposer(); event('service.installed', $this->service); $this->service->server->os()->cleanup(); diff --git a/app/Services/ProcessManager/Supervisor.php b/app/Services/ProcessManager/Supervisor.php index d94a18a07..d913150e8 100644 --- a/app/Services/ProcessManager/Supervisor.php +++ b/app/Services/ProcessManager/Supervisor.php @@ -27,10 +27,12 @@ public function unit(): string */ public function install(): void { - $this->service->server->ssh()->exec( - view('ssh.services.process-manager.supervisor.install-supervisor'), - 'install-supervisor' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.process-manager.supervisor.install-supervisor'), + 'install-supervisor' + ); event('service.installed', $this->service); $this->service->server->os()->cleanup(); } diff --git a/app/Services/Redis/Redis.php b/app/Services/Redis/Redis.php index 7c1104d6d..4a440a241 100644 --- a/app/Services/Redis/Redis.php +++ b/app/Services/Redis/Redis.php @@ -45,10 +45,12 @@ function (string $attribute, mixed $value, Closure $fail): void { */ public function install(): void { - $this->service->server->ssh()->exec( - view('ssh.services.redis.install'), - 'install-redis' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.redis.install'), + 'install-redis' + ); $status = $this->service->server->systemd()->status($this->unit()); $this->service->validateInstall($status); event('service.installed', $this->service); diff --git a/app/Services/Valkey/Valkey.php b/app/Services/Valkey/Valkey.php index 2e33a7a84..f799f6c65 100644 --- a/app/Services/Valkey/Valkey.php +++ b/app/Services/Valkey/Valkey.php @@ -45,10 +45,12 @@ function (string $attribute, mixed $value, Closure $fail): void { */ public function install(): void { - $this->service->server->ssh()->exec( - view('ssh.services.valkey.install'), - 'install-valkey' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.valkey.install'), + 'install-valkey' + ); $status = $this->service->server->systemd()->status($this->unit()); $this->service->validateInstall($status); event('service.installed', $this->service); diff --git a/app/Services/Webserver/Caddy.php b/app/Services/Webserver/Caddy.php index a73d401c5..5b49b3068 100755 --- a/app/Services/Webserver/Caddy.php +++ b/app/Services/Webserver/Caddy.php @@ -30,10 +30,12 @@ public function unit(): string */ public function install(): void { - $this->service->server->ssh()->exec( - view('ssh.services.webserver.caddy.install-caddy'), - 'install-caddy' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.webserver.caddy.install-caddy'), + 'install-caddy' + ); $this->service->server->ssh()->write( '/etc/caddy/Caddyfile', diff --git a/app/Services/Webserver/Nginx.php b/app/Services/Webserver/Nginx.php index b5ad51d59..89bc9eb0d 100755 --- a/app/Services/Webserver/Nginx.php +++ b/app/Services/Webserver/Nginx.php @@ -30,10 +30,12 @@ public function unit(): string */ public function install(): void { - $this->service->server->ssh()->exec( - view('ssh.services.webserver.nginx.install-nginx'), - 'install-nginx' - ); + $this->service->server->ssh() + ->setLog($this->service->log) + ->exec( + view('ssh.services.webserver.nginx.install-nginx'), + 'install-nginx' + ); $this->service->server->ssh()->write( '/etc/nginx/nginx.conf', diff --git a/database/migrations/2026_02_01_232849_add_log_id_to_services_table.php b/database/migrations/2026_02_01_232849_add_log_id_to_services_table.php new file mode 100644 index 000000000..9f324e90c --- /dev/null +++ b/database/migrations/2026_02_01_232849_add_log_id_to_services_table.php @@ -0,0 +1,28 @@ +unsignedBigInteger('log_id')->nullable()->after('server_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropColumn('log_id'); + }); + } +}; diff --git a/resources/js/pages/services/components/columns.tsx b/resources/js/pages/services/components/columns.tsx index 8e995b56c..8b76898e3 100644 --- a/resources/js/pages/services/components/columns.tsx +++ b/resources/js/pages/services/components/columns.tsx @@ -9,6 +9,7 @@ import Uninstall from '@/pages/services/components/uninstall'; import { Action } from '@/pages/services/components/action'; import Version from './version'; import ConfigFile from './config-file'; +import InstallationLog from './installation-log'; export const columns: ColumnDef[] = [ { @@ -73,6 +74,12 @@ export const columns: ColumnDef[] = [ ))} )} + {row.original.log && ( + <> + + + + )} diff --git a/resources/js/pages/services/components/installation-log.tsx b/resources/js/pages/services/components/installation-log.tsx new file mode 100644 index 000000000..1dce8d76e --- /dev/null +++ b/resources/js/pages/services/components/installation-log.tsx @@ -0,0 +1,66 @@ +import { Button } from '@/components/ui/button'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { DropdownMenuItem } from '@/components/ui/dropdown-menu'; +import LogOutput from '@/components/log-output'; +import { Service } from '@/types/service'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { ReactNode, useState } from 'react'; + +export default function InstallationLog({ service, children }: { service: Service; children?: ReactNode }) { + const [open, setOpen] = useState(false); + + const query = useQuery({ + queryKey: ['service-installation-log', service.id], + queryFn: async () => { + if (!service.log) { + throw new Error('No installation log available'); + } + try { + const response = await axios.get(route('logs.show', { server: service.server_id, log: service.log.id })); + return typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2); + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + throw new Error(error.response?.data?.error || 'An error occurred while fetching the log'); + } + throw new Error('Unknown error occurred'); + } + }, + enabled: open && !!service.log, + retry: false, + refetchInterval: (query) => { + if (query.state.status === 'error') return false; + return 2500; + }, + }); + + if (!service.log) { + return null; + } + + return ( + + + {children ? children : e.preventDefault()}>Installation Log} + + + + Installation Log + Installation log for {service.name} + + + <> + {query.isLoading && 'Loading...'} + {query.isError &&
Error: {query.error.message}
} + {query.data && !query.isError && query.data} + +
+ + + + + +
+
+ ); +} diff --git a/resources/js/types/service.d.ts b/resources/js/types/service.d.ts index 30ad5aa9a..837d42743 100644 --- a/resources/js/types/service.d.ts +++ b/resources/js/types/service.d.ts @@ -1,3 +1,5 @@ +import { ServerLog } from './server-log'; + export interface ConfigPath { name: string; path: string; @@ -21,6 +23,7 @@ export interface Service { status: string; status_color: 'gray' | 'success' | 'info' | 'warning' | 'danger'; icon: string; + log?: ServerLog | null; created_at: string; updated_at: string; [key: string]: unknown; diff --git a/tests/Feature/ServicesTest.php b/tests/Feature/ServicesTest.php index 807b418bf..1f283a2e7 100644 --- a/tests/Feature/ServicesTest.php +++ b/tests/Feature/ServicesTest.php @@ -313,6 +313,43 @@ public function test_install_service(string $name, string $type, string $version ]); } + public function test_install_service_creates_installation_log(): void + { + Http::fake([ + 'https://api.github.com/repos/vito/vito-agent/releases/latest' => Http::response([ + 'tag_name' => '0.1.0', + ]), + ]); + SSH::fake('Active: active'); + + $this->actingAs($this->user); + + $server = Server::factory()->create([ + 'user_id' => $this->user->id, + 'project_id' => $this->user->current_project_id, + ]); + + $keys = $server->sshKey(); + if (! File::exists($keys['public_key_path']) || ! File::exists($keys['private_key_path'])) { + $server->provider()->generateKeyPair(); + } + + $this->post(route('services.store', [ + 'server' => $server, + ]), [ + 'name' => 'redis', + 'version' => 'latest', + ]) + ->assertSessionDoesntHaveErrors(); + + $service = $server->services()->where('name', 'redis')->firstOrFail(); + + // Verify that the installation log is linked to the service + $this->assertNotNull($service->log_id); + $this->assertNotNull($service->log); + $this->assertStringStartsWith('install-redis', $service->log->type); + } + public function test_fetch_php_installed_version(): void { SSH::fake('8.4.10');