1739 lines
126 KiB
PHP
1739 lines
126 KiB
PHP
<x-proxmox::layout
|
|
:server="$server"
|
|
view="overview"
|
|
:status="$status"
|
|
:status-polling="true"
|
|
:status-polling-interval="'2s'"
|
|
>
|
|
@if(!$status)
|
|
{{-- Error state when status cannot be loaded --}}
|
|
<x-proxmox::ui.alert variant="danger" icon="ri-error-warning-fill" class="mt-8">
|
|
<x-slot name="title">Unable to Load Server Status</x-slot>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-3">Could not retrieve status from hypervisor:</p>
|
|
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 mb-3 space-y-1 text-sm">
|
|
<li>Temporary connectivity issues with the Proxmox cluster</li>
|
|
<li>The VM may be in an intermediate state</li>
|
|
<li>Network timeout while fetching data</li>
|
|
</ul>
|
|
<x-proxmox::ui.button wire:click="loadVmStatus" icon="ri-refresh-line">
|
|
Retry
|
|
</x-proxmox::ui.button>
|
|
</x-proxmox::ui.alert>
|
|
@else
|
|
<<<<<<< HEAD
|
|
=======
|
|
<script>
|
|
function copyToClipboard(text, buttonEl) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
// Change button style & text
|
|
const originalText = buttonEl.innerText;
|
|
buttonEl.innerText = "Copied!";
|
|
buttonEl.classList.remove("bg-blue-600", "hover:bg-blue-700");
|
|
buttonEl.classList.add("bg-green-600", "hover:bg-green-700");
|
|
|
|
// Revert after 2s
|
|
setTimeout(() => {
|
|
buttonEl.innerText = originalText;
|
|
buttonEl.classList.remove("bg-green-600", "hover:bg-green-700");
|
|
buttonEl.classList.add("bg-blue-600", "hover:bg-blue-700");
|
|
}, 2000);
|
|
}).catch(err => {
|
|
console.error("Failed to copy: ", err);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-6">
|
|
<div class="bg-gradient-to-r from-info/8 to-info/4 border border-info/25 rounded-xl p-4 lg:p-5 shadow-sm hover:shadow-md transition-shadow duration-200"
|
|
x-data="{
|
|
isCollapsed: localStorage.getItem('overview-notice-collapsed') === 'true',
|
|
isAdvancedView: localStorage.getItem('proxmox-view-mode') === 'advanced',
|
|
toggle() {
|
|
this.isCollapsed = !this.isCollapsed;
|
|
localStorage.setItem('overview-notice-collapsed', this.isCollapsed);
|
|
}
|
|
}"
|
|
x-init="
|
|
isAdvancedView = localStorage.getItem('proxmox-view-mode') === 'advanced';
|
|
window.addEventListener('proxmox-view-mode-changed', (e) => {
|
|
isAdvancedView = e.detail.isAdvancedView;
|
|
});
|
|
"
|
|
x-show="isAdvancedView"
|
|
x-transition>
|
|
<div class="flex items-start gap-3.5">
|
|
<svg class="size-5 text-info flex-shrink-0 mt-0.5 opacity-80" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center justify-between mb-2.5 gap-2">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-100">Cloud-init Configuration Notice</h3>
|
|
<button @click="toggle()" type="button" class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors flex-shrink-0">
|
|
<svg x-bind:class="isCollapsed ? 'size-4 rotate-180 transition-transform duration-200' : 'size-4 transition-transform duration-200'" fill="currentColor" viewBox="0 0 24 24"><path d="M12 15.5L6 9.5h12l-6 6z"/></svg>
|
|
</button>
|
|
</div>
|
|
<div x-show="!isCollapsed" x-transition class="space-y-2">
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 leading-relaxed font-medium">
|
|
<strong class="text-gray-700 dark:text-gray-200">Hostname, password, and networking settings</strong> are configured automatically via <strong class="text-gray-700 dark:text-gray-200">cloud-init</strong>.
|
|
This applies to standard operating system installations that support cloud-init. However, if you are:
|
|
</p>
|
|
<ul class="text-xs text-gray-600 dark:text-gray-400 leading-relaxed ml-5 list-disc space-y-1 opacity-90">
|
|
<li>Installing from an ISO image</li>
|
|
<li>Configuring the operating system manually during installation</li>
|
|
<li>Using Windows (which does not support cloud-init)</li>
|
|
</ul>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 leading-relaxed font-medium">
|
|
You will need to <strong class="text-gray-700 dark:text-gray-200">manually configure networking, security settings, and hostname</strong> within the operating system, as cloud-init will not be available in these scenarios.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div wire:init="checkStatus" wire:poll.{{ $fastPolling ? '2s' : '30s' }}="checkStatus"
|
|
wire:key="{{ $fastPolling ? 'fast' : 'slow' }}">
|
|
<x-proxmox::ui.section-header
|
|
title="Server Overview"
|
|
subtitle="Real-time resource monitoring and server configuration details."
|
|
icon="ri-server-line">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.help-modal icon="ri-question-line" title="Server Overview">
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Complete overview of your virtual machine including real-time resource usage, server status, network configuration, and DNS information.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
|
|
<div class="flex gap-3">
|
|
<div class="flex-shrink-0 mt-0.5">
|
|
<span class="flex items-center justify-center h-8 w-8 rounded-lg bg-primary/10 opacity-75">
|
|
<svg class="h-4 w-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><rect x="3" y="3" width="7" height="7" stroke-linecap="round" stroke-linejoin="round"/><rect x="14" y="3" width="7" height="7" stroke-linecap="round" stroke-linejoin="round"/><rect x="14" y="14" width="7" height="7" stroke-linecap="round" stroke-linejoin="round"/><rect x="3" y="14" width="7" height="7" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p class="block font-sans text-xs font-semibold text-base/90">Status</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">Power state with real-time updates every 2-30 seconds.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<div class="flex-shrink-0 mt-0.5">
|
|
<span class="flex items-center justify-center h-8 w-8 rounded-lg bg-success/10 opacity-75">
|
|
<svg class="h-4 w-4 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 9h6v6H9zM9 2v2M9 20v2M15 2v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p class="block font-sans text-xs font-semibold text-base/90">Resources</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">CPU, memory usage, and uptime statistics from the hypervisor.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<div class="flex-shrink-0 mt-0.5">
|
|
<span class="flex items-center justify-center h-8 w-8 rounded-lg bg-warning/10 opacity-75">
|
|
<svg class="h-4 w-4 text-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<p class="block font-sans text-xs font-semibold text-base/90">Network</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">IPv4/IPv6 addresses and DNS configuration for your VM.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-proxmox::ui.help-modal>
|
|
</x-slot>
|
|
</x-proxmox::ui.section-header>
|
|
<style>
|
|
/* TailAdmin Progress Bar Styles */
|
|
progress {
|
|
border-radius: 8px;
|
|
background-color: #f1f5f9;
|
|
border: none;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
height: 8px;
|
|
}
|
|
|
|
.dark progress {
|
|
background-color: #334155;
|
|
}
|
|
|
|
progress[value]::-webkit-progress-bar {
|
|
background-color: #f1f5f9;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.dark progress[value]::-webkit-progress-bar {
|
|
background-color: #334155;
|
|
}
|
|
|
|
progress[value]#memory::-webkit-progress-value {
|
|
background: linear-gradient(90deg, #22c55e 0%, #10b981 100%);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
progress[value]#memory::-moz-progress-bar {
|
|
background: linear-gradient(90deg, #22c55e 0%, #10b981 100%);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
progress[value]#cpu::-webkit-progress-value {
|
|
background: linear-gradient(90deg, #465fff 0%, #818cf8 100%);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
progress[value]#cpu::-moz-progress-bar {
|
|
background: linear-gradient(90deg, #465fff 0%, #818cf8 100%);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.formRight select {
|
|
background-color: white;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
padding: 0.625rem 1rem;
|
|
font-size: 0.875rem;
|
|
line-height: 1.25rem;
|
|
outline: none;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.dark .formRight select {
|
|
background-color: #1e293b;
|
|
border-color: #334155;
|
|
}
|
|
|
|
.formRight select:focus {
|
|
outline: none;
|
|
border-color: #465fff;
|
|
box-shadow: 0 0 0 3px rgba(70, 95, 255, 0.1);
|
|
}
|
|
</style>
|
|
>>>>>>> c5530e923149080e7d739823785456eb3267393f
|
|
|
|
@php
|
|
// Calculate resource metrics
|
|
$cpuCores = max(1, (int)($status->cpus ?? 1));
|
|
$cpuUsagePercent = round(max(0, ($status->cpu ?? 0) * 100), 1);
|
|
$memoryTotal = (int)($status->maxmem ?? 0);
|
|
$memoryUsed = (int)($status->mem ?? 0);
|
|
$memoryPercent = $memoryTotal > 0 ? round(($memoryUsed / $memoryTotal) * 100, 1) : 0;
|
|
<<<<<<< HEAD
|
|
=======
|
|
$manualSnapshotLimit = 5;
|
|
$manualSnapshotUsed = $server->statistics()->where('is_automatic', false)->count();
|
|
$manualSnapshotPercent = $manualSnapshotLimit > 0
|
|
? round(($manualSnapshotUsed / $manualSnapshotLimit) * 100, 1)
|
|
: 0;
|
|
|
|
$badgeForPercent = function (float $value) {
|
|
if ($value >= 100) {
|
|
return ['label' => 'Over entitlement', 'class' => 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900'];
|
|
}
|
|
|
|
if ($value >= 85) {
|
|
return ['label' => 'Watchlist', 'class' => 'text-amber-600 bg-amber-100 dark:text-amber-400 dark:bg-amber-900'];
|
|
}
|
|
|
|
return ['label' => 'Healthy', 'class' => 'text-emerald-600 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-900'];
|
|
};
|
|
|
|
$cpuBadge = $badgeForPercent($cpuUsagePercent);
|
|
$memoryBadge = $badgeForPercent($memoryPercent);
|
|
$reportedUptime = $this->status && isset($this->status->uptime)
|
|
? $this->formatSeconds($this->status->uptime)
|
|
: 'N/A';
|
|
|
|
$powerStats = $this->powerEventStats ?? [
|
|
'starts' => 0,
|
|
'shutdowns' => 0,
|
|
'reboots' => 0,
|
|
'uptime_percent' => 0,
|
|
'samples' => 0,
|
|
];
|
|
|
|
$resourceAllocations = [
|
|
[
|
|
'label' => 'CPU Allocation',
|
|
'used_percent' => $cpuUsagePercent,
|
|
'usage_summary' => number_format(($status->cpu ?? 0) * $cpuCores, 2) . ' / ' . $cpuCores . ' vCPU',
|
|
'entitlement_label' => $cpuCores . ' vCPU entitlement',
|
|
'description' => 'Live hypervisor usage sampled directly from the cluster.',
|
|
'gradient' => 'linear-gradient(90deg, #38bdf8 0%, #6366f1 100%)',
|
|
'badge' => $cpuBadge,
|
|
],
|
|
[
|
|
'label' => 'Memory Allocation',
|
|
'used_percent' => $memoryPercent,
|
|
'usage_summary' => ($memoryTotal > 0
|
|
? \Illuminate\Support\Number::fileSize(bytes: $memoryUsed) . ' / ' . \Illuminate\Support\Number::fileSize(bytes: $memoryTotal)
|
|
: \Illuminate\Support\Number::fileSize(bytes: $memoryUsed) . ' / N/A'),
|
|
'entitlement_label' => $memoryTotal > 0
|
|
? \Illuminate\Support\Number::fileSize(bytes: $memoryTotal) . ' entitlement'
|
|
: 'Entitlement unavailable',
|
|
'description' => 'Calculated from the VPS\'s provisioned RAM.',
|
|
'gradient' => 'linear-gradient(90deg, #34d399 0%, #10b981 100%)',
|
|
'badge' => $memoryBadge,
|
|
],
|
|
[
|
|
'label' => 'Hypervisor Uptime',
|
|
'used_percent' => null,
|
|
'usage_summary' => $reportedUptime,
|
|
'entitlement_label' => ($powerStats['starts'] ?? 0) . ' starts • ' . ($powerStats['shutdowns'] ?? 0) . ' shutdowns • ' . ($powerStats['reboots'] ?? 0) . ' reboots',
|
|
'description' => 'Shows the live uptime reported by the hypervisor plus counts from the task history.',
|
|
'gradient' => 'linear-gradient(90deg, #a855f7 0%, #ec4899 100%)',
|
|
'badge' => ['label' => 'Live', 'class' => 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-900'],
|
|
],
|
|
];
|
|
|
|
$networkRates = $this->networkRateStats ?? ['in_rate' => 0, 'out_rate' => 0];
|
|
$formatRate = function ($bytesPerSecond) {
|
|
if ($bytesPerSecond >= 1073741824) {
|
|
return number_format($bytesPerSecond / 1073741824, 2) . ' GiB/s';
|
|
}
|
|
if ($bytesPerSecond >= 1048576) {
|
|
return number_format($bytesPerSecond / 1048576, 2) . ' MiB/s';
|
|
}
|
|
if ($bytesPerSecond >= 1024) {
|
|
return number_format($bytesPerSecond / 1024, 2) . ' KiB/s';
|
|
}
|
|
|
|
return number_format($bytesPerSecond, 0) . ' B/s';
|
|
};
|
|
$rateScale = 5 * 1024 * 1024; // 5 MiB/s caps the bar width
|
|
$aggregateRate = ($networkRates['in_rate'] ?? 0) + ($networkRates['out_rate'] ?? 0);
|
|
$totalNetworkBytes = (int) (($status->netin ?? 0) + ($status->netout ?? 0));
|
|
$totalNetworkFormatted = number_format($totalNetworkBytes / 1024 / 1024, 2);
|
|
$lastSampleTimestamp = now()->toIso8601String();
|
|
@endphp
|
|
|
|
<!-- Resource Stats Grid -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-5"
|
|
wire:loading.class="opacity-50"
|
|
wire:target="checkStatus"
|
|
x-data="{ loaded: false }"
|
|
x-init="setTimeout(() => loaded = true, 100)">
|
|
@foreach($resourceAllocations as $index => $resource)
|
|
<div class="group bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm p-4 lg:p-5
|
|
transition-all duration-300 ease-out
|
|
hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700"
|
|
:class="loaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
:style="loaded ? 'transition-delay: {{ $index * 75 }}ms' : ''">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<h3 class="text-xs font-bold text-gray-600 dark:text-gray-400 uppercase tracking-widest opacity-75 group-hover:opacity-100 transition-opacity duration-200">{{ $resource['label'] }}</h3>
|
|
@if(isset($resource['badge']))
|
|
<span class="text-xs font-bold px-2.5 py-1 rounded-full transition-transform duration-200 group-hover:scale-105 {{ $resource['badge']['label'] === 'Healthy' ? 'bg-success/15 text-success opacity-90' : ($resource['badge']['label'] === 'Watchlist' ? 'bg-warning/15 text-warning opacity-90' : 'bg-error/15 text-error opacity-90') }}">
|
|
{{ $resource['badge']['label'] }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight mb-1 transition-colors duration-200 group-hover:text-primary">{{ $resource['usage_summary'] }}</div>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-75 font-medium group-hover:opacity-90 transition-opacity duration-200">{{ $resource['entitlement_label'] }}</p>
|
|
</div>
|
|
@if($resource['used_percent'] !== null)
|
|
<div class="space-y-2">
|
|
<div class="relative w-full h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shadow-inner">
|
|
<div class="absolute inset-y-0 left-0 rounded-full transition-all duration-700 ease-out origin-left"
|
|
x-data="{ width: 0 }"
|
|
x-init="setTimeout(() => width = {{ min(100, $resource['used_percent']) }}, 300)"
|
|
:style="'width: ' + width + '%; background: {{ $resource['gradient'] }}; box-shadow: 0 0 20px {{ $resource['gradient'] }};'"></div>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-semibold text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-white transition-colors duration-200">{{ number_format($resource['used_percent'], 1) }}%</span>
|
|
@if($resource['used_percent'] > 100)
|
|
<span class="text-xs font-bold text-error opacity-85 animate-pulse">Over entitlement</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<!-- Main Grid Layout -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-5"
|
|
x-data="{ cardsLoaded: false }"
|
|
x-init="setTimeout(() => cardsLoaded = true, 250)">
|
|
<!-- Server Information Card -->
|
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm overflow-hidden
|
|
transition-all duration-300 ease-out
|
|
hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700"
|
|
:class="cardsLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'">
|
|
<div class="flex items-center justify-between p-4 lg:p-5 bg-gradient-to-r from-primary/8 to-primary/4 border-b border-primary/20 transition-all duration-200 hover:from-primary/12 hover:to-primary/6">
|
|
<div class="flex items-center gap-2.5">
|
|
<svg class="size-5 text-primary opacity-80 transition-transform duration-200 hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="5" rx="1" fill="currentColor"/><rect x="2" y="10" width="20" height="5" rx="1" fill="currentColor"/><circle cx="6" cy="5.5" r="0.7" fill="white" opacity="0.8"/><circle cx="6" cy="12.5" r="0.7" fill="white" opacity="0.8"/></svg>
|
|
<h3 class="text-base sm:text-lg font-bold text-gray-900 dark:text-white">Server Information</h3>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 lg:p-5">
|
|
<div class="space-y-3.5">
|
|
<!-- Status -->
|
|
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:translate-x-0.5 group">
|
|
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80 group-hover:opacity-100 transition-opacity duration-200">
|
|
<svg class="size-4 opacity-75 group-hover:opacity-100 transition-all duration-200 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<span>Status</span>
|
|
</div>
|
|
<div>
|
|
@if ($status && $status->status === 'running')
|
|
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-success/15 text-success rounded-full opacity-90 transition-all duration-200 hover:opacity-100 hover:bg-success/20">
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full animate-pulse"></span>
|
|
Running
|
|
</span>
|
|
@else
|
|
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-warning/15 text-warning rounded-full opacity-90">
|
|
<svg class="size-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-linecap="round" stroke-linejoin="round"/><rect x="8" y="8" width="8" height="8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Stopped
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<!-- Operating System -->
|
|
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:translate-x-0.5 group">
|
|
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80 group-hover:opacity-100 transition-opacity duration-200">
|
|
<svg class="size-4 opacity-75 group-hover:opacity-100 transition-all duration-200 group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="4" r="1.5" fill="currentColor"/><circle cx="18" cy="15" r="1.5" fill="currentColor"/><circle cx="6" cy="15" r="1.5" fill="currentColor"/></svg>
|
|
<span>Operating System</span>
|
|
</div>
|
|
<span class="text-sm text-gray-900 dark:text-white font-medium opacity-90 group-hover:opacity-100 transition-opacity duration-200">{{ $server->os->name }}</span>
|
|
</div>
|
|
<!-- Hostname -->
|
|
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-all duration-200 hover:translate-x-0.5 group" x-data="{ showHostnameEditModal: @entangle('isEditingHostname') }">
|
|
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80 group-hover:opacity-100 transition-opacity duration-200">
|
|
<svg class="size-4 opacity-75 group-hover:opacity-100 transition-all duration-200 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 21h8M12 17v4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<span>Hostname</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90 group-hover:opacity-100 transition-opacity duration-200">{{ $server->hostname ?? $status->name ?? 'N/A' }}</code>
|
|
<button wire:click="startEditingHostname" type="button" class="text-primary opacity-70 hover:opacity-100 transition-all duration-200 hover:scale-110">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Hostname Edit Modal (unchanged for brevity) -->
|
|
<div x-cloak x-show="showHostnameEditModal"
|
|
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"
|
|
@click="showHostnameEditModal = false; $wire.cancelEditingHostname()"
|
|
@keydown.escape.window="showHostnameEditModal = false; $wire.cancelEditingHostname()"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50"
|
|
style="display: none;">
|
|
</div>
|
|
<div x-cloak x-show="showHostnameEditModal"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.away="showHostnameEditModal = false; $wire.cancelEditingHostname()"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="lte-card" style="width: 100%; max-width: 500px; margin: 0;">
|
|
<div class="lte-card-header lte-bg-primary" style="color: white;">
|
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
<h5 class="lte-card-title" style="margin: 0; color: white;">Edit Hostname</h5>
|
|
<button type="button" class="lte-btn lte-btn-sm" style="background: rgba(255,255,255,0.2); border: none; color: white;"
|
|
@click="showHostnameEditModal = false; $wire.cancelEditingHostname()">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="lte-card-body">
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="font-size: 0.875rem; font-weight: 600; display: block; margin-bottom: 0.5rem;">Hostname</label>
|
|
<input type="text" wire:model.live.debounce.300ms="hostname" class="lte-form-control" placeholder="Enter hostname" />
|
|
</div>
|
|
@if($hostnameError)
|
|
<div class="lte-alert lte-alert-danger" style="font-size: 0.875rem;">{{ $hostnameError }}</div>
|
|
@endif
|
|
<div class="lte-callout lte-callout-info" style="font-size: 0.875rem; padding: 0.75rem;">
|
|
<strong>Save:</strong> Database only. <strong>Save & Apply:</strong> + cloud-init on next boot.
|
|
</div>
|
|
</div>
|
|
<div class="lte-card-footer" style="display: flex; justify-content: flex-end; gap: 0.5rem;">
|
|
<button type="button" @click="showHostnameEditModal = false; $wire.cancelEditingHostname()" class="lte-btn lte-btn-secondary">
|
|
Cancel
|
|
</button>
|
|
<button type="button" wire:click="updateHostname" class="lte-btn lte-btn-primary">
|
|
Save
|
|
</button>
|
|
<button type="button" wire:click="applyHostname" class="lte-btn lte-btn-success">
|
|
<x-ri-check-line class="size-4" />
|
|
Save & Apply
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Primary IPv4 Address -->
|
|
@if($server->primaryIpv4)
|
|
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" x-data="{ copied: false, copyText: '{{ $server->primaryIpv4->ip }}', copy() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); } }">
|
|
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80">
|
|
<svg class="size-4 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<span>IPv4 Address</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90">{{ $server->primaryIpv4->ip }}</code>
|
|
<button @click="copy()" type="button" class="text-success opacity-70 hover:opacity-100 transition-opacity">
|
|
<x-ri-check-fill x-show="copied" class="size-4" />
|
|
<x-ri-file-copy-line x-show="!copied" class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Primary IPv6 Address -->
|
|
@if($server->primaryIpv6)
|
|
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" x-data="{ copied: false, copyText: '{{ $server->primaryIpv6->ip }}', copy() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); } }">
|
|
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80">
|
|
<svg class="size-4 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<span>IPv6 Address</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<code class="text-xs text-gray-900 dark:text-white font-mono truncate max-w-[200px] opacity-90">{{ $server->primaryIpv6->ip }}</code>
|
|
<button @click="copy()" type="button" class="text-success opacity-70 hover:opacity-100 transition-opacity flex-shrink-0">
|
|
<svg x-show="copied" class="size-4" fill="currentColor" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
|
<svg x-show="!copied" class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" stroke-linecap="round" stroke-linejoin="round"/><rect x="8" y="2" width="8" height="4" rx="1" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Root Password -->
|
|
<div class="flex items-center justify-between py-2.5 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" x-data="{ showPasswordViewModal: false, showPasswordEditModal: @entangle('isEditingPassword') }">
|
|
<div class="flex items-center gap-2 text-sm font-semibold text-gray-600 dark:text-gray-400 opacity-80">
|
|
<svg class="size-4 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 11V7a5 5 0 0 1 10 0v4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<span>Root Password</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm">
|
|
@if(isset($server->password) && $server->password)
|
|
••••••••
|
|
@else
|
|
<span class="inline-flex items-center px-2.5 py-1 text-xs font-semibold bg-info/15 text-info rounded-full opacity-90">Emailed</span>
|
|
@endif
|
|
</span>
|
|
@if(isset($server->password) && $server->password)
|
|
<button type="button" class="text-gray-500 opacity-70 hover:opacity-100 transition-opacity" @click="showPasswordViewModal = true">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
</button>
|
|
@endif
|
|
<button type="button" class="text-primary opacity-70 hover:opacity-100 transition-opacity" wire:click="startEditingPassword">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Password View Modal -->
|
|
<div x-cloak x-show="showPasswordViewModal"
|
|
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"
|
|
@click="showPasswordViewModal = false"
|
|
@keydown.escape.window="showPasswordViewModal = false"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50"
|
|
style="display: none;">
|
|
</div>
|
|
<div x-cloak x-show="showPasswordViewModal"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.away="showPasswordViewModal = false"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
|
style="display: none;"
|
|
x-data="{
|
|
copied: false,
|
|
copyText: {{ json_encode($server->password) }},
|
|
copy() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); }
|
|
}">
|
|
<div class="lte-card" style="width: 100%; max-width: 500px; margin: 0;">
|
|
<div class="lte-card-header lte-bg-primary" style="color: white;">
|
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
<h5 class="lte-card-title" style="margin: 0; color: white;">Root Password</h5>
|
|
<button type="button" class="lte-btn lte-btn-sm" style="background: rgba(255,255,255,0.2); border: none; color: white;" @click="showPasswordViewModal = false">
|
|
<x-ri-close-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="lte-card-body">
|
|
<div style="background: rgba(0,0,0,0.05); border: 1px solid rgba(0,0,0,0.1); border-radius: 0.375rem; padding: 1rem;">
|
|
<code style="display: block; font-family: 'Courier New', monospace; font-size: 0.875rem; word-break: break-all;">{{ $server->password }}</code>
|
|
</div>
|
|
</div>
|
|
<div class="lte-card-footer" style="display: flex; justify-content: flex-end;">
|
|
<button type="button" class="lte-btn lte-btn-success" @click="copy()">
|
|
<x-ri-file-copy-line class="size-4" />
|
|
<span x-show="!copied">Copy Password</span>
|
|
<span x-show="copied">Copied!</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Password Edit Modal (keeping structure but styling with AdminLTE) -->
|
|
<div x-cloak x-show="showPasswordEditModal"
|
|
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"
|
|
@click="showPasswordEditModal = false; $wire.cancelEditingPassword()"
|
|
@keydown.escape.window="showPasswordEditModal = false; $wire.cancelEditingPassword()"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50"
|
|
style="display: none;">
|
|
</div>
|
|
<div x-cloak x-show="showPasswordEditModal"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.away="showPasswordEditModal = false; $wire.cancelEditingPassword()"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="lte-card" style="width: 100%; max-width: 500px; margin: 0;">
|
|
<div class="lte-card-header lte-bg-primary" style="color: white;">
|
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
<h5 class="lte-card-title" style="margin: 0; color: white;">Edit Root Password</h5>
|
|
<button type="button" class="lte-btn lte-btn-sm" style="background: rgba(255,255,255,0.2); border: none; color: white;"
|
|
@click="showPasswordEditModal = false; $wire.cancelEditingPassword()">
|
|
<x-ri-close-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="lte-card-body">
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="font-size: 0.875rem; font-weight: 600; display: block; margin-bottom: 0.5rem;">New Password</label>
|
|
<input type="password" wire:model.live.debounce.300ms="password" class="lte-form-control" placeholder="Enter new password" />
|
|
</div>
|
|
<div style="margin-bottom: 1rem;">
|
|
<label style="font-size: 0.875rem; font-weight: 600; display: block; margin-bottom: 0.5rem;">Confirm Password</label>
|
|
<input type="password" wire:model.live.debounce.300ms="password_confirmation" class="lte-form-control" placeholder="Confirm password" />
|
|
</div>
|
|
@if($passwordError)
|
|
<div class="lte-alert lte-alert-danger" style="font-size: 0.875rem; margin-bottom: 1rem;">{{ $passwordError }}</div>
|
|
@endif
|
|
<div class="lte-callout lte-callout-info" style="font-size: 0.875rem; padding: 0.75rem;">
|
|
<strong>Save:</strong> Database only. <strong>Save & Apply:</strong> + cloud-init (reboot required).
|
|
</div>
|
|
</div>
|
|
<div class="lte-card-footer" style="display: flex; justify-content: flex-end; gap: 0.5rem;">
|
|
<button type="button" @click="showPasswordEditModal = false; $wire.cancelEditingPassword()" class="lte-btn lte-btn-secondary">
|
|
Cancel
|
|
</button>
|
|
<button type="button" wire:click="updatePassword" class="lte-btn lte-btn-primary">
|
|
Save
|
|
</button>
|
|
<button type="button" wire:click="applyPassword" class="lte-btn lte-btn-success">
|
|
<x-ri-check-line class="size-4" />
|
|
Save & Apply
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<x-proxmox::ui.button type="button" @click="showPasswordEditModal = false; $wire.cancelEditingPassword()" variant="secondary">
|
|
Cancel
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button type="button" wire:click="updatePassword" variant="primary">
|
|
Save
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button type="button" wire:click="applyPassword" variant="primary">
|
|
Save & Apply
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between bg-red-50 dark:bg-red-950 border-t border-x border-red-200 dark:border-red-800 px-3 py-2 rounded-b-lg">
|
|
<div>
|
|
<span class="text-sm font-semibold text-red-700 dark:text-red-300">Reinstall Server</span>
|
|
<p class="text-xs text-red-600 dark:text-red-400">Data permanently deleted. Irreversible.</p>
|
|
</div>
|
|
<livewire:proxmox.reinstall :server="$server" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700 transition-all duration-200 overflow-hidden">
|
|
<div style="background-image: url('https://billing.virtnet.bond/assets/images/dns.jpeg'); background-size: cover; background-position: center; height: 3rem;" class="border-b border-gray-200 dark:border-gray-800"></div>
|
|
<div class="p-4 lg:p-5 space-y-4">
|
|
<div class="flex items-center gap-2.5">
|
|
<x-ri-global-line class="size-5 text-primary opacity-80" />
|
|
<h2 class="text-base sm:text-lg font-bold text-gray-900 dark:text-white">DNS Configuration</h2>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-gray-400 opacity-70 hover:opacity-100 transition-opacity">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<div x-show="open" @click="open = false" @keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50 backdrop-blur-sm" style="display: none;"></div>
|
|
<div x-show="open" @click.away="open = false"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4" style="display: none;">
|
|
<div x-transition:enter="transition ease-out duration-200 translate-y-4"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
class="relative w-full max-w-xl max-h-[90vh] overflow-y-auto bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6 lg:p-8">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-gray-400 opacity-75 hover:opacity-100 transition-opacity">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/15 flex-shrink-0">
|
|
<x-ri-global-line class="size-5 text-primary opacity-80" />
|
|
</div>
|
|
<div>
|
|
<h6 class="text-lg font-bold text-gray-900 dark:text-white">Domain Name System (DNS)</h6>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">DNS configuration for your virtual server</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 opacity-90 mb-4">
|
|
DNS info for your VM. Displays hostname and DNS configuration for the guest OS. Multiple DNS providers are available for flexible hostname resolution.
|
|
</p>
|
|
<div class="bg-info/10 border border-info/25 rounded-lg p-4 mb-4">
|
|
<p class="text-xs text-info font-semibold mb-2 opacity-90">
|
|
<x-ri-information-line class="inline size-3.5 mr-1" />
|
|
Important Note
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-300 opacity-80">
|
|
DNS is configured via cloud-init. For Windows or manual configuration, you'll need to configure DNS directly within the guest OS.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="text-sm font-semibold text-gray-700 dark:text-gray-300 opacity-80">Primary IPv6: <code class="text-xs font-mono text-gray-900 dark:text-white opacity-90">{{ $server->primaryIpv6->ip }}</code></div>
|
|
|
|
<!-- Virtnet -->
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
<div class="min-w-0 flex-1">
|
|
<span class="text-xs font-semibold text-gray-600 dark:text-gray-400 opacity-80 block mb-1">Virtnet Internal</span>
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90">{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.virtnet.host' }}</code>
|
|
</div>
|
|
<div x-data="{
|
|
copied: false,
|
|
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.virtnet.host' }}',
|
|
copyToClipboard() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); }
|
|
}">
|
|
<button @click="copyToClipboard()" type="button" class="text-gray-500 opacity-70 hover:opacity-100 transition-opacity ml-3 flex-shrink-0">
|
|
<span x-show="copied" x-transition class="inline"><x-ri-check-fill class="size-4 text-success" /></span>
|
|
<span x-show="!copied" x-transition class="inline"><x-ri-file-copy-line class="size-4" /></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- nip.io -->
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
<div class="min-w-0 flex-1">
|
|
<span class="text-xs font-semibold text-gray-600 dark:text-gray-400 opacity-80 block mb-1">nip.io (Public)</span>
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90">{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.nip.io' }}</code>
|
|
</div>
|
|
<div x-data="{
|
|
copied: false,
|
|
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.nip.io' }}',
|
|
copyToClipboard() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); }
|
|
}">
|
|
<button @click="copyToClipboard()" type="button" class="text-gray-500 opacity-70 hover:opacity-100 transition-opacity ml-3 flex-shrink-0">
|
|
<span x-show="copied" x-transition class="inline"><x-ri-check-fill class="size-4 text-success" /></span>
|
|
<span x-show="!copied" x-transition class="inline"><x-ri-file-copy-line class="size-4" /></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- sslip.io -->
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
<div class="min-w-0 flex-1">
|
|
<span class="text-xs font-semibold text-gray-600 dark:text-gray-400 opacity-80 block mb-1">sslip.io (Public)</span>
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90">{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.sslip.io' }}</code>
|
|
</div>
|
|
<div x-data="{
|
|
copied: false,
|
|
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.sslip.io' }}',
|
|
copyToClipboard() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); }
|
|
}">
|
|
<button @click="copyToClipboard()" type="button" class="text-gray-500 opacity-70 hover:opacity-100 transition-opacity ml-3 flex-shrink-0">
|
|
<span x-show="copied" x-transition class="inline"><x-ri-check-fill class="size-4 text-success" /></span>
|
|
<span x-show="!copied" x-transition class="inline"><x-ri-file-copy-line class="size-4" /></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- backname.io -->
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
<div class="min-w-0 flex-1">
|
|
<span class="text-xs font-semibold text-gray-600 dark:text-gray-400 opacity-80 block mb-1">backname.io (Public)</span>
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono opacity-90">{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.backname.io' }}</code>
|
|
</div>
|
|
<div x-data="{
|
|
copied: false,
|
|
copyText: '{{ str_replace(':', '-', $server->primaryIpv6->ip) . '.backname.io' }}',
|
|
copyToClipboard() { navigator.clipboard.writeText(this.copyText); this.copied = true; setTimeout(() => this.copied = false, 2000); }
|
|
}">
|
|
<button @click="copyToClipboard()" type="button" class="text-gray-500 opacity-70 hover:opacity-100 transition-opacity ml-3 flex-shrink-0">
|
|
<span x-show="copied" x-transition class="inline"><x-ri-check-fill class="size-4 text-success" /></span>
|
|
<span x-show="!copied" x-transition class="inline"><x-ri-file-copy-line class="size-4" /></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gradient-to-r from-info/8 to-info/4 border-t border-info/20 p-4 rounded-b-2xl">
|
|
<p class="text-xs font-semibold text-gray-700 dark:text-gray-300 opacity-90 mb-2">
|
|
<svg class="inline size-3.5 mr-1 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 9h6v6H9z" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
DNS Resolution
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80">
|
|
<code class="font-mono">@</code> = root domain → {{ $server->primaryIpv6->ip }} | <code class="font-mono">*.@</code> = wildcard subdomains → same IP
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-75 mt-2">
|
|
rDNS PTR: No PTR records via RIR systems except direct contract customers.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if(stripos($server->os->name, 'Windows') !== false)
|
|
<div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700 transition-all duration-200 overflow-hidden">
|
|
<div style="background-image: url('https://billing.virtnet.bond/assets/images/windows.jpg'); background-size: cover; background-position: center; height: 3rem;" class="border-b border-gray-200 dark:border-gray-800"></div>
|
|
<div class="p-4 lg:p-5 space-y-4">
|
|
<div class="flex items-center gap-2.5">
|
|
<svg class="size-5 text-primary opacity-80" fill="currentColor" viewBox="0 0 24 24"><path d="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 0h8v8h-8v-8z"/></svg>
|
|
<h2 class="text-base sm:text-lg font-bold text-gray-900 dark:text-white">Windows Server</h2>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-gray-400 opacity-70 hover:opacity-100 transition-opacity">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<div x-show="open" @click="open = false" @keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50 backdrop-blur-sm" style="display: none;"></div>
|
|
<div x-show="open" @click.away="open = false"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4" style="display: none;">
|
|
<div x-transition:enter="transition ease-out duration-200 translate-y-4"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
class="relative w-full max-w-xl max-h-[90vh] overflow-y-auto bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6 lg:p-8">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-gray-400 opacity-75 hover:opacity-100 transition-opacity">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/15 flex-shrink-0">
|
|
<x-ri-windows-line class="size-5 text-primary opacity-80" />
|
|
</div>
|
|
<div>
|
|
<h6 class="text-lg font-bold text-gray-900 dark:text-white">Windows Server Configuration</h6>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">Manual configuration required</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 opacity-90 mb-4">
|
|
Windows doesn't support cloud-init for automated configuration. You'll need to manually configure networking, hostname, and system settings during or after installation.
|
|
</p>
|
|
<div class="space-y-3">
|
|
<div class="bg-info/10 border border-info/25 rounded-lg p-3.5">
|
|
<p class="text-xs font-semibold text-info opacity-90 mb-2">
|
|
<svg class="inline size-3.5 mr-1 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><circle cx="12" cy="12" r="1" stroke-linecap="round" stroke-linejoin="round"/><path d="M5.64 5.64a9 9 0 0112.72 0M2 12a10 10 0 0120 0" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Network Configuration
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-300 opacity-80">Use static IPs or DHCP via Windows Network Configuration. Cloud-init doesn't configure networking on Windows.</p>
|
|
</div>
|
|
<div class="bg-warning/10 border border-warning/25 rounded-lg p-3.5">
|
|
<p class="text-xs font-semibold text-warning opacity-90 mb-2">
|
|
<svg class="inline size-3.5 mr-1 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Licensing
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-300 opacity-80">No license key provided. You must maintain compliant licensing for your Windows installation.</p>
|
|
</div>
|
|
<div class="bg-error/10 border border-error/25 rounded-lg p-3.5">
|
|
<p class="text-xs font-semibold text-error opacity-90 mb-2">
|
|
<svg class="inline size-3.5 mr-1 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M12 9v2m0 4v2m7.07-10.07A10 10 0 113.93 19.93" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Compliance Audits
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-300 opacity-80">Periodic audits may occur. Non-compliant installations are subject to suspension.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-start gap-3 p-3.5 rounded-lg border border-info/20 bg-info/5 hover:bg-info/10 transition-colors">
|
|
<svg class="size-4 text-info opacity-80 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><polyline points="20 6 9 17 4 12" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<div class="min-w-0">
|
|
<p class="text-xs font-semibold text-gray-900 dark:text-white opacity-90">IPv4/IPv6 Setup</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">Configure addresses manually within Windows Network Configuration.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-3 p-3.5 rounded-lg border border-warning/20 bg-warning/5 hover:bg-warning/10 transition-colors">
|
|
<svg class="size-4 text-warning opacity-80 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke-linecap="round" stroke-linejoin="round"/><polyline points="8 12 12 16 16 12" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<div class="min-w-0">
|
|
<p class="text-xs font-semibold text-gray-900 dark:text-white opacity-90">Licensing Compliance</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">You're responsible for maintaining valid Windows licensing for your installation.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-3 p-3.5 rounded-lg border border-error/20 bg-error/5 hover:bg-error/10 transition-colors">
|
|
<svg class="size-4 text-error opacity-80 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M12 9v2m0 4v2m-9.01 4.79A10 10 0 1021 12a9.94 9.94 0 01-8.99 9.79" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<div class="min-w-0">
|
|
<p class="text-xs font-semibold text-gray-900 dark:text-white opacity-90">Audit Risks</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">Non-compliant installations may be suspended after verification audits.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Notes Section -->
|
|
<div class="mt-4">
|
|
<div id="notes-section" class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm hover:shadow-md hover:border-gray-300 dark:hover:border-gray-700 transition-all duration-200 p-4 lg:p-5 flex flex-col" x-data="{ showExportErrorModal: false }" x-cloak>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-2.5">
|
|
<svg class="size-5 text-primary opacity-80" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke-linecap="round" stroke-linejoin="round"/><polyline points="14 2 14 8 20 8" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="11" x2="12" y2="17" stroke-linecap="round" stroke-linejoin="round"/><line x1="9" y1="14" x2="15" y2="14" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
<h2 class="text-base sm:text-lg font-bold text-gray-900 dark:text-white">Notes</h2>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-gray-400 opacity-70 hover:opacity-100 transition-opacity">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<div x-show="open" @click="open = false" @keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50 backdrop-blur-sm" style="display: none;"></div>
|
|
<div x-show="open" @click.away="open = false" @keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4" style="display: none;">
|
|
<div x-transition:enter="transition ease-out duration-200 translate-y-4"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
class="relative w-full max-w-xl max-h-[90vh] overflow-y-auto bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6 lg:p-8">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-gray-400 opacity-75 hover:opacity-100 transition-opacity">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/15 flex-shrink-0">
|
|
<x-ri-file-text-line class="size-5 text-primary opacity-80" />
|
|
</div>
|
|
<div>
|
|
<h6 class="text-lg font-bold text-gray-900 dark:text-white">Server Notes</h6>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">Store and reference information about your server</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 opacity-90 mb-4">
|
|
Store notes about this VM with full Markdown support. Notes are persisted to the database but will be cleared when you reinstall the server.
|
|
</p>
|
|
<div class="space-y-3">
|
|
<div class="bg-info/10 border border-info/25 rounded-lg p-3.5">
|
|
<p class="text-xs font-semibold text-info opacity-90 mb-2">
|
|
<svg class="inline size-3.5 mr-1 opacity-75" fill="currentColor" viewBox="0 0 24 24"><path d="M3 3h18v18H3V3zm2 2v14h14V5H5zM7 9h2v6H7V9zm8 0h2v6h-2V9zM7 7h8v2H7V7z"/></svg>
|
|
Markdown Support
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-300 opacity-80">Headers, lists, links, code blocks, and formatting are all supported.</p>
|
|
</div>
|
|
<div class="bg-warning/10 border border-warning/25 rounded-lg p-3.5">
|
|
<p class="text-xs font-semibold text-warning opacity-90 mb-2">
|
|
<svg class="inline size-3.5 mr-1 opacity-75" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 5v14a9 3 0 0 0 18 0V5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 12a9 3 0 0 0 18 0" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Persistence
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-300 opacity-80">Notes are saved to the database. They will be deleted when you reinstall the server.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span class="text-xs text-gray-600 dark:text-gray-400 opacity-75">(Markdown • Cleared on reinstall)</span>
|
|
</div>
|
|
<button type="button" @click="exportNotesToCSV()"
|
|
class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 px-3 py-1.5 rounded-lg text-xs font-semibold transition-colors flex items-center gap-1.5 opacity-80 hover:opacity-100"
|
|
title="Export notes">
|
|
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" stroke-linecap="round" stroke-linejoin="round"/><polyline points="7 10 12 15 17 10" stroke-linecap="round" stroke-linejoin="round"/><line x1="12" y1="15" x2="12" y2="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Export
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Export Error Modal -->
|
|
<div x-show="showExportErrorModal"
|
|
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"
|
|
@click="showExportErrorModal = false"
|
|
@keydown.escape.window="showExportErrorModal = false"
|
|
class="fixed inset-0 z-[60] bg-gray-900 bg-opacity-50 backdrop-blur-sm"
|
|
style="display: none;">
|
|
</div>
|
|
<div x-show="showExportErrorModal" @click.away="showExportErrorModal = false"
|
|
x-transition:enter="transition ease-out duration-200 translate-y-4"
|
|
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="fixed inset-0 z-[60] flex items-center justify-center p-4" style="display: none;">
|
|
<div class="relative w-full max-w-sm bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button @click="showExportErrorModal = false" class="absolute top-4 right-4 text-gray-400 opacity-75 hover:opacity-100 transition-opacity">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-error/15 flex-shrink-0">
|
|
<x-ri-alert-line class="size-5 text-error opacity-80" />
|
|
</div>
|
|
<div>
|
|
<h6 class="text-base font-bold text-gray-900 dark:text-white">Export Error</h6>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 opacity-80 mt-1">Unable to export notes</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 opacity-90 mb-5">No notes available to export. Add some notes first.</p>
|
|
<div class="flex justify-end">
|
|
<button type="button" @click="showExportErrorModal = false" class="bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 px-3 py-1.5 rounded-lg text-sm font-semibold transition-colors">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 flex-1">
|
|
@if($isEditingNotes)
|
|
<div class="space-y-3 flex-1 flex flex-col">
|
|
<textarea wire:model.live.debounce.500ms="notes"
|
|
class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2 text-sm w-full resize-y flex-1 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors font-mono"
|
|
placeholder="Enter notes with Markdown support..."></textarea>
|
|
@if($notesError)
|
|
<div class="flex items-center gap-2 text-xs text-error opacity-90">
|
|
<x-ri-alert-line class="size-3.5" />
|
|
{{ $notesError }}
|
|
</div>
|
|
@endif
|
|
<div class="flex gap-2">
|
|
<button wire:click="saveNotes" wire:loading.attr="disabled" class="bg-primary hover:bg-primary/90 disabled:opacity-50 text-white px-3 py-1.5 rounded-lg text-sm font-semibold transition-colors">
|
|
<span wire:loading.remove wire:target="saveNotes">Save</span>
|
|
<span wire:loading wire:target="saveNotes">Saving...</span>
|
|
</button>
|
|
<button wire:click="cancelEditingNotes" wire:loading.attr="disabled" class="bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 px-3 py-1.5 rounded-lg text-sm font-semibold transition-colors">Cancel</button>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="space-y-3 flex-1 flex flex-col">
|
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2.5 text-sm flex-1 overflow-auto prose prose-sm dark:prose-invert max-w-none text-gray-700 dark:text-gray-300 leading-relaxed">
|
|
@if($notes)
|
|
{!! \Illuminate\Support\Str::markdown(e($notes), [
|
|
'allow_unsafe_links' => false,
|
|
'renderer' => ['soft_break' => "<br>"]
|
|
]) !!}
|
|
@else
|
|
<p class="text-gray-500 dark:text-gray-400 opacity-80">No notes yet. Click Edit to add some.</p>
|
|
@endif
|
|
</div>
|
|
<button wire:click="startEditingNotes" class="bg-primary hover:bg-primary/90 text-white px-3 py-1.5 rounded-lg text-sm font-semibold transition-colors inline-flex items-center gap-1.5 w-fit">
|
|
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Edit
|
|
</button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server Information Section -->
|
|
@php
|
|
// Gather comprehensive server information
|
|
>>>>>>> c5530e923149080e7d739823785456eb3267393f
|
|
$diskTotal = (int)($status->maxdisk ?? 0);
|
|
$diskUsed = (int)($status->disk ?? 0);
|
|
$diskPercent = $diskTotal > 0 ? round(($diskUsed / $diskTotal) * 100, 1) : 0;
|
|
|
|
// Uptime
|
|
$reportedUptime = $this->status && isset($this->status->uptime)
|
|
? $this->formatSeconds($this->status->uptime)
|
|
: 'N/A';
|
|
|
|
// Power stats
|
|
$powerStats = $this->powerEventStats ?? [
|
|
'starts' => 0,
|
|
'shutdowns' => 0,
|
|
'reboots' => 0,
|
|
];
|
|
|
|
// Network
|
|
$totalNetIn = (int)($status->netin ?? 0);
|
|
$totalNetOut = (int)($status->netout ?? 0);
|
|
|
|
// Product/Service info
|
|
$productSettings = $server->service?->product?->settings ?? collect();
|
|
$allocatedCores = $productSettings->where('key', 'cores')->first()?->value ?? $cpuCores;
|
|
$allocatedMemoryMiB = $productSettings->where('key', 'memory')->first()?->value ?? 0;
|
|
$allocatedDiskGiB = $productSettings->where('key', 'disk')->first()?->value ?? 0;
|
|
$allocatedBackups = $productSettings->where('key', 'backups')->first()?->value ?? 0;
|
|
$servicePlan = $server->service?->plan;
|
|
$serviceExpires = $server->service?->expires_at;
|
|
$serviceStatus = $server->service?->status ?? 'unknown';
|
|
$productName = $server->service?->product?->name ?? 'Unknown Product';
|
|
$locationName = $server->node?->location?->name ?? 'Unknown';
|
|
$nodeName = $server->node?->name ?? 'Unknown';
|
|
|
|
// Badge helper
|
|
$badgeForPercent = function (float $value) {
|
|
if ($value >= 100) return ['label' => 'Critical', 'class' => 'bg-error/15 text-error'];
|
|
if ($value >= 85) return ['label' => 'Warning', 'class' => 'bg-warning/15 text-warning'];
|
|
return ['label' => 'Healthy', 'class' => 'bg-success/15 text-success'];
|
|
};
|
|
$cpuBadge = $badgeForPercent($cpuUsagePercent);
|
|
$memoryBadge = $badgeForPercent($memoryPercent);
|
|
@endphp
|
|
|
|
<div class="space-y-6" x-data="{ pageLoaded: false }" x-init="setTimeout(() => pageLoaded = true, 50)" wire:init="checkStatus" wire:poll.{{ $fastPolling ? '2s' : '30s' }}="checkStatus" wire:key="{{ $fastPolling ? 'fast' : 'slow' }}">
|
|
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col gap-2"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'"
|
|
style="transition: all 0.4s ease-out;">
|
|
<x-proxmox::ui.section-header
|
|
title="Server Overview"
|
|
icon="ri-server-line"
|
|
subtitle="Real-time monitoring dashboard for your virtual machine."
|
|
class="mb-0">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.help-modal title="Server Overview" icon="ri-question-line">
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
The Server Overview provides a comprehensive real-time dashboard of your virtual machine's status, resource utilization, and configuration details.
|
|
</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">Resource Metrics:</span> Live CPU, memory, and storage utilization with health indicators.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Server Details:</span> Operating system, hostname, IP addresses, and credentials.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">DNS Configuration:</span> Available hostname resolution options for your server.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Infrastructure:</span> Datacenter location, billing cycle, and service information.
|
|
</p>
|
|
</div>
|
|
</x-proxmox::ui.help-modal>
|
|
</x-slot>
|
|
</x-proxmox::ui.section-header>
|
|
</div>
|
|
|
|
<!-- Resource Metrics Section -->
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-theme-sm transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
style="transition: all 0.4s ease-out 0.1s;">
|
|
<div class="flex items-center gap-2 mb-5">
|
|
<x-ri-dashboard-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Resource Metrics</h2>
|
|
<span class="ml-auto inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-full {{ $status && $status->status === 'running' ? 'bg-success/15 text-success' : 'bg-warning/15 text-warning' }}">
|
|
@if($status && $status->status === 'running')
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full animate-pulse"></span>
|
|
Running
|
|
@else
|
|
<span class="w-1.5 h-1.5 bg-warning rounded-full"></span>
|
|
Stopped
|
|
@endif
|
|
</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
|
<!-- CPU -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
|
<x-ri-cpu-line class="size-4 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">CPU</span>
|
|
</div>
|
|
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{ $cpuBadge['class'] }}">{{ $cpuBadge['label'] }}</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-gray-900 dark:text-white mb-1">{{ $cpuUsagePercent }}%</div>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">{{ number_format(($status->cpu ?? 0) * $cpuCores, 2) }} / {{ $cpuCores }} vCPU</p>
|
|
<div class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div class="h-full bg-gradient-to-r from-blue-400 to-blue-600 rounded-full transition-all duration-500" style="width: {{ min(100, $cpuUsagePercent) }}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Memory -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-8 rounded-lg bg-green-100 dark:bg-green-900/50 flex items-center justify-center">
|
|
<x-ri-database-2-line class="size-4 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">Memory</span>
|
|
</div>
|
|
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{ $memoryBadge['class'] }}">{{ $memoryBadge['label'] }}</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-gray-900 dark:text-white mb-1">{{ $memoryPercent }}%</div>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">{{ \Illuminate\Support\Number::fileSize(bytes: $memoryUsed) }} / {{ \Illuminate\Support\Number::fileSize(bytes: $memoryTotal) }}</p>
|
|
<div class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div class="h-full bg-gradient-to-r from-green-400 to-green-600 rounded-full transition-all duration-500" style="width: {{ min(100, $memoryPercent) }}%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Uptime -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-8 h-8 rounded-lg bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
|
|
<x-ri-time-line class="size-4 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">Uptime</span>
|
|
</div>
|
|
<span class="text-xs font-medium px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400">Live</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-gray-900 dark:text-white mb-1">{{ $reportedUptime }}</div>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $powerStats['starts'] }} starts • {{ $powerStats['shutdowns'] }} shutdowns • {{ $powerStats['reboots'] }} reboots</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Server Details & DNS Grid -->
|
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
|
|
|
<!-- Server Details Section -->
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
style="transition: all 0.4s ease-out 0.2s;">
|
|
<div class="flex items-center gap-2 p-5 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-server-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Server Details</h2>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-3">
|
|
<!-- Status -->
|
|
<div class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Status</span>
|
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-semibold rounded-full {{ $status && $status->status === 'running' ? 'bg-success/15 text-success' : 'bg-warning/15 text-warning' }}">
|
|
@if($status && $status->status === 'running')
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full animate-pulse"></span>
|
|
Running
|
|
@else
|
|
Stopped
|
|
@endif
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Operating System -->
|
|
<div class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Operating System</span>
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ $server->os->name }}</span>
|
|
</div>
|
|
|
|
<!-- Hostname -->
|
|
<div class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" x-data="{ showHostnameEditModal: @entangle('isEditingHostname') }">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Hostname</span>
|
|
<div class="flex items-center gap-2">
|
|
<code class="text-sm font-mono text-gray-900 dark:text-white">{{ $server->hostname ?? $status->name ?? 'N/A' }}</code>
|
|
<button wire:click="startEditingHostname" type="button" class="text-primary opacity-70 hover:opacity-100 transition-opacity">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Primary IPv4 -->
|
|
@if($server->primaryIpv4)
|
|
<div class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" x-data="{ copied: false }">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">IPv4 Address</span>
|
|
<div class="flex items-center gap-2">
|
|
<code class="text-sm font-mono text-gray-900 dark:text-white">{{ $server->primaryIpv4->ip }}</code>
|
|
<button @click="navigator.clipboard.writeText('{{ $server->primaryIpv4->ip }}'); copied = true; setTimeout(() => copied = false, 2000)" type="button" class="text-gray-400 hover:text-success transition-colors">
|
|
<x-ri-check-fill x-show="copied" class="size-4 text-success" x-cloak />
|
|
<x-ri-file-copy-line x-show="!copied" class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Primary IPv6 -->
|
|
@if($server->primaryIpv6)
|
|
<div class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" x-data="{ copied: false }">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">IPv6 Address</span>
|
|
<div class="flex items-center gap-2">
|
|
<code class="text-xs font-mono text-gray-900 dark:text-white truncate max-w-[180px]">{{ $server->primaryIpv6->ip }}</code>
|
|
<button @click="navigator.clipboard.writeText('{{ $server->primaryIpv6->ip }}'); copied = true; setTimeout(() => copied = false, 2000)" type="button" class="text-gray-400 hover:text-success transition-colors flex-shrink-0">
|
|
<x-ri-check-fill x-show="copied" class="size-4 text-success" x-cloak />
|
|
<x-ri-file-copy-line x-show="!copied" class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Root Password -->
|
|
<div class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors" x-data="{ showPasswordViewModal: false, showPasswordEditModal: @entangle('isEditingPassword') }">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Root Password</span>
|
|
<div class="flex items-center gap-2">
|
|
@if(isset($server->password) && $server->password)
|
|
<span class="text-sm text-gray-500">••••••••</span>
|
|
<button type="button" class="text-gray-400 hover:text-gray-600 transition-colors" @click="showPasswordViewModal = true">
|
|
<x-ri-eye-line class="size-4" />
|
|
</button>
|
|
@else
|
|
<span class="text-xs font-medium px-2 py-0.5 rounded-full bg-info/15 text-info">Emailed</span>
|
|
@endif
|
|
<button type="button" class="text-primary opacity-70 hover:opacity-100 transition-opacity" wire:click="startEditingPassword">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reinstall Footer -->
|
|
<div class="flex items-center justify-between bg-error/5 border-t border-error/20 px-5 py-3">
|
|
<div>
|
|
<span class="text-sm font-semibold text-error">Reinstall Server</span>
|
|
<p class="text-xs text-error/70">Data permanently deleted. Irreversible.</p>
|
|
</div>
|
|
<livewire:proxmox.reinstall :server="$server" />
|
|
</div>
|
|
</section>
|
|
|
|
<!-- DNS Configuration Section -->
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
style="transition: all 0.4s ease-out 0.25s;">
|
|
<div class="flex items-center gap-2 p-5 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-global-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">DNS Configuration</h2>
|
|
<x-proxmox::ui.help-modal title="DNS Configuration" icon="ri-question-line" class="ml-auto">
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
DNS hostnames that automatically resolve to your server's IP address. These work without any configuration.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Note:</span> DNS is configured via cloud-init. For Windows or manual configuration, configure DNS directly within the guest OS.
|
|
</p>
|
|
</div>
|
|
</x-proxmox::ui.help-modal>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-3">
|
|
@if($server->primaryIpv6)
|
|
@php
|
|
$ipv6Dashed = str_replace(':', '-', $server->primaryIpv6->ip);
|
|
$dnsProviders = [
|
|
['name' => 'Virtnet Internal', 'domain' => $ipv6Dashed . '.virtnet.host', 'badge' => 'Internal'],
|
|
['name' => 'nip.io', 'domain' => $ipv6Dashed . '.nip.io', 'badge' => 'Public'],
|
|
['name' => 'sslip.io', 'domain' => $ipv6Dashed . '.sslip.io', 'badge' => 'Public'],
|
|
['name' => 'backname.io', 'domain' => $ipv6Dashed . '.backname.io', 'badge' => 'Public'],
|
|
];
|
|
@endphp
|
|
|
|
@foreach($dnsProviders as $provider)
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors" x-data="{ copied: false }">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="text-xs font-semibold text-gray-600 dark:text-gray-400">{{ $provider['name'] }}</span>
|
|
<span class="text-xs px-1.5 py-0.5 rounded bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400">{{ $provider['badge'] }}</span>
|
|
</div>
|
|
<code class="text-sm text-gray-900 dark:text-white font-mono break-all">{{ $provider['domain'] }}</code>
|
|
</div>
|
|
<button @click="navigator.clipboard.writeText('{{ $provider['domain'] }}'); copied = true; setTimeout(() => copied = false, 2000)" type="button" class="text-gray-400 hover:text-success transition-colors ml-3 flex-shrink-0">
|
|
<x-ri-check-fill x-show="copied" class="size-4 text-success" x-cloak />
|
|
<x-ri-file-copy-line x-show="!copied" class="size-4" />
|
|
</button>
|
|
</div>
|
|
@endforeach
|
|
@else
|
|
<div class="text-center py-8">
|
|
<x-ri-global-line class="size-10 text-gray-300 dark:text-gray-600 mx-auto mb-3" />
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">No IPv6 address assigned</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 border-t border-gray-100 dark:border-gray-800 px-5 py-3">
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
<x-ri-information-line class="inline size-3.5 mr-1" />
|
|
<code class="font-mono">@</code> = root domain • <code class="font-mono">*.@</code> = wildcard subdomains
|
|
</p>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- Notes & Windows Sections Grid -->
|
|
<div class="grid grid-cols-1 {{ stripos($server->os->name, 'Windows') !== false ? 'xl:grid-cols-2' : '' }} gap-6">
|
|
|
|
<!-- Notes Section -->
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
style="transition: all 0.4s ease-out 0.3s;"
|
|
x-data="{ showExportErrorModal: false }">
|
|
<div class="flex items-center gap-2 p-5 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-file-text-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Notes</h2>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">(Markdown supported)</span>
|
|
<div class="ml-auto flex items-center gap-2">
|
|
<button type="button" @click="exportNotesToCSV()"
|
|
class="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors flex items-center gap-1">
|
|
<x-ri-download-line class="size-3.5" />
|
|
Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-5">
|
|
@if($isEditingNotes)
|
|
<div class="space-y-4">
|
|
<textarea wire:model.live.debounce.500ms="notes"
|
|
class="w-full min-h-[150px] bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors resize-y"
|
|
placeholder="Enter notes with Markdown support..."></textarea>
|
|
@if($notesError)
|
|
<p class="text-xs text-error flex items-center gap-1">
|
|
<x-ri-alert-line class="size-3.5" />
|
|
{{ $notesError }}
|
|
</p>
|
|
@endif
|
|
<div class="flex gap-2">
|
|
<button wire:click="saveNotes" wire:loading.attr="disabled" class="px-4 py-2 bg-primary hover:bg-primary/90 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors">
|
|
<span wire:loading.remove wire:target="saveNotes">Save Notes</span>
|
|
<span wire:loading wire:target="saveNotes">Saving...</span>
|
|
</button>
|
|
<button wire:click="cancelEditingNotes" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-sm font-medium rounded-lg transition-colors">Cancel</button>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="space-y-4">
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700/50 rounded-lg px-4 py-3 min-h-[100px] prose prose-sm dark:prose-invert max-w-none">
|
|
@if($notes)
|
|
{!! \Illuminate\Support\Str::markdown(e($notes), ['allow_unsafe_links' => false, 'renderer' => ['soft_break' => "<br>"]]) !!}
|
|
@else
|
|
<p class="text-gray-400 dark:text-gray-500 italic">No notes yet. Click Edit to add some.</p>
|
|
@endif
|
|
</div>
|
|
<button wire:click="startEditingNotes" class="px-4 py-2 bg-primary hover:bg-primary/90 text-white text-sm font-medium rounded-lg transition-colors inline-flex items-center gap-2">
|
|
<x-ri-edit-line class="size-4" />
|
|
Edit Notes
|
|
</button>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Export Error Modal -->
|
|
<div x-show="showExportErrorModal" x-cloak
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
@click="showExportErrorModal = false"
|
|
class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm">
|
|
</div>
|
|
<div x-show="showExportErrorModal" x-cloak @click.away="showExportErrorModal = false"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-sm bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button @click="showExportErrorModal = false" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 rounded-lg bg-error/10 flex items-center justify-center">
|
|
<x-ri-alert-line class="size-5 text-error" />
|
|
</div>
|
|
<h3 class="text-base font-semibold text-gray-900 dark:text-white">Export Error</h3>
|
|
</div>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-5">No notes available to export. Add some notes first.</p>
|
|
<button @click="showExportErrorModal = false" class="w-full px-4 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-sm font-medium rounded-lg transition-colors">Close</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Windows Notice (conditional) -->
|
|
@if(stripos($server->os->name, 'Windows') !== false)
|
|
<section class="bg-white dark:bg-gray-900 border border-warning/30 rounded-2xl shadow-theme-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
style="transition: all 0.4s ease-out 0.3s;">
|
|
<div class="flex items-center gap-2 p-5 border-b border-warning/20 bg-warning/5">
|
|
<x-ri-windows-line class="size-5 text-warning" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Windows Configuration</h2>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Windows doesn't support cloud-init for automated configuration. You'll need to manually configure networking, hostname, and system settings.
|
|
</p>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-start gap-3 p-3 rounded-lg border border-info/20 bg-info/5">
|
|
<x-ri-settings-3-line class="size-4 text-info flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-900 dark:text-white">Network Setup</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Configure IP addresses manually in Windows Network Settings.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-3 p-3 rounded-lg border border-warning/20 bg-warning/5">
|
|
<x-ri-key-line class="size-4 text-warning flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-900 dark:text-white">Licensing</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">You must maintain valid Windows licensing.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-3 p-3 rounded-lg border border-error/20 bg-error/5">
|
|
<x-ri-shield-check-line class="size-4 text-error flex-shrink-0 mt-0.5" />
|
|
<div>
|
|
<p class="text-xs font-semibold text-gray-900 dark:text-white">Compliance</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Non-compliant installations may be suspended.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Infrastructure & Billing Section -->
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-theme-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'"
|
|
style="transition: all 0.4s ease-out 0.35s;">
|
|
<div class="flex items-center gap-2 p-5 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-information-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Server Information</h2>
|
|
</div>
|
|
|
|
<div class="p-5">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
|
<!-- Specifications -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<div class="w-8 h-8 rounded-lg bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
|
|
<x-ri-cpu-line class="size-4 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Specifications</h3>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">vCPU Cores</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ $allocatedCores }} cores</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Memory</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">
|
|
@if($allocatedMemoryMiB > 0)
|
|
{{ number_format($allocatedMemoryMiB / 1024, 1) }} GiB
|
|
@else
|
|
{{ \Illuminate\Support\Number::fileSize(bytes: $memoryTotal) }}
|
|
@endif
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Storage</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">
|
|
@if($allocatedDiskGiB > 0)
|
|
{{ $allocatedDiskGiB }} GiB
|
|
@elseif($diskTotal > 0)
|
|
{{ number_format($diskTotal / 1024 / 1024 / 1024, 0) }} GiB
|
|
@else
|
|
N/A
|
|
@endif
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Backups</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ $allocatedBackups > 0 ? $allocatedBackups . ' slots' : 'Disabled' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<div class="w-8 h-8 rounded-lg bg-green-100 dark:bg-green-900/50 flex items-center justify-center">
|
|
<x-ri-global-line class="size-4 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Network Transfer</h3>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Inbound</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ \Illuminate\Support\Number::fileSize(bytes: $totalNetIn) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Outbound</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ \Illuminate\Support\Number::fileSize(bytes: $totalNetOut) }}</span>
|
|
</div>
|
|
<div class="flex justify-between pt-2 border-t border-gray-200 dark:border-gray-700">
|
|
<span class="text-gray-500 dark:text-gray-400">Total</span>
|
|
<span class="font-semibold text-gray-900 dark:text-white">{{ \Illuminate\Support\Number::fileSize(bytes: $totalNetIn + $totalNetOut) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Billing -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<div class="w-8 h-8 rounded-lg bg-purple-100 dark:bg-purple-900/50 flex items-center justify-center">
|
|
<x-ri-calendar-line class="size-4 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Billing</h3>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Product</span>
|
|
<span class="font-medium text-gray-900 dark:text-white truncate max-w-[120px]">{{ $productName }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Status</span>
|
|
<span class="font-medium {{ $serviceStatus === 'active' ? 'text-success' : 'text-warning' }}">{{ ucfirst($serviceStatus) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Renewal</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ $serviceExpires ? $serviceExpires->format('M d, Y') : 'N/A' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Infrastructure -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<div class="w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center">
|
|
<x-ri-building-line class="size-4 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Infrastructure</h3>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Datacenter</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ $locationName }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Node</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ $nodeName }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">VM ID</span>
|
|
<span class="font-medium font-mono text-gray-900 dark:text-white">{{ $server->vm_id ?? 'N/A' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Identifiers -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<div class="w-8 h-8 rounded-lg bg-cyan-100 dark:bg-cyan-900/50 flex items-center justify-center">
|
|
<x-ri-fingerprint-line class="size-4 text-cyan-600 dark:text-cyan-400" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Identifiers</h3>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Server ID</span>
|
|
<span class="font-medium font-mono text-gray-900 dark:text-white">{{ $server->id }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Service ID</span>
|
|
<span class="font-medium font-mono text-gray-900 dark:text-white">{{ $server->service_id ?? 'N/A' }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-gray-500 dark:text-gray-400">Created</span>
|
|
<span class="font-medium text-gray-900 dark:text-white">{{ $server->created_at?->format('M d, Y') ?? 'N/A' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current Usage -->
|
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-xl p-4 border border-gray-100 dark:border-gray-700/50">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<div class="w-8 h-8 rounded-lg bg-rose-100 dark:bg-rose-900/50 flex items-center justify-center">
|
|
<x-ri-line-chart-line class="size-4 text-rose-600 dark:text-rose-400" />
|
|
</div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Live Usage</h3>
|
|
</div>
|
|
<div class="space-y-2.5 text-sm">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-500 dark:text-gray-400">CPU</span>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-16 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div class="h-full bg-blue-500 rounded-full" style="width: {{ min(100, $cpuUsagePercent) }}%"></div>
|
|
</div>
|
|
<span class="font-medium text-gray-900 dark:text-white text-xs w-10 text-right">{{ $cpuUsagePercent }}%</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-500 dark:text-gray-400">Memory</span>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-16 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div class="h-full bg-green-500 rounded-full" style="width: {{ min(100, $memoryPercent) }}%"></div>
|
|
</div>
|
|
<span class="font-medium text-gray-900 dark:text-white text-xs w-10 text-right">{{ $memoryPercent }}%</span>
|
|
</div>
|
|
</div>
|
|
@if($diskTotal > 0)
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-gray-500 dark:text-gray-400">Disk</span>
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-16 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div class="h-full bg-purple-500 rounded-full" style="width: {{ min(100, $diskPercent) }}%"></div>
|
|
</div>
|
|
<span class="font-medium text-gray-900 dark:text-white text-xs w-10 text-right">{{ $diskPercent }}%</span>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<<<<<<< HEAD
|
|
</section>
|
|
|
|
=======
|
|
</div>
|
|
</div>
|
|
>>>>>>> c5530e923149080e7d739823785456eb3267393f
|
|
</div>
|
|
|
|
@once
|
|
<script>
|
|
window.downloadText = function(textContent, filename) {
|
|
const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', filename);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
};
|
|
|
|
window.exportNotesToCSV = function() {
|
|
let notesText = '';
|
|
const livewireComponent = Livewire.find(document.querySelector('[wire\\:id]')?.getAttribute('wire:id'));
|
|
|
|
if (livewireComponent && livewireComponent.get('notes')) {
|
|
notesText = livewireComponent.get('notes') || '';
|
|
} else {
|
|
const notesTextarea = document.querySelector('[wire\\:model="notes"]');
|
|
if (notesTextarea && notesTextarea.tagName === 'TEXTAREA') {
|
|
notesText = notesTextarea.value || '';
|
|
} else {
|
|
const notesElement = document.querySelector('.prose');
|
|
if (notesElement) {
|
|
notesText = notesElement.innerText || notesElement.textContent || '';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!notesText || notesText.trim() === '' || notesText.includes('No notes')) {
|
|
const notesSection = document.querySelector('[x-data*="showExportErrorModal"]');
|
|
if (notesSection && notesSection._x_dataStack && notesSection._x_dataStack[0]) {
|
|
notesSection._x_dataStack[0].showExportErrorModal = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const filename = 'notes-' + new Date().toISOString().split('T')[0] + '.txt';
|
|
window.downloadText(notesText, filename);
|
|
};
|
|
</script>
|
|
@endonce
|
|
@endif
|
|
|
|
</x-proxmox::layout>
|