Files
Virtnet.bond/extensions/Servers/Proxmox/views/partials/statistics/lifecycle-inner.blade.php
Joseph.Rawlings 9e899cb947 feat(proxmox): rebuild statistics UI and client runtime
New Blade layout, copy, and visual system for the monitoring tab; RRD hosts use data-statistics-chart hooks; tables use pxStatTableKit and data-statistics-data-row; network and comparison panels use new selectors; CSV helpers renamed (pxExport*); modals and telemetry refreshed.

Made-with: Cursor
2026-03-28 11:38:56 -04:00

428 lines
30 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Recent Lifecycle Events Section -->
@php
$lifecycleEventsExportRows = [];
@endphp
<div class="ring-1 ring-neutral/40 rounded-2xl overflow-hidden bg-background-secondary/80 shadow-sm" wire:poll.30s="refreshTaskData" data-statistics-panel="lifecycle-audit" id="px-stat-lifecycle-card">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between p-5 border-b border-neutral/50 bg-gradient-to-br from-rose-500/10 via-transparent to-transparent">
<div class="flex items-start gap-3 min-w-0 flex-1">
<div class="flex items-center justify-center w-11 h-11 rounded-2xl bg-rose-500/15 shrink-0 ring-1 ring-rose-500/20">
<x-ri-shut-down-line class="size-5 text-rose-600 dark:text-rose-400" />
</div>
<div class="min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h3 class="text-base font-semibold tracking-tight">Guest lifecycle timeline</h3>
<div class="relative" x-data="{ open: false }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-question-line class="size-4" />
</button>
<!-- Modal Backdrop -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
style="display: none;">
</div>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.self="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
style="display: none;">
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
<x-ri-close-line class="size-5" />
</button>
<div class="flex items-center gap-3 mb-4 pr-8">
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-rose-500/15">
<x-ri-shut-down-line class="size-5 text-rose-600 dark:text-rose-400" />
</div>
<h6 class="font-sans text-lg font-semibold leading-tight">
Why this strip exists
</h6>
</div>
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
We distill noisy Proxmox task streams into a concise narrative: power transitions, hard resets, backups, and anything else that changed the guest’s run state.
</p>
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Signals we highlight:</span> Boot, graceful shutdown, reboot, reset, and backup milestones—each tinted for quick scanning.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Expandable receipts:</span> Open a row to see the originating task name, UPID, status, and exact timestamps pulled live from the node.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Outcome chips:</span> Success, running, and failure states mirror the upstream job result so you can spot regressions fast.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">Cadence:</span> Like the job console, this block refreshes every 30 seconds while the tab stays open.
</p>
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
<span class="font-semibold text-base/90">CSV:</span> Download the filtered grid for auditors or change-management tickets.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center gap-2 shrink-0 flex-wrap"
x-data
@lifecycle-export:no-data.window="$wire.showExportInfo($event.detail.message)">
<button type="button"
@click="pxExportLifecycleCsv()"
class="bg-background border border-neutral px-3 py-2 rounded-md text-sm font-semibold hover:bg-background/80 flex items-center gap-2 h-[calc(0.5rem*2+1.25rem)]"
title="Download CSV">
<x-ri-download-line class="size-4" />
CSV
</button>
</div>
</div>
<div x-data="pxStatTableKit('px-stat-lifecycle')">
<div class="p-4 border-b border-neutral bg-background/50">
<div class="flex items-center gap-3 flex-wrap">
<div class="flex-1 min-w-[200px]">
<input type="text"
x-model="search"
@input="filterTable()"
placeholder="Search events..."
class="w-full px-3 py-2 text-sm bg-background border border-neutral rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50">
</div>
<button @click="search = ''; filterTable()"
x-show="search"
class="px-3 py-2 text-sm text-base/70 hover:text-base border border-neutral rounded-md hover:bg-background/80">
Clear
</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full" id="lifecycle-events-table">
<thead>
<tr class="border-b border-neutral bg-background">
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap w-8"></th>
<th @click.stop="sortTable('type')" data-sort="type" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Type
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('status')" data-sort="status" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Status
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('start-time')" data-sort="start-time" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Start Time
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('end-time')" data-sort="end-time" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
End Time
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
<th @click.stop="sortTable('duration')" data-sort="duration" class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase tracking-wider whitespace-nowrap cursor-pointer hover:bg-background-secondary/50 select-none">
<div class="flex items-center gap-2">
Duration
<x-ri-arrow-up-down-line class="size-3" />
</div>
</th>
</tr>
</thead>
@forelse($this->paginatedLifecycleEvents as $index => $event)
@php
$startTime = isset($event['starttime']) ? \Carbon\Carbon::createFromTimestamp($event['starttime']) : null;
$endTime = isset($event['endtime']) && $event['endtime'] > 0 ? \Carbon\Carbon::createFromTimestamp($event['endtime']) : null;
$duration = $event['duration'] ?? null;
$eventUpid = $event['upid'] ?? $event['timestamp'] ?? '';
$eventKey = md5($eventUpid . ($event['starttime'] ?? ''));
$isExpanded = in_array($eventUpid, $this->expandedTaskUpids ?? []);
$eventColorMap = [
'start' => 'text-success',
'shutdown' => 'text-danger',
'reboot' => 'text-info',
'reset' => 'text-warning',
'backup' => 'text-purple-200',
];
$eventColor = $eventColorMap[$event['type']] ?? 'text-base/70';
$lifecycleEventsExportRows[] = [
'type' => $event['label'] ?? 'N/A',
'status' => ucfirst($event['status'] ?? '-'),
'start_time' => $startTime ? $startTime->format('M d, Y H:i:s') : '-',
'end_time' => $endTime ? $endTime->format('M d, Y H:i:s') : '-',
'duration' => $duration !== null ? $this->formatUptime($duration) : '-',
];
@endphp
<tbody
wire:key="lifecycle-{{ $eventKey }}"
x-data="{
expanded: {{ $isExpanded ? 'true' : 'false' }}
}"
x-init="
$watch('expanded', function(value) {
if (value) {
$wire.call('expandTask', '{{ $eventUpid }}');
} else {
$wire.call('collapseTask', '{{ $eventUpid }}');
}
});
"
class="divide-y divide-neutral/40"
>
<tr
data-statistics-data-row
class="hover:bg-background-secondary/40 transition-colors"
data-type="{{ strtolower($event['label'] ?? '') }}"
data-status="{{ strtolower($event['status'] ?? '') }}"
data-start-time="{{ $startTime ? $startTime->format('Y-m-d H:i:s') : '' }}"
data-end-time="{{ $endTime ? $endTime->format('Y-m-d H:i:s') : '' }}"
data-duration="{{ $duration ?? 0 }}"
>
<td class="py-3 px-4 text-sm whitespace-nowrap">
<button
@click="expanded = !expanded"
class="p-1 hover:bg-background rounded transition-colors"
title="Toggle details"
>
<x-ri-arrow-down-s-line
class="size-4 text-base/60 transition-transform duration-200"
x-bind:class="expanded ? 'rotate-180' : ''"
/>
</button>
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
<span class="font-medium {{ $eventColor }}">{{ $event['label'] ?? 'N/A' }}</span>
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@if(isset($event['status']))
@if(strtolower($event['status']) === 'ok')
<span class="inline-flex items-center justify-center px-2 py-1 rounded-md text-xs font-semibold bg-success/20 text-success w-20">
Success
</span>
@elseif(strtolower($event['status']) === 'running')
<span class="inline-flex items-center justify-center px-2 py-1 rounded-md text-xs font-semibold bg-info/20 text-info w-20">
Running
</span>
@else
<span class="inline-flex items-center justify-center px-2 py-1 rounded-md text-xs font-semibold bg-red-500/20 text-red-600 dark:text-red-400 w-20">
Failure
</span>
@endif
@else
<span class="text-base/50">-</span>
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@if($startTime)
<div class="font-medium">{{ $startTime->format('M d, Y') }}</div>
<div class="text-xs text-base/60">{{ $startTime->format('H:i:s') }}</div>
@else
<span class="text-base/50">-</span>
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@if($endTime)
<div class="font-medium">{{ $endTime->format('M d, Y') }}</div>
<div class="text-xs text-base/60">{{ $endTime->format('H:i:s') }}</div>
@else
<span class="text-base/50">-</span>
@endif
</td>
<td class="py-3 px-4 text-sm whitespace-nowrap">
@if($duration !== null)
<span class="font-medium">{{ $this->formatUptime($duration) }}</span>
@else
<span class="text-base/50">-</span>
@endif
</td>
</tr>
<!-- Expanded Details Row -->
<tr
x-show="expanded"
x-cloak
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="bg-background/30"
>
<td colspan="6" class="px-6 py-6">
<div class="space-y-3">
<h4 class="text-sm font-semibold text-base/80 flex items-center gap-2">
<x-ri-information-line class="size-4 text-base/60" />
Event Information
</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex flex-col p-2 bg-background rounded border border-neutral/40">
<span class="text-xs text-base/70 mb-1">Type</span>
<span class="text-sm font-semibold">{{ $event['original_type'] ?? 'N/A' }}</span>
</div>
<div class="flex flex-col p-2 bg-background rounded border border-neutral/40">
<span class="text-xs text-base/70 mb-1">Status</span>
<span class="text-sm font-semibold">{{ ucfirst($event['status'] ?? 'Unknown') }}</span>
</div>
<div class="flex flex-col p-2 bg-background rounded border border-neutral/40">
<span class="text-xs text-base/70 mb-1">Event Type</span>
<span class="text-sm font-semibold {{ $eventColor }}">{{ $event['label'] ?? 'N/A' }}</span>
</div>
</div>
<div class="grid grid-cols-1 gap-4">
@if(isset($event['upid']))
<div class="flex flex-col p-2 bg-background rounded border border-neutral/40">
<span class="text-xs text-base/70 mb-1">Task ID</span>
<span class="text-sm font-semibold">{{ str_replace([':root@pam!paymenter:', ':root@pam:'], '', $event['upid']) }}</span>
</div>
@endif
</div>
</div>
</td>
</tr>
</tbody>
@empty
<tbody>
<tr>
<td colspan="6" class="py-12 text-center">
<div class="flex flex-col items-center gap-3">
<div class="p-3 bg-base/10 rounded-full">
<x-ri-shut-down-line class="size-6 text-base/60" />
</div>
<div>
<p class="text-base font-semibold mb-1">No lifecycle events available</p>
<p class="text-sm text-base/60">Lifecycle events will appear here as operations are performed on the server.</p>
</div>
</div>
</td>
</tr>
</tbody>
@endforelse
</table>
</div>
</div>
<template id="lifecycle-events-export" type="application/json">
@json($lifecycleEventsExportRows)
</template>
<!-- Lifecycle Events Pagination -->
@php
$lifecycleTotalCount = count($this->chartEvents);
@endphp
@if($lifecycleTotalCount > 0)
<div class="p-4 border-t border-neutral flex flex-col sm:flex-row items-center justify-between gap-4">
{{-- Records per page selector --}}
<div class="flex items-center gap-3">
<label class="text-sm text-base/70">Show:</label>
<select wire:model.live="lifecycleEventsPerPage" class="px-3 py-1.5 border border-neutral rounded-lg bg-background text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span class="text-sm text-base/60">per page</span>
</div>
{{-- Page info and navigation --}}
@php
$lifecycleTotalPages = $this->lifecycleEventsTotalPages;
$lifecycleCurrentPage = $lifecycleEventsCurrentPage;
@endphp
<div class="flex items-center gap-4">
<span class="text-sm text-base/60">
Showing {{ (($lifecycleCurrentPage - 1) * $lifecycleEventsPerPage) + 1 }} - {{ min($lifecycleCurrentPage * $lifecycleEventsPerPage, $lifecycleTotalCount) }} of {{ $lifecycleTotalCount }} events
</span>
@if($lifecycleTotalPages > 1)
<div class="flex items-center gap-1">
{{-- First page --}}
<button type="button"
wire:click="goToLifecycleEventsPage(1)"
@if($lifecycleCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="First page">
<x-ri-arrow-left-double-line class="size-4" />
</button>
{{-- Previous page --}}
<button type="button"
wire:click="previousLifecycleEventsPage"
@if($lifecycleCurrentPage === 1) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page">
<x-ri-arrow-left-s-line class="size-4" />
</button>
{{-- Page numbers --}}
@php
$lifecycleStartPage = max(1, $lifecycleCurrentPage - 2);
$lifecycleEndPage = min($lifecycleTotalPages, $lifecycleCurrentPage + 2);
@endphp
@if($lifecycleStartPage > 1)
<button type="button" wire:click="goToLifecycleEventsPage(1)" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">1</button>
@if($lifecycleStartPage > 2)
<span class="px-2 text-base/60">...</span>
@endif
@endif
@for($i = $lifecycleStartPage; $i <= $lifecycleEndPage; $i++)
<button type="button"
wire:click="goToLifecycleEventsPage({{ $i }})"
class="px-3 py-1 border rounded text-sm transition-colors {{ $lifecycleCurrentPage === $i ? 'bg-primary text-white border-primary' : 'border-neutral hover:bg-background-secondary' }}">
{{ $i }}
</button>
@endfor
@if($lifecycleEndPage < $lifecycleTotalPages)
@if($lifecycleEndPage < $lifecycleTotalPages - 1)
<span class="px-2 text-base/60">...</span>
@endif
<button type="button" wire:click="goToLifecycleEventsPage({{ $lifecycleTotalPages }})" class="px-3 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors text-sm">{{ $lifecycleTotalPages }}</button>
@endif
{{-- Next page --}}
<button type="button"
wire:click="nextLifecycleEventsPage"
@if($lifecycleCurrentPage === $lifecycleTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page">
<x-ri-arrow-right-s-line class="size-4" />
</button>
{{-- Last page --}}
<button type="button"
wire:click="goToLifecycleEventsPage({{ $lifecycleTotalPages }})"
@if($lifecycleCurrentPage === $lifecycleTotalPages) disabled @endif
class="px-2 py-1 border border-neutral rounded hover:bg-background-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page">
<x-ri-arrow-right-double-line class="size-4" />
</button>
</div>
@endif
</div>
</div>
@endif
</div>
<script>
window.lifecycleEventsExportData = @json($lifecycleEventsExportRows ?? []);
</script>