Commit f0bc5db1 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add live container state and logs viewer to instance detail page

Load State button fetches replicas, build status, SSL, env var count,
volumes, and recent deploy versions from CapRover API. Fetch Logs shows
the container's stdout/stderr in a dark terminal-style panel with refresh.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c491f9b1
...@@ -3,28 +3,85 @@ ...@@ -3,28 +3,85 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\Instance; use App\Models\Instance;
use App\Services\CapRoverService;
use App\Services\InstanceProvisionerService; use App\Services\InstanceProvisionerService;
use Livewire\Component; use Livewire\Component;
class InstanceShow extends Component class InstanceShow extends Component
{ {
public Instance $instance; public Instance $instance;
public string $logs = '';
public bool $showLogs = false;
public array $appState = [];
public bool $stateLoaded = false;
public ?string $stateError = null;
public ?string $logsError = null;
public function mount(Instance $instance): void public function mount(Instance $instance): void
{ {
$this->instance = $instance; $this->instance = $instance;
} }
public function loadState(): void
{
try {
$caprover = app(CapRoverService::class);
$info = $caprover->getAppInfo($this->instance->app_name);
$this->appState = [
'instanceCount' => $info['instanceCount'] ?? 0,
'isAppBuilding' => $info['isAppBuilding'] ?? false,
'hasPersistentData' => $info['hasPersistentData'] ?? false,
'containerHttpPort' => $info['containerHttpPort'] ?? null,
'forceSsl' => $info['forceSsl'] ?? false,
'hasDefaultSubDomainSsl' => $info['hasDefaultSubDomainSsl'] ?? false,
'versions' => array_slice($info['versions'] ?? [], -5),
'envVarCount' => count($info['envVars'] ?? []),
'volumeCount' => count($info['volumes'] ?? []),
];
$this->stateLoaded = true;
$this->stateError = null;
} catch (\Throwable $e) {
$this->stateError = $e->getMessage();
$this->stateLoaded = false;
}
}
public function fetchLogs(): void
{
try {
$caprover = app(CapRoverService::class);
$this->logs = $caprover->getAppLogs($this->instance->app_name);
$this->showLogs = true;
$this->logsError = null;
} catch (\Throwable $e) {
$this->logsError = $e->getMessage();
$this->showLogs = true;
}
}
public function refreshLogs(): void
{
$this->fetchLogs();
}
public function hideLogs(): void
{
$this->showLogs = false;
$this->logs = '';
}
public function suspend(): void public function suspend(): void
{ {
app(InstanceProvisionerService::class)->suspend($this->instance, 'Manual suspension from dashboard'); app(InstanceProvisionerService::class)->suspend($this->instance, 'Manual suspension from dashboard');
$this->instance->refresh(); $this->instance->refresh();
$this->stateLoaded = false;
} }
public function resume(): void public function resume(): void
{ {
app(InstanceProvisionerService::class)->resume($this->instance); app(InstanceProvisionerService::class)->resume($this->instance);
$this->instance->refresh(); $this->instance->refresh();
$this->stateLoaded = false;
} }
public function delete(): void public function delete(): void
......
...@@ -105,6 +105,144 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-50 ...@@ -105,6 +105,144 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-50
</div> </div>
</div> </div>
{{-- Container State --}}
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6">
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900">Container State</h3>
<button wire:click="loadState" wire:loading.attr="disabled" wire:target="loadState"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition">
<svg wire:loading.remove wire:target="loadState" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
<svg wire:loading wire:target="loadState" class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
<span wire:loading.remove wire:target="loadState">{{ $stateLoaded ? 'Refresh' : 'Load State' }}</span>
<span wire:loading wire:target="loadState">Loading...</span>
</button>
</div>
@if($stateError)
<div class="px-5 py-4">
<div class="p-3 bg-red-50 border border-red-200 rounded-lg">
<p class="text-sm text-red-700">Failed to fetch state: {{ $stateError }}</p>
</div>
</div>
@elseif($stateLoaded)
<div class="px-5 py-4">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-4">
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold {{ ($appState['instanceCount'] ?? 0) > 0 ? 'text-green-600' : 'text-red-600' }}">
{{ $appState['instanceCount'] ?? 0 }}
</div>
<div class="text-xs text-gray-500 mt-1">Replicas</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold {{ ($appState['isAppBuilding'] ?? false) ? 'text-yellow-600' : 'text-gray-400' }}">
{{ ($appState['isAppBuilding'] ?? false) ? 'Yes' : 'No' }}
</div>
<div class="text-xs text-gray-500 mt-1">Building</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-gray-700">{{ $appState['envVarCount'] ?? 0 }}</div>
<div class="text-xs text-gray-500 mt-1">Env Vars</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-2xl font-bold text-gray-700">{{ $appState['volumeCount'] ?? 0 }}</div>
<div class="text-xs text-gray-500 mt-1">Volumes</div>
</div>
</div>
<dl class="grid grid-cols-2 gap-2 text-xs">
<div class="flex justify-between p-2 bg-gray-50 rounded">
<dt class="text-gray-500">HTTP Port</dt>
<dd class="font-mono font-medium text-gray-700">{{ $appState['containerHttpPort'] ?? 'N/A' }}</dd>
</div>
<div class="flex justify-between p-2 bg-gray-50 rounded">
<dt class="text-gray-500">Force SSL</dt>
<dd class="font-medium {{ ($appState['forceSsl'] ?? false) ? 'text-green-600' : 'text-gray-500' }}">
{{ ($appState['forceSsl'] ?? false) ? 'Yes' : 'No' }}
</dd>
</div>
<div class="flex justify-between p-2 bg-gray-50 rounded">
<dt class="text-gray-500">SSL Enabled</dt>
<dd class="font-medium {{ ($appState['hasDefaultSubDomainSsl'] ?? false) ? 'text-green-600' : 'text-gray-500' }}">
{{ ($appState['hasDefaultSubDomainSsl'] ?? false) ? 'Yes' : 'No' }}
</dd>
</div>
<div class="flex justify-between p-2 bg-gray-50 rounded">
<dt class="text-gray-500">Persistent Data</dt>
<dd class="font-medium text-gray-700">{{ ($appState['hasPersistentData'] ?? false) ? 'Yes' : 'No' }}</dd>
</div>
</dl>
@if(!empty($appState['versions']))
<div class="mt-4">
<h4 class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-2">Recent Deploys</h4>
<div class="space-y-1">
@foreach(array_reverse($appState['versions']) as $version)
<div class="flex items-center justify-between p-2 bg-gray-50 rounded text-xs">
<span class="font-mono text-gray-600">v{{ $version['version'] ?? '?' }}</span>
<span class="text-gray-500">{{ isset($version['timeStamp']) ? \Carbon\Carbon::parse($version['timeStamp'])->diffForHumans() : '' }}</span>
<span class="inline-block px-1.5 py-0.5 rounded font-medium {{ ($version['deployedImageName'] ?? '') ? 'bg-blue-50 text-blue-600' : 'bg-gray-100 text-gray-500' }}">
{{ ($version['gitHash'] ?? '') ? substr($version['gitHash'], 0, 7) : 'image' }}
</span>
</div>
@endforeach
</div>
</div>
@endif
</div>
@else
<div class="px-5 py-8 text-center text-gray-400 text-sm">
Click "Load State" to fetch live container info from CapRover.
</div>
@endif
</div>
{{-- Logs --}}
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6">
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900">Container Logs</h3>
<div class="flex items-center gap-2">
@if($showLogs)
<button wire:click="refreshLogs" wire:loading.attr="disabled" wire:target="refreshLogs"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 rounded-lg hover:bg-blue-100 transition">
<svg wire:loading.remove wire:target="refreshLogs" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
<svg wire:loading wire:target="refreshLogs" class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
Refresh
</button>
<button wire:click="hideLogs" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition">
Hide
</button>
@else
<button wire:click="fetchLogs" wire:loading.attr="disabled" wire:target="fetchLogs"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 transition">
<svg wire:loading.remove wire:target="fetchLogs" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
<svg wire:loading wire:target="fetchLogs" class="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
<span wire:loading.remove wire:target="fetchLogs">Fetch Logs</span>
<span wire:loading wire:target="fetchLogs">Fetching...</span>
</button>
@endif
</div>
</div>
@if($showLogs)
@if($logsError)
<div class="px-5 py-4">
<div class="p-3 bg-red-50 border border-red-200 rounded-lg">
<p class="text-sm text-red-700">Failed to fetch logs: {{ $logsError }}</p>
</div>
</div>
@elseif($logs)
<div class="relative">
<pre class="px-5 py-4 text-xs font-mono text-gray-300 bg-gray-900 overflow-x-auto max-h-[500px] overflow-y-auto whitespace-pre-wrap break-all leading-relaxed">{{ $logs }}</pre>
</div>
@else
<div class="px-5 py-6 text-center text-gray-400 text-sm">No logs available.</div>
@endif
@else
<div class="px-5 py-8 text-center text-gray-400 text-sm">
Click "Fetch Logs" to view the container's recent output.
</div>
@endif
</div>
{{-- Billing Periods --}} {{-- Billing Periods --}}
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6"> <div class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6">
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between"> <div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment