Files
Virtnet.bond/themes/lucentui/views/vendor/pterodactyl2/console.blade.php
Joseph.Rawlings 6440a24390 feat: add dark/light mode toggle to navigation, enhance sidebar styles, and update layout colors
- Implemented a button for toggling between dark and light modes in the navigation component.
- Adjusted sidebar width and improved active link indicators with shadows for better visibility.
- Updated layout colors for a more vibrant and cohesive theme, focusing on a "Pure Neutral Black + Neon" aesthetic.
- Refined ambient gradient accents for improved content readability across devices.
- Enhanced console layout for better responsiveness and user experience.
2026-04-22 01:43:38 -04:00

2227 lines
105 KiB
PHP
Raw Blame History

{{-- import xterm uising cdn --}}
@assets
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" />
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-canvas@0.7.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0"></script>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css" rel="stylesheet">
<style>
@keyframes status-pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
50% {
opacity: 0.9;
transform: scale(1.02);
box-shadow: 0 0 2px 0.5px rgba(255, 255, 255, 0.15);
}
}
.status-pulse {
animation: status-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.status-pulse-green {
animation: status-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.status-pulse-yellow {
animation: status-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.status-pulse-orange {
animation: status-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.status-pulse-red {
animation: status-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.status-pulse-blue {
animation: status-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.status-pulse-purple {
animation: status-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
</style>
@endassets
<section
class="fade-appear-done fade-enter-done container mx-auto px-4 py-8 sm:px-6 lg:px-8"
id="console-section">
{{-- Page Header --}}
<div class="mb-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-4">
<div>
<h1 class="text-3xl font-bold text-base">{{ $server['name'] }}</h1>
<p class="text-sm text-base/50 mt-1">Console</p>
</div>
<div id="status-indicator"
class="flex items-center gap-2 px-3 py-1.5 bg-gray-100 dark:bg-gray-800 rounded-full">
<div id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 status-pulse"></div>
<span id="status-text"
class="text-sm font-medium text-gray-700 dark:text-base/40">Loading...</span>
</div>
</div>
<div class="flex items-center gap-3">
{{-- Server Action Buttons --}}
<div class="flex items-center gap-1.5 p-1 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-200 dark:border-neutral/50"
id="server-actions">
<button id="btn-start" data-action="start" disabled
class="group relative p-2.5 rounded-xl bg-white dark:bg-background-secondary/50 border border-gray-200 dark:border-gray-600
text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20
hover:border-green-300 dark:hover:border-green-500 transition-all duration-200
disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white
disabled:hover:border-gray-200 dark:disabled:hover:bg-background-secondary/50
dark:disabled:hover:border-gray-600 shadow-sm hover:shadow-md
w-10 h-10 flex items-center justify-center"
title="Start Server">
<i class="ri-play-fill text-lg"></i>
<div
class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-gray-900 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
Start
</div>
</button>
<button id="btn-stop" data-action="stop" disabled
class="group relative p-2.5 rounded-xl bg-white dark:bg-background-secondary/50 border border-gray-200 dark:border-gray-600
text-yellow-600 dark:text-yellow-400 hover:bg-yellow-50 dark:hover:bg-yellow-900/20
hover:border-yellow-300 dark:hover:border-yellow-500 transition-all duration-200
disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white
disabled:hover:border-gray-200 dark:disabled:hover:bg-background-secondary/50
dark:disabled:hover:border-gray-600 shadow-sm hover:shadow-md
w-10 h-10 flex items-center justify-center"
title="Stop Server">
<i class="ri-pause-fill text-lg"></i>
<div
class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-gray-900 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
Stop
</div>
</button>
<button id="btn-restart" data-action="restart" disabled
class="group relative p-2.5 rounded-xl bg-white dark:bg-background-secondary/50 border border-gray-200 dark:border-gray-600
text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20
hover:border-blue-300 dark:hover:border-blue-500 transition-all duration-200
disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white
disabled:hover:border-gray-200 dark:disabled:hover:bg-background-secondary/50
dark:disabled:hover:border-gray-600 shadow-sm hover:shadow-md
w-10 h-10 flex items-center justify-center"
title="Restart Server">
<i class="ri-restart-fill text-lg"></i>
<div
class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-gray-900 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
Restart
</div>
</button>
<button id="btn-kill" data-action="kill" disabled
class="group relative p-2.5 rounded-xl bg-white dark:bg-background-secondary/50 border border-gray-200 dark:border-gray-600
text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20
hover:border-red-300 dark:hover:border-red-500 transition-all duration-200
disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-white
disabled:hover:border-gray-200 dark:disabled:hover:bg-background-secondary/50
dark:disabled:hover:border-gray-600 shadow-sm hover:shadow-md
w-10 h-10 flex items-center justify-center"
title="Force Stop Server">
<i class="ri-stop-fill text-lg"></i>
<div
class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-gray-900 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none">
Kill
</div>
</button>
</div>
</div>
</div>
</div>
{{-- Status Banner --}}
<div id="status-banner" class="hidden mb-4 p-4 rounded-xl border-l-4 flex items-center gap-3">
<div id="status-icon" class="flex-shrink-0">
</div>
<div class="flex-1">
<h4 id="status-title" class="font-semibold text-sm"></h4>
<p id="status-message" class="text-sm"></p>
</div>
</div>
<div class="grid grid-cols-9 gap-4 my-4">
<div class="grid grid-cols-9 xl:col-span-7 lg:col-span-9 col-span-9 gap-1 rounded-xl p-3 bg-gray-900/50 border border-neutral/50 shadow-lg">
{{-- status banner --}}
{{-- status indicator --}}
{{-- terminal --}}
<div id="terminal" class="w-full h-[790] col-span-9 mb-0">
</div>
<input type="text" id="console-input" name="ConsoleInput" placeholder="Enter command here..."
class="col-span-9 p-2 bg-background-secondary/50 rounded-md text-base opacity-85" />
</div>
<div class="col-span-2 xl:col-span-2 col-span-9 grid grid-cols-6 gap-2">
{{-- server information cards --}}
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full ml-4 h-full">
<p class="text-lg text-base">Address</p>
<p class="text-sm font-semibold" style="font-size: 115.625%;" id="server-adress"> loading....</p>
</div>
</div>
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full ml-4 h-full">
<p class="text-lg text-base">Uptime</p>
<p class="text-sm font-semibold" style="font-size: 115.625%;" id="server-uptime"> loading...</p>
</div>
</div>
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full ml-4 h-full">
<p class="text-lg text-base">CPU Load</p>
<div class="h-[1.75rem] w-full font-semibold text-base truncate" style="font-size: 115.625%;"
id="server-cpu">
~ <span class="ml-1 text-base/40 text-[70%] select-none">/ 100%</span></div>
</div>
</div>
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full px-4 h-full">
<p class="text-lg text-base">Memory</p>
<div class="h-[1.75rem] w-full font-semibold text-base truncate" style="font-size: 115.625%;"
id="server-ram">~<span class="ml-1 text-base/40 text-[70%] select-none">/ ~</span></div>
<div class="mt-2 w-full bg-gray-200 rounded-full h-2 dark:bg-background-secondary/50">
<div id="server-ram-bar" class="bg-yellow-500 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full px-4 h-full">
<p class="text-lg text-base">Disk</p>
<div class="h-[1.75rem] w-full font-semibold text-base truncate" style="font-size: 115.625%;"
id="server-disk">~<span class="ml-1 text-base/40 text-[70%] select-none">/ ~</span></div>
<div class="mt-2 w-full bg-gray-200 rounded-full h-2 dark:bg-background-secondary/50">
<div id="server-disk-bar" class="bg-green-500 h-2 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full ml-4 h-full">
<p class="text-lg text-base">Network (Inbound)</p>
<div class="h-[1.75rem] w-full font-semibold text-base truncate" style="font-size: 115.625%;"
id="server-inbound">~</div>
<div id="server-inbound-pps" class="text-sm text-base/75 select-none">~ pps</div>
</div>
</div>
<div
class="bg-background-secondary/50 border border-neutral/50 p-2 rounded-xl h-28 max-h-28 max-w-64 xl:col-span-6 lg:col-span-2 sm:col-span-3 col-span-3">
<div class="flex flex-col justify-center overflow-hidden w-full ml-4 h-full">
<p class="text-lg text-base">Network (Outbound)</p>
<div class="h-[1.75rem] w-full font-semibold text-base truncate" style="font-size: 115.625%;"
id="server-outbound">~</div>
<div id="server-outbound-pps" class="text-sm text-base/75 select-none">~ pps</div>
</div>
</div>
</div>
{{-- here you can add the grids for the server information like CPU, RAM, Disk Usage, Server Address. on the left side of thwe terminal --}}
{{-- CPU RAM network charts with tailwind dark theme --}}
<div class="group bg-background-secondary/50 border border-neutral/50 p-4 rounded-xl xl:col-span-3 lg:col-span-9 col-span-9">
<div class="flex items-center justify-between">
<h3 class=" group-hover:text-gray-50">CPU Load</h3>
</div>
<div id="cpu-chart" class="w-full h-64"></div>
</div>
<div class="group bg-background-secondary/50 border border-neutral/50 p-4 rounded-xl xl:col-span-3 lg:col-span-9 col-span-9">
<div class="flex items-center justify-between">
<h3 class=" group-hover:text-gray-50">RAM Usage</h3>
</div>
<div id="ram-chart" class="w-full h-64"></div>
</div>
<div class="group bg-background-secondary/50 border border-neutral/50 p-4 rounded-xl xl:col-span-3 lg:col-span-9 col-span-9">
<div class="flex items-center justify-between">
<h3 class="group-hover:text-gray-50">Network Load</h3>
</div>
<div id="network-chart" class="w-full h-64"></div>
</div>
</div>
</section>
{{-- Toast container --}}
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50 pointer-events-none"></div>
@script
<script type="module">
/**
* Console Management Script
*
* Fixes for preventing double console loading:
* 1. Added mounting flag to prevent concurrent mounting operations
* 2. Added proper error handling and cleanup in try/catch blocks
* 3. Added global interval cleanup to prevent multiple watchers
* 4. Added debouncing to prevent rapid mount/unmount cycles
* 5. Added page unload cleanup to prevent memory leaks
* 6. Added logging for debugging mount/unmount operations
*/
(async function() {
// --- FETCH STATIC SERVER INFO (address + limits) ---
async function fetchServerInfo() {
const response = await fetch("/services/{{ $service->id }}/details");
if (!response.ok) throw new Error('Network response was not ok ' + response.statusText);
const data = await response.json();
return data.attributes; // { address, limits:{ cpu, memory, disk }, … }
}
const serverInfo = await fetchServerInfo();
// check if cpu limits is 0 if 0 its infinite set to infite symbol
if (serverInfo.limits.cpu === 0) {
serverInfo.limits.cpu = '∞';
}
// Update status banner based on server state
updateStatusBanner(serverInfo);
// Update status indicator
updateStatusIndicator(serverInfo);
// populate the address card immediately
document.getElementById('server-adress').innerText = serverInfo.address;
// pull limits out for later
const {
cpu: MAX_CPU,
memory: MAX_RAM_MB,
disk: MAX_DISK_MB
} = serverInfo.limits;
// --- STATUS BANNER ---
function updateStatusBanner(serverInfo) {
const banner = document.getElementById('status-banner');
const icon = document.getElementById('status-icon');
const title = document.getElementById('status-title');
const message = document.getElementById('status-message');
// Check for various server states (in order of priority)
if (serverInfo.is_node_under_maintenance) {
showBanner('maintenance', 'Node Under Maintenance',
'The node hosting this server is currently under maintenance. Server functionality may be limited.'
);
} else if (serverInfo.is_suspended) {
showBanner('suspended', 'Server Suspended',
'This server has been suspended. Please check your invoices for unpaid bills or contact support if this is a mistake.'
);
} else if (serverInfo.is_installing) {
showBanner('installing', 'Installing Server',
'This server is currently being installed. Please wait for the installation to complete.'
);
} else if (serverInfo.is_transferring) {
showBanner('transferring', 'Server Transfer in Progress',
'This server is being transferred to another node. Some functionality may be limited during this process.'
);
} else {
// Hide banner if no special status
banner.classList.add('hidden');
return;
}
function showBanner(type, titleText, messageText) {
banner.classList.remove('hidden');
title.textContent = titleText;
message.textContent = messageText;
// Reset classes
banner.className = 'mb-4 p-4 rounded-xl border-l-4 flex items-center gap-3';
// Set colors and icons based on type
switch (type) {
case 'maintenance':
banner.classList.add('bg-orange-50', 'dark:bg-orange-900/20', 'border-orange-500',
'text-orange-900', 'dark:text-orange-200');
icon.innerHTML = `
<svg class="w-6 h-6 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
`;
break;
case 'suspended':
banner.classList.add('bg-red-50', 'dark:bg-red-900/20', 'border-red-500',
'text-red-900', 'dark:text-red-200');
icon.innerHTML = `
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L5.636 5.636"></path>
</svg>
`;
break;
case 'installing':
banner.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'border-blue-500',
'text-blue-900', 'dark:text-blue-200');
icon.innerHTML = `
<svg class="w-6 h-6 text-blue-500 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
`;
break;
case 'transferring':
banner.classList.add('bg-purple-50', 'dark:bg-purple-900/20', 'border-purple-500',
'text-purple-900', 'dark:text-purple-200');
icon.innerHTML = `
<svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path>
</svg>
`;
break;
}
}
}
// --- STATUS INDICATOR ---
function updateStatusIndicator(serverInfo) {
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
if (!dot || !text) return;
// Remove all existing classes except base ones
dot.className = 'w-2.5 h-2.5 rounded-full';
// Determine status and styling
if (serverInfo.is_node_under_maintenance) {
dot.classList.add('bg-orange-500', 'status-pulse-orange');
text.textContent = 'Under Maintenance';
text.className = 'text-sm font-medium text-orange-600 dark:text-orange-400';
} else if (serverInfo.is_suspended) {
dot.classList.add('bg-red-500', 'status-pulse-red');
text.textContent = 'Suspended';
text.className = 'text-sm font-medium text-red-600 dark:text-red-400';
} else if (serverInfo.is_installing) {
dot.classList.add('bg-blue-500', 'status-pulse-blue');
text.textContent = 'Installing';
text.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else if (serverInfo.is_transferring) {
dot.classList.add('bg-purple-500', 'status-pulse-purple');
text.textContent = 'Transferring';
text.className = 'text-sm font-medium text-purple-600 dark:text-purple-400';
} else {
// Normal operation - status will be updated by websocket
dot.classList.add('bg-gray-500', 'status-pulse');
text.textContent = 'Connecting...';
text.className = 'text-sm font-medium text-gray-600 dark:text-base/50';
}
}
// Function to update status indicator from websocket data
function updateStatusFromWebsocket(status) {
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
if (!dot || !text) return;
// Remove all animation classes but keep base classes
dot.classList.remove('animate-pulse', 'animate-ping', 'animate-bounce', 'status-pulse',
'status-pulse-strong',
'status-pulse-green', 'status-pulse-yellow', 'status-pulse-orange', 'status-pulse-red',
'status-pulse-blue', 'status-pulse-purple');
dot.className = 'w-2.5 h-2.5 rounded-full';
switch (status) {
case 'running':
dot.classList.add('bg-green-500', 'status-pulse-green');
text.textContent = 'Online';
text.className = 'text-sm font-medium text-green-600 dark:text-green-400';
break;
case 'offline':
dot.classList.add('bg-gray-500');
text.textContent = 'Offline';
text.className = 'text-sm font-medium text-gray-600 dark:text-base/50';
break;
case 'starting':
dot.classList.add('bg-yellow-500', 'status-pulse-yellow');
text.textContent = 'Starting';
text.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
break;
case 'stopping':
dot.classList.add('bg-orange-500', 'status-pulse-orange');
text.textContent = 'Stopping';
text.className = 'text-sm font-medium text-orange-600 dark:text-orange-400';
break;
default:
dot.classList.add('bg-gray-500', 'status-pulse');
text.textContent = 'Unknown';
text.className = 'text-sm font-medium text-gray-600 dark:text-base/50';
break;
}
}
// --- UTILS ---
function getLastXSecondsLabels(n) {
return Array.from({
length: n
}, (_, i) => {
const d = new Date(Date.now() - i * 1000);
return d.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}).reverse();
}
function formatBytes(bytes, dec = 2) {
// check if bytes is a number
if (typeof bytes !== 'number' || isNaN(bytes)) return 'N/A';
if (bytes === 0) return '0 Bytes';
const k = 1024,
dm = Math.max(0, dec);
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function formatUptime(s) {
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = Math.floor(s % 60);
return [
d ? `${d}d` : '',
h ? `${h}h` : '',
m ? `${m}m` : '',
(sec || (!d && !h && !m)) ? `${sec}s` : ''
].filter(Boolean).join(' ');
}
// --- HISTORY ---
class History {
constructor(input, key = 'consoleCommandHistory') {
this.el = input;
this.key = key;
this.list = JSON.parse(localStorage.getItem(key) || '[]');
this.idx = -1;
this._bind();
}
_bind() {
this.el.addEventListener('keydown', e => {
if (e.key === 'ArrowUp') this._up(e);
else if (e.key === 'ArrowDown') this._down(e);
});
}
commit(cmd) {
if (!cmd) return;
this.list.push(cmd);
// keep only the last 100 commands
if (this.list.length > 100) {
this.list.shift();
}
localStorage.setItem(this.key, JSON.stringify(this.list));
this.idx = -1;
}
_up(e) {
if (!this.list.length) return;
this.idx = this.idx <= 0 ?
this.list.length - 1 :
this.idx - 1;
this.el.value = this.list[this.idx];
e.preventDefault();
}
_down(e) {
if (!this.list.length) return;
this.idx = this.idx < 0 || this.idx >= this.list.length - 1 ?
-1 :
this.idx + 1;
this.el.value = this.idx < 0 ?
'' :
this.list[this.idx];
e.preventDefault();
}
}
// --- TERMINAL MANAGEMENT ---
const Terminal = window.Terminal;
const FitAddon = window.FitAddon.FitAddon;
const CanvasAddon = window.CanvasAddon.CanvasAddon;
class TerminalManager {
constructor(containerId) {
const container = document.getElementById(containerId);
if (!container) {
throw new Error(`Terminal container '${containerId}' not found`);
}
// Mark container as initialized
container.setAttribute('data-console-initialized', 'true');
this.term = new Terminal({
convertEol: true,
cursorBlink: false,
theme: {
// make it transparent
background: 'rgba(255, 255, 255, 0)',
},
allowTransparency: true
});
this.fit = new FitAddon();
this.term.loadAddon(this.fit);
this.term.loadAddon(new CanvasAddon());
this.term.open(container);
window.addEventListener('resize', () => this.fit.fit());
setTimeout(() => this.fit.fit(), 100);
}
write(msg, kind = 'default') {
const prefix = {
error: '\x1b[31m[ERROR]\x1b[39m ',
info: '\x1b[33m[INFO]\x1b[39m '
} [kind] || '';
this.term.writeln(prefix + msg);
}
}
// --- CHARTS (ApexCharts) ---
class ChartManager {
constructor() {
this.cpu = null;
this.ram = null;
this.net = null;
// common chart options
this._initCharts();
}
_initCharts() {
// common chart options
const MAX_POINTS = 60; // max points to show in the chart
const common = {
chart: {
type: 'area',
height: '98%',
width: '100%',
toolbar: {
show: false
},
animations: {
enabled: true,
easing: 'easeout'
},
background: 'transparent'
},
theme: {
mode: 'dark'
},
stroke: {
curve: 'smooth',
width: 2
},
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
gradientToColors: ['transparent'],
opacityFrom: 0.9,
opacityTo: 0.1,
shadeIntensity: 1
}
},
// disable all grid lines
grid: {
show: false,
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: false
}
}
},
xaxis: {
type: 'datetime',
labels: {
show: false
},
axisTicks: {
show: false
},
axisBorder: {
show: false
}
},
yaxis: {
labels: {
style: {
colors: ['#888']
},
formatter: v => v.toFixed(0)
},
axisTicks: {
show: false
},
axisBorder: {
show: false
}
},
tooltip: {
theme: 'dark'
},
dataLabels: {
enabled: false
},
markers: {
size: 0
},
legend: {
show: false
},
noData: {
text: ''
}
};
this.cpu = new ApexCharts(
document.getElementById('cpu-chart'), {
...common,
series: [{
name: 'CPU',
data: []
}],
colors: ['#10B981']
}
);
this.ram = new ApexCharts(
document.getElementById('ram-chart'), {
...common,
series: [{
name: 'RAM',
data: []
}],
colors: ['#06B6D4']
}
);
this.net = new ApexCharts(
document.getElementById('network-chart'), {
...common,
series: [{
name: 'In',
data: []
},
{
name: 'Out',
data: []
}
],
colors: ['#F59E0B', '#EF4444']
}
);
this.cpu.render();
this.ram.render();
this.net.render();
}
push(chartKey, val) {
const chart = this[chartKey];
const ts = Date.now();
// clone current series arrays
const newSeries = chart.w.config.series.map(s => ({
name: s.name,
data: [...s.data]
}));
// push the new point(s)
if (chartKey === 'net') {
newSeries[0].data.push({
x: ts,
y: val.rx
});
newSeries[1].data.push({
x: ts,
y: val.tx
});
} else {
newSeries[0].data.push({
x: ts,
y: val
});
}
// trim to last 60 points
newSeries.forEach(s => {
if (s.data.length > 45) s.data = s.data.slice(-45);
});
// update the chart
chart.updateSeries(newSeries, /* animate = */ false);
}
}
// --- TOAST FUNCTION ---
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `px-4 py-3 rounded-xl text-white pointer-events-auto transition-all duration-300 transform translate-x-full ${
type === 'success' ? 'bg-green-600' :
type === 'error' ? 'bg-red-600' :
type === 'warning' ? 'bg-yellow-600' :
type === 'info' ? 'bg-blue-600' : 'bg-gray-600'
}`;
toast.textContent = message;
const container = document.getElementById('toast-container');
container?.appendChild(toast);
// Slide in
setTimeout(() => toast.classList.remove('translate-x-full'), 100);
// Remove after 5 seconds
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => container?.removeChild(toast), 300);
}, 5000);
}
// --- WEBSOCKET CLIENT ---
class WSClient {
constructor(term, charts) {
this.term = term;
this.charts = charts;
this.socket = null;
this.total = {
rx: 0,
tx: 0
};
this.netBase = null; // <-- use this to calc throughput
this.serviceId = null; // will be set when connecting
}
async getToken(serviceId) {
const res = await fetch(`/services/${serviceId}/websocket`);
if (!res.ok) throw new Error(res.statusText);
const {
data
} = await res.json();
return data;
}
async connect(serviceId) {
this.serviceId = serviceId; // store serviceId for later use
const res = await this.getToken(serviceId);
if (!res) throw new Error('Failed to get token');
// Store reference globally for cleanup detection
this.socket = new WebSocket(res.socket);
window.activeConsoleWebSocket = this.socket;
this.socket.addEventListener('open', () => {
this.socket.send(JSON.stringify({
event: 'auth',
args: [res.token]
}));
});
this.socket.addEventListener('message', e => this._onMsg(JSON.parse(e.data)));
this.socket.addEventListener('close', () => {
this.term.write('Disconnected', 'warning');
// Clear global reference when closed
if (window.activeConsoleWebSocket === this.socket) {
window.activeConsoleWebSocket = null;
}
});
this.socket.addEventListener('error', () => this.term.write('WS error', 'error'));
}
_onMsg({
event,
args
}) {
switch (event) {
case 'stats':
const s = JSON.parse(args[0]);
// Only update if console is still mounted
if (mounted) {
this._updateStatusCards(s);
this._handleStats(s);
}
break;
case 'auth success':
this.socket.send(JSON.stringify({
event: 'send logs',
args: [null]
}));
break;
case 'console output':
args.forEach(line => this.term.write(line));
break;
case 'status':
updateButtons(args[0]);
this.term.write(`Server is now ${args[0]}`, 'info');
break;
case 'install output':
this.term.write(args[0]);
break;
case 'token expiring':
this.term.write('Session token is expiring generating a new one...', 'info');
this.getToken(this.serviceId)
.then(res => {
this.socket.send(JSON.stringify({
event: 'auth',
args: [res.token]
}));
})
.catch(err => this.term.write(`Error getting new token: ${err.message}`,
'error'));
break;
case 'daemon message':
// Show daemon messages as warning toasts
if (args[0]) {
showToast(args[0], 'warning');
this.term.write(`[Daemon] ${args[0]}`, 'info');
}
break;
case 'install started':
// Show install start as info toast
showToast('Server installation has started...', 'info');
this.term.write('Installation started...', 'info');
break;
case 'install completed':
// Show install completion as success toast
showToast('Server installation completed successfully!', 'success');
this.term.write('Installation completed successfully!', 'success');
break;
default:
this.term.write(`Unknown event: ${event}`, 'error');
console.warn(`Unknown event: ${event}`, args);
break;
}
}
_updateStatusCards(s) {
// Prevent updates if elements don't exist (during unmount)
if (!document.getElementById('server-adress')) {
console.log('Status cards not found, skipping update');
return;
}
// Address (note your blade uses “server-adress”) attributes.relationships.allocations.data[0].attributes.ip and attributes.relationships.allocations.data[0].attributes.port we need to find the default attributes.relationships.allocations.data[0].attributes.is_default
const allocation = serverInfo.relationships.allocations.data.find(a => a.attributes
.is_default);
// Address
// Note: allocation.attributes.ip and allocation.attributes.port are used to display the server address
// If no allocation is found, we display 'N/A'
const address = allocation ?
`${allocation.attributes.ip_alias ?? allocation.attributes.ip }:${allocation.attributes.port}` :
'N/A';
const addressEl = document.getElementById('server-adress');
if (addressEl) addressEl.innerText = address || 'N/A';
// Uptime
const uptimeEl = document.getElementById('server-uptime');
if (uptimeEl) uptimeEl.innerText = formatUptime(s.uptime / 1000 || 0);
// CPU (used / max)
const cpuEl = document.getElementById('server-cpu');
if (cpuEl) cpuEl.innerText = MAX_CPU === '∞' ?
`${s.cpu_absolute.toFixed(1)} / ∞` :
`${s.cpu_absolute.toFixed(1)} / ${MAX_CPU}%`;
// RAM: used / max using formatBytes
const maxRamBytes = MAX_RAM_MB * 1024 * 1024;
const ramEl = document.getElementById('server-ram');
if (ramEl) ramEl.innerText =
`${formatBytes(s.memory_bytes)} / ${formatBytes(maxRamBytes)}`;
// update RAM bar width
const ramPercent = Math.round((s.memory_bytes / maxRamBytes) * 100);
const ramBarEl = document.getElementById('server-ram-bar');
if (ramBarEl) ramBarEl.style.width = `${ramPercent}%`;
// Disk: used / max using formatBytes
if (s.disk_bytes != null) {
const maxDiskBytes = MAX_DISK_MB * 1024 * 1024;
const diskEl = document.getElementById('server-disk');
if (diskEl) diskEl.innerText =
`${formatBytes(s.disk_bytes)} / ${formatBytes(maxDiskBytes)}`;
// update Disk bar width
const diskPercent = Math.round((s.disk_bytes / maxDiskBytes) * 100);
const diskBarEl = document.getElementById('server-disk-bar');
if (diskBarEl) diskBarEl.style.width = `${diskPercent}%`;
}
// Network in/out unchanged …
const inboundEl = document.getElementById('server-inbound');
if (inboundEl) inboundEl.innerText = formatBytes(s.network.rx_bytes);
const outboundEl = document.getElementById('server-outbound');
if (outboundEl) outboundEl.innerText = formatBytes(s.network.tx_bytes);
// rolling bytes-per-second
const now = Date.now();
if (!this.netBase) {
this.netBase = {
rx: s.network.rx_bytes,
tx: s.network.tx_bytes,
time: now
};
} else {
const secs = (now - this.netBase.time) / 1000;
const rxBps = (s.network.rx_bytes - this.netBase.rx) / secs;
const txBps = (s.network.tx_bytes - this.netBase.tx) / secs;
this.netBase = {
rx: s.network.rx_bytes,
tx: s.network.tx_bytes,
time: now
};
document.getElementById('server-inbound-pps').innerText =
formatBytes(rxBps) + '/s';
document.getElementById('server-outbound-pps').innerText =
formatBytes(txBps) + '/s';
}
}
_handleStats(s) {
const t = new Date().toLocaleTimeString();
this.charts.push('cpu', s.cpu_absolute, t);
this.charts.push('ram', s.memory_bytes / 1024 / 1024, t);
const rx = Math.max(0, s.network.rx_bytes / 1024 - this.total.rx);
const tx = Math.max(0, s.network.tx_bytes / 1024 - this.total.tx);
this.total = {
rx: s.network.rx_bytes / 1024,
tx: s.network.tx_bytes / 1024
};
this.charts.push('net', {
rx,
tx
}, t);
}
sendCommand(cmd) {
this.socket.send(JSON.stringify({
event: 'send command',
args: [cmd]
}));
}
setState(state) {
this.socket.send(JSON.stringify({
event: 'set state',
args: [state]
}));
}
}
// button refs will be (re)selected on each mount
let btnStart, btnStop, btnRestart, btnKill;
// define server action handlers
let onSuspendedCommand; // Declare in outer scope for cleanup
let tetrisGame = null; // Declare tetrisGame in outer scope
let history, term, charts, ws, mounted = false;
let mounting = false; // Flag to prevent concurrent mounting
let watchInterval = null; // Store interval reference for cleanup
let lastTerminalCheck = 0; // Debounce rapid checks
const inputEl = () => document.getElementById('console-input');
// Function to check if elements are already initialized
function elementsAlreadyInitialized() {
const terminalEl = document.getElementById('terminal');
const consoleInputEl = inputEl();
// Check for signs that elements are already initialized
const hasTerminalData = terminalEl && (
terminalEl.hasAttribute('data-console-initialized') ||
terminalEl.querySelector('.xterm') !== null ||
terminalEl.querySelector('.xterm-viewport') !== null ||
terminalEl.querySelector('.xterm-screen') !== null ||
terminalEl.querySelector('canvas') !== null ||
terminalEl.children.length > 0 ||
terminalEl.innerHTML.trim() !== ''
);
const hasInputListeners = consoleInputEl && (
consoleInputEl.hasAttribute('data-console-listeners') ||
// Check if input has any non-default event listeners
(consoleInputEl._consoleEventListeners && consoleInputEl._consoleEventListeners.length > 0)
);
const hasActiveWebSocket = window.activeConsoleWebSocket &&
window.activeConsoleWebSocket.readyState === WebSocket.OPEN;
// Check for existing ApexCharts instances
const hasActiveCharts = ['cpu-chart', 'ram-chart', 'network-chart'].some(chartId => {
const chartEl = document.getElementById(chartId);
return chartEl && (
chartEl.querySelector('.apexcharts-canvas') !== null ||
chartEl.children.length > 0 ||
chartEl.innerHTML.trim() !== ''
);
});
// Check for any global console state
const hasGlobalState = window.consoleWatchInterval !== undefined ||
window.suspendedInterval !== undefined ||
window.activeConsoleWebSocket !== undefined;
return hasTerminalData || hasInputListeners || hasActiveWebSocket || hasActiveCharts ||
hasGlobalState;
}
// Function to cleanup existing elements before reinitializing
function cleanupExistingElements(force = false) {
console.log('Cleaning up existing elements...', force ? '(forced)' : '');
// Clean up terminal element - be more aggressive
const terminalEl = document.getElementById('terminal');
if (terminalEl) {
// Clear any existing xterm instances (multiple selectors for thorough cleanup)
const xtermSelectors = ['.xterm', '.xterm-viewport', '.xterm-screen', '.xterm-helper-textarea',
'.xterm-rows', '.xterm-cursor-layer', '.xterm-selection-layer',
'canvas', '.xterm-decoration-container'
];
xtermSelectors.forEach(selector => {
const elements = terminalEl.querySelectorAll(selector);
elements.forEach(el => {
try {
el.remove();
} catch (e) {
console.warn('Error removing element:', e);
}
});
});
// Force clear all children and reset
try {
terminalEl.innerHTML = '';
terminalEl.removeAttribute('data-console-initialized');
// Clear any inline styles that might interfere
terminalEl.style.cssText = '';
} catch (e) {
console.warn('Error clearing terminal element:', e);
}
}
// Clean up input element listeners - more thorough approach
const consoleInputEl = inputEl();
if (consoleInputEl) {
try {
// Mark existing listeners for tracking
if (consoleInputEl._consoleEventListeners) {
consoleInputEl._consoleEventListeners.forEach(({
type,
listener
}) => {
consoleInputEl.removeEventListener(type, listener);
});
}
// Clone element to remove ALL event listeners
const newInput = consoleInputEl.cloneNode(true);
if (consoleInputEl.parentNode) {
consoleInputEl.parentNode.replaceChild(newInput, consoleInputEl);
}
newInput.removeAttribute('data-console-listeners');
newInput._consoleEventListeners = [];
} catch (e) {
console.warn('Error cleaning input element:', e);
}
}
// Clean up any existing WebSocket connections
if (window.activeConsoleWebSocket) {
try {
if (window.activeConsoleWebSocket.readyState === WebSocket.OPEN ||
window.activeConsoleWebSocket.readyState === WebSocket.CONNECTING) {
window.activeConsoleWebSocket.close();
}
} catch (e) {
console.warn('Error closing WebSocket:', e);
}
window.activeConsoleWebSocket = null;
}
// Clean up chart containers - more thorough
['cpu-chart', 'ram-chart', 'network-chart'].forEach(chartId => {
const chartEl = document.getElementById(chartId);
if (chartEl) {
try {
// Remove ApexCharts specific elements
const apexSelectors = ['.apexcharts-canvas', '.apexcharts-svg',
'.apexcharts-inner',
'.apexcharts-graphical', '.apexcharts-tooltip'
];
apexSelectors.forEach(selector => {
const elements = chartEl.querySelectorAll(selector);
elements.forEach(el => {
try {
el.remove();
} catch (removeError) {
console.warn(`Error removing chart element:`, removeError);
}
});
});
chartEl.innerHTML = '';
} catch (e) {
console.warn(`Error cleaning chart ${chartId}:`, e);
}
}
});
// Reset button states - clone to remove all listeners
const buttons = ['start', 'stop', 'restart', 'kill'];
buttons.forEach(action => {
try {
const btn = document.querySelector(`[data-action="${action}"]`);
if (btn) {
// Clone button to remove event listeners
const newBtn = btn.cloneNode(true);
if (btn.parentNode) {
btn.parentNode.replaceChild(newBtn, btn);
}
}
} catch (e) {
console.warn(`Error cleaning button ${action}:`, e);
}
});
// Clean up any global intervals
if (window.suspendedInterval) {
clearInterval(window.suspendedInterval);
window.suspendedInterval = null;
}
if (window.consoleWatchInterval) {
clearInterval(window.consoleWatchInterval);
window.consoleWatchInterval = null;
}
// Force garbage collection if available (development only)
if (force && window.gc && typeof window.gc === 'function') {
try {
window.gc();
} catch (e) {
// Ignore - gc() not available in production
}
}
}
function updateButtons(status) {
btnStart = btnStart || document.querySelector('[data-action="start"]');
btnStop = btnStop || document.querySelector('[data-action="stop"]');
btnRestart = btnRestart || document.querySelector('[data-action="restart"]');
btnKill = btnKill || document.querySelector('[data-action="kill"]');
// offline → allow start, disallow stop/restart/kill
const isRunning = status === 'running';
const isOffline = status === 'offline';
btnStart.disabled = !isOffline;
btnStop.disabled = !isRunning;
btnRestart.disabled = !isRunning;
btnKill.disabled = !isRunning;
// Update status indicator
updateStatusFromWebsocket(status);
}
// --- SUSPENDED SERVER SIMULATION ---
async function mountSuspendedConsole() {
history = new History(inputEl());
term = new TerminalManager('terminal');
charts = new ChartManager();
// Disable all buttons for suspended server
btnStart = document.querySelector('[data-action="start"]');
btnStop = document.querySelector('[data-action="stop"]');
btnRestart = document.querySelector('[data-action="restart"]');
btnKill = document.querySelector('[data-action="kill"]');
[btnStart, btnStop, btnRestart, btnKill].forEach(btn => {
if (btn) {
btn.disabled = true;
btn.title = 'Server is suspended';
}
});
// Suspended console messages with easter eggs
const suspendedMessages = [
'🔒 Server suspended - Check your invoices or contact support if this is a mistake',
'💤 Server is taking a nap... Check for unpaid invoices or contact support!',
'🚫 Access denied - Verify payment status or contact support if error',
'<27> Server suspended: Check your billing or contact support for assistance',
'📋 Review your invoices for unpaid bills or contact support if needed',
'🎭 Plot twist: Your server is suspended! Check invoices or contact support',
'🕷️ With great power comes great responsibility... and invoice payments',
'🎮 Game over! Check your billing or contact support to continue (or type "tetris" for fun)',
'🍕 Your server is as suspended as delivery with unpaid bills',
'🎸 Server suspended like a guitar string... Check invoices or contact support!',
'🚀 Houston, we have a problem... Check your billing dashboard',
'🧙‍♂️ Even magic can\'t unsuspend without payment... or support assistance!',
'🦄 Unicorns are real, but unpaid invoices suspend servers',
'🎪 Welcome to the suspended server circus! Check billing or contact support',
'🏰 Your server is locked in a tower. Payment or support has the key!',
'💰 Server hibernating due to billing issues - Check invoices or contact support',
'📞 Suspension notice: Verify payments or contact support if this is an error',
'🔍 Check your billing dashboard or contact support for clarification',
];
let messageIndex = 0;
let fakeStats = {
cpu: 0,
memory: 0,
disk: 0,
uptime: 0,
network: {
rx: 0,
tx: 0
}
};
// Show initial suspended message
term.write('🔒 SERVER SUSPENDED', 'error');
term.write('Your server has been suspended and cannot be accessed.', 'info');
term.write(
'Please check your invoices for unpaid bills or contact support if this is a mistake.',
'info');
term.write('', 'default');
// Update info cards with fake data but real address
function updateSuspendedCards() {
// Prevent updates if elements don't exist (during unmount)
if (!document.getElementById('server-adress')) {
console.log('Suspended status cards not found, skipping update');
return;
}
// Keep real address
const allocation = serverInfo.relationships.allocations.data.find(a => a.attributes
.is_default);
const address = allocation ?
`${allocation.attributes.ip_alias ?? allocation.attributes.ip}:${allocation.attributes.port}` :
'N/A';
const addressEl = document.getElementById('server-adress');
if (addressEl) addressEl.innerText = address;
// Fake uptime
const uptimeEl = document.getElementById('server-uptime');
if (uptimeEl) uptimeEl.innerText = '0s';
// Fake CPU (always 0)
const cpuEl = document.getElementById('server-cpu');
if (cpuEl) cpuEl.innerText = '0.0 / 0%';
// Fake RAM (always 0)
const maxRamBytes = MAX_RAM_MB * 1024 * 1024;
const ramEl = document.getElementById('server-ram');
if (ramEl) ramEl.innerText = `0 Bytes / ${formatBytes(maxRamBytes)}`;
const ramBarEl = document.getElementById('server-ram-bar');
if (ramBarEl) ramBarEl.style.width = '0%';
// Fake Disk (always 0)
const maxDiskBytes = MAX_DISK_MB * 1024 * 1024;
const diskEl = document.getElementById('server-disk');
if (diskEl) diskEl.innerText = `0 Bytes / ${formatBytes(maxDiskBytes)}`;
const diskBarEl = document.getElementById('server-disk-bar');
if (diskBarEl) diskBarEl.style.width = '0%';
// Fake Network (always 0)
const inboundEl = document.getElementById('server-inbound');
if (inboundEl) inboundEl.innerText = '0 Bytes';
const outboundEl = document.getElementById('server-outbound');
if (outboundEl) outboundEl.innerText = '0 Bytes';
const inboundPpsEl = document.getElementById('server-inbound-pps');
if (inboundPpsEl) inboundPpsEl.innerText = '0 Bytes/s';
const outboundPpsEl = document.getElementById('server-outbound-pps');
if (outboundPpsEl) outboundPpsEl.innerText = '0 Bytes/s';
}
// Update charts with fake data (always 0)
function updateSuspendedCharts() {
charts.push('cpu', 0);
charts.push('ram', 0);
charts.push('net', {
rx: 0,
tx: 0
});
}
// Console input handler for suspended server
onSuspendedCommand = function(e) {
if (e.key !== 'Enter' && e.keyCode !== 13) return;
e.preventDefault();
const cmd = inputEl().value.trim();
if (!cmd) return;
// Show the command they tried
term.write(`> ${cmd}`, 'default');
// Check for special commands
if (cmd.toLowerCase() === 'tetris' || cmd.toLowerCase() === 'play tetris') {
startTetrisGame();
history.commit(cmd);
inputEl().value = '';
return;
}
if (cmd.toLowerCase() === 'help' || cmd.toLowerCase() === 'commands') {
term.write('Available commands while suspended:', 'info');
term.write(' tetris - Play a fun Tetris game!', 'info');
term.write(' help - Show this help message', 'info');
term.write('Note: Server management is disabled while suspended.', 'info');
history.commit(cmd);
inputEl().value = '';
return;
}
// Respond with suspension message
const responses = [
'🔒 Command blocked: Server suspended - Check invoices or contact support',
'❌ Access denied: Verify payment status or contact support if error',
'🚫 Server suspended: Check billing dashboard or contact support',
'💤 Server sleeping: Check unpaid invoices or contact support',
'🎭 Nice try! Server suspended - Check your billing or contact support',
'🎮 Command not found: Check invoices or try "tetris" for fun, or "help" for commands',
'🦄 Magic command detected, but billing issues suspend servers',
'🕷️ With great commands comes great invoice responsibility',
'💳 Suspension active: Check payment status or contact support for help',
'📋 Server locked: Review billing or contact support if this is an error',
];
term.write(responses[Math.floor(Math.random() * responses.length)], 'error');
history.commit(cmd);
inputEl().value = '';
};
// Wire up input
const inputElement = inputEl();
// Track event listeners for cleanup detection
if (!inputElement._consoleEventListeners) {
inputElement._consoleEventListeners = [];
}
inputElement.addEventListener('keydown', onSuspendedCommand);
inputElement.addEventListener('keypress', onSuspendedCommand);
inputElement.setAttribute('data-console-listeners', 'true');
// Track the listeners we just added
inputElement._consoleEventListeners.push({
type: 'keydown',
listener: onSuspendedCommand
}, {
type: 'keypress',
listener: onSuspendedCommand
});
// TETRIS GAME IMPLEMENTATION
function startTetrisGame() {
term.write('🎮 Starting Tetris Game!', 'info');
term.write('Controls: A/D = Move, S = Rotate, W = Drop, Q = Quit', 'info');
term.write('═══════════════════════════════════════', 'default');
tetrisGame = new TetrisGame(term);
tetrisGame.start();
}
class TetrisGame {
constructor(terminal) {
this.term = terminal;
this.board = Array(20).fill().map(() => Array(10).fill(' '));
this.score = 0;
this.level = 1;
this.lines = 0;
this.gameRunning = false;
this.currentPiece = null;
this.nextPiece = null;
this.gameInterval = null;
// Tetris pieces
this.pieces = [{
shape: [
[1, 1, 1, 1]
],
color: '█'
}, // I
{
shape: [
[1, 1],
[1, 1]
],
color: '█'
}, // O
{
shape: [
[0, 1, 0],
[1, 1, 1]
],
color: '█'
}, // T
{
shape: [
[0, 1, 1],
[1, 1, 0]
],
color: '█'
}, // S
{
shape: [
[1, 1, 0],
[0, 1, 1]
],
color: '█'
}, // Z
{
shape: [
[1, 0, 0],
[1, 1, 1]
],
color: '█'
}, // J
{
shape: [
[0, 0, 1],
[1, 1, 1]
],
color: '█'
}, // L
];
this.currentPos = {
x: 4,
y: 0
};
this.bindControls();
}
bindControls() {
this.keyHandler = (e) => {
if (!this.gameRunning) return;
switch (e.key.toLowerCase()) {
case 'a':
this.movePiece(-1, 0);
break;
case 'd':
this.movePiece(1, 0);
break;
case 's':
this.rotatePiece();
break;
case 'w':
this.dropPiece();
break;
case 'q':
this.quitGame();
break;
}
e.preventDefault();
};
document.addEventListener('keydown', this.keyHandler);
}
start() {
this.gameRunning = true;
this.spawnPiece();
this.gameInterval = setInterval(() => {
this.update();
}, Math.max(100, 1000 - (this.level - 1) * 100));
this.render();
}
spawnPiece() {
this.currentPiece = this.nextPiece || this.getRandomPiece();
this.nextPiece = this.getRandomPiece();
this.currentPos = {
x: 4,
y: 0
};
if (this.checkCollision(this.currentPiece, this.currentPos)) {
this.gameOver();
}
}
getRandomPiece() {
return JSON.parse(JSON.stringify(this.pieces[Math.floor(Math.random() * this.pieces
.length)]));
}
movePiece(dx, dy) {
const newPos = {
x: this.currentPos.x + dx,
y: this.currentPos.y + dy
};
if (!this.checkCollision(this.currentPiece, newPos)) {
this.currentPos = newPos;
this.render();
}
}
rotatePiece() {
const rotated = this.rotate(this.currentPiece.shape);
const rotatedPiece = {
...this.currentPiece,
shape: rotated
};
if (!this.checkCollision(rotatedPiece, this.currentPos)) {
this.currentPiece.shape = rotated;
this.render();
}
}
dropPiece() {
while (!this.checkCollision(this.currentPiece, {
x: this.currentPos.x,
y: this.currentPos.y + 1
})) {
this.currentPos.y++;
}
this.render();
}
rotate(matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
const rotated = Array(cols).fill().map(() => Array(rows).fill(0));
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
rotated[j][rows - 1 - i] = matrix[i][j];
}
}
return rotated;
}
checkCollision(piece, pos) {
for (let y = 0; y < piece.shape.length; y++) {
for (let x = 0; x < piece.shape[y].length; x++) {
if (piece.shape[y][x]) {
const newX = pos.x + x;
const newY = pos.y + y;
if (newX < 0 || newX >= 10 || newY >= 20) return true;
if (newY >= 0 && this.board[newY][newX] !== ' ') return true;
}
}
}
return false;
}
placePiece() {
for (let y = 0; y < this.currentPiece.shape.length; y++) {
for (let x = 0; x < this.currentPiece.shape[y].length; x++) {
if (this.currentPiece.shape[y][x]) {
const boardY = this.currentPos.y + y;
const boardX = this.currentPos.x + x;
if (boardY >= 0) {
this.board[boardY][boardX] = this.currentPiece.color;
}
}
}
}
this.clearLines();
this.spawnPiece();
}
clearLines() {
let linesCleared = 0;
for (let y = this.board.length - 1; y >= 0; y--) {
if (this.board[y].every(cell => cell !== ' ')) {
this.board.splice(y, 1);
this.board.unshift(Array(10).fill(' '));
linesCleared++;
y++; // Check the same line again
}
}
if (linesCleared > 0) {
this.lines += linesCleared;
this.score += linesCleared * 100 * this.level;
this.level = Math.floor(this.lines / 10) + 1;
}
}
update() {
if (!this.gameRunning) return;
if (this.checkCollision(this.currentPiece, {
x: this.currentPos.x,
y: this.currentPos.y + 1
})) {
this.placePiece();
} else {
this.currentPos.y++;
}
this.render();
}
render() {
if (!this.gameRunning) return;
// Create display board
const display = this.board.map(row => [...row]);
// Add current piece
if (this.currentPiece) {
for (let y = 0; y < this.currentPiece.shape.length; y++) {
for (let x = 0; x < this.currentPiece.shape[y].length; x++) {
if (this.currentPiece.shape[y][x]) {
const boardY = this.currentPos.y + y;
const boardX = this.currentPos.x + x;
if (boardY >= 0 && boardY < 20 && boardX >= 0 && boardX < 10) {
display[boardY][boardX] = this.currentPiece.color;
}
}
}
}
}
// Clear terminal area (simplified)
let output = '';
output += `Score: ${this.score} | Level: ${this.level} | Lines: ${this.lines}\n`;
output += '┌──────────────────────┐\n';
for (let y = 0; y < Math.min(15, display.length); y++) {
output += '│';
for (let x = 0; x < display[y].length; x++) {
output += display[y][x] === ' ' ? '·' : '█';
output += display[y][x] === ' ' ? '·' : '█';
}
output += '│\n';
}
output += '└──────────────────────┘';
this.term.write('\x1b[2J\x1b[H' + output); // Clear screen and move cursor to top
}
gameOver() {
this.gameRunning = false;
clearInterval(this.gameInterval);
document.removeEventListener('keydown', this.keyHandler);
this.term.write('', 'default');
this.term.write('🎮 GAME OVER!', 'error');
this.term.write(`Final Score: ${this.score}`, 'info');
this.term.write(`Lines Cleared: ${this.lines}`, 'info');
this.term.write(`Level Reached: ${this.level}`, 'info');
this.term.write('Type "tetris" to play again!', 'info');
this.term.write('═══════════════════════════════════════', 'default');
tetrisGame = null;
}
quitGame() {
this.gameRunning = false;
clearInterval(this.gameInterval);
document.removeEventListener('keydown', this.keyHandler);
this.term.write('', 'default');
this.term.write('🎮 Game Quit! Thanks for playing!', 'info');
this.term.write(`Final Score: ${this.score}`, 'info');
this.term.write('Type "tetris" to play again!', 'info');
this.term.write('═══════════════════════════════════════', 'default');
tetrisGame = null;
}
}
// Initial data update
updateSuspendedCards();
updateSuspendedCharts();
// Periodic fake messages and data updates
const messageInterval = setInterval(() => {
if (!mounted || !serverInfo.is_suspended) {
clearInterval(messageInterval);
return;
}
// Show random suspended message
term.write(suspendedMessages[messageIndex % suspendedMessages.length], 'info');
messageIndex++;
// Update fake stats and charts
updateSuspendedCards();
updateSuspendedCharts();
}, 8000 + Math.random() * 4000); // Random interval 8-12 seconds
// Store interval for cleanup
window.suspendedInterval = messageInterval;
}
async function mountConsole() {
if (mounted || mounting) {
console.log('Mount console called but already mounted/mounting:', {
mounted,
mounting
});
return; // Prevent double mounting
}
// Check if elements are already initialized and clean them up
if (elementsAlreadyInitialized()) {
console.log('Elements already initialized, cleaning up before mounting...');
cleanupExistingElements();
// Small delay to ensure cleanup completes
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log('Starting console mount...');
mounting = true; // Set mounting flag
try {
// Check if server is suspended and handle differently
if (serverInfo.is_suspended) {
console.log('Mounting suspended console...');
await mountSuspendedConsole();
mounted = true;
mounting = false;
console.log('Suspended console mounted successfully');
return;
}
history = new History(inputEl());
term = new TerminalManager('terminal');
charts = new ChartManager();
ws = new WSClient(term, charts);
function onStartClick() {
ws.setState('start');
}
function onStopClick() {
ws.setState('stop');
}
function onRestartClick() {
ws.setState('restart');
}
function onKillClick() {
ws.setState('kill');
}
// wire ENTER on console input (support keydown & keypress)
const inputElement = inputEl();
// Track event listeners for cleanup detection
if (!inputElement._consoleEventListeners) {
inputElement._consoleEventListeners = [];
}
inputElement.addEventListener('keydown', onCommand);
inputElement.addEventListener('keypress', onCommand);
inputElement.setAttribute('data-console-listeners', 'true');
// Track the listeners we just added
inputElement._consoleEventListeners.push({
type: 'keydown',
listener: onCommand
}, {
type: 'keypress',
listener: onCommand
});
// (re)select buttons and wire actions
btnStart = document.querySelector('[data-action="start"]');
btnStop = document.querySelector('[data-action="stop"]');
btnRestart = document.querySelector('[data-action="restart"]');
btnKill = document.querySelector('[data-action="kill"]');
btnStart.addEventListener('click', onStartClick);
btnStop.addEventListener('click', onStopClick);
btnRestart.addEventListener('click', onRestartClick);
btnKill.addEventListener('click', onKillClick);
await ws.connect({{ $service->id }});
term.write('Waiting for server…', 'info');
updateButtons(serverInfo.status);
mounted = true;
console.log('Console mounted successfully');
} catch (error) {
console.error('Error mounting console:', error);
term?.write('Error initializing console: ' + error.message, 'error');
} finally {
mounting = false; // Always clear mounting flag
}
}
function unmountConsole() {
if (!mounted) {
console.log('Unmount console called but not mounted');
return;
}
console.log('Unmounting console...');
mounted = false;
mounting = false; // Reset mounting flag
// Clean up suspended server interval if it exists
if (window.suspendedInterval) {
clearInterval(window.suspendedInterval);
window.suspendedInterval = null;
}
// Clean up Tetris game if running
if (tetrisGame && tetrisGame.gameRunning) {
try {
tetrisGame.quitGame();
} catch (e) {
console.warn('Error quitting Tetris game:', e);
}
}
tetrisGame = null; // Clear reference
// Clean up regular console components if they exist
if (ws) {
if (ws.socket) {
try {
ws.socket.close();
// Clear global reference
if (window.activeConsoleWebSocket === ws.socket) {
window.activeConsoleWebSocket = null;
}
} catch (e) {
console.warn('Error closing WebSocket:', e);
}
}
// Clear the netBase to prevent further updates
ws.netBase = null;
}
if (term && term.term) {
try {
term.term.dispose();
} catch (e) {
console.warn('Error disposing terminal:', e);
}
}
if (charts) {
try {
if (charts.cpu) charts.cpu.destroy();
if (charts.ram) charts.ram.destroy();
if (charts.net) charts.net.destroy();
} catch (e) {
console.warn('Error destroying charts:', e);
}
}
// Remove event listeners with error handling
const input = inputEl();
if (input) {
try {
if (typeof onCommand === 'function') {
input.removeEventListener('keydown', onCommand);
input.removeEventListener('keypress', onCommand);
}
if (typeof onSuspendedCommand === 'function') {
input.removeEventListener('keydown', onSuspendedCommand);
input.removeEventListener('keypress', onSuspendedCommand);
}
input.removeAttribute('data-console-listeners');
if (input._consoleEventListeners) {
input._consoleEventListeners = [];
}
} catch (e) {
console.warn('Error removing input event listeners:', e);
}
}
// Clear terminal initialization marker
const terminalEl = document.getElementById('terminal');
if (terminalEl) {
terminalEl.removeAttribute('data-console-initialized');
}
// Clear references
history = null;
term = null;
charts = null;
ws = null;
onSuspendedCommand = null;
console.log('Console unmounted successfully');
}
function onCommand(e) {
// only react to Enter key
if (e.key !== 'Enter' && e.keyCode !== 13) return;
e.preventDefault();
const cmd = inputEl().value.trim();
if (!cmd || !ws) return;
console.log('Sending command:', cmd);
ws.sendCommand(cmd);
history.commit(cmd);
inputEl().value = '';
}
// Cleanup function to prevent memory leaks and multiple intervals
function cleanup() {
console.log('Cleaning up console resources');
// Clean up watch interval
if (window.consoleWatchInterval) {
clearInterval(window.consoleWatchInterval);
window.consoleWatchInterval = null;
}
// Clean up mutation observer
if (mutationObserver) {
mutationObserver.disconnect();
mutationObserver = null;
}
// Unmount console
if (mounted) {
unmountConsole();
}
}
// Clean up on page unload/navigation
window.addEventListener('beforeunload', cleanup);
window.addEventListener('pagehide', cleanup);
// Also clean up if the script runs multiple times (e.g., hot reload during development)
if (window.consoleCleanup) {
window.consoleCleanup();
}
window.consoleCleanup = cleanup;
// Clean up any existing interval (in case script runs multiple times)
if (window.consoleWatchInterval) {
clearInterval(window.consoleWatchInterval);
window.consoleWatchInterval = null;
}
// Check for and clean up any existing console elements before initial mount
// Be more aggressive about cleanup on page load
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts && elementsAlreadyInitialized()) {
console.log(
`Detected existing console elements on page load (attempt ${attempts + 1}), cleaning up...`);
cleanupExistingElements(true); // Force cleanup
attempts++;
// Small delay to ensure cleanup completes
await new Promise(resolve => setTimeout(resolve, 100 + (attempts * 50)));
}
if (attempts >= maxAttempts) {
console.warn('Max cleanup attempts reached, forcing fresh start...');
// Last resort: clear everything we can find
const terminalEl = document.getElementById('terminal');
if (terminalEl && terminalEl.parentNode) {
const newTerminal = document.createElement('div');
newTerminal.id = 'terminal';
newTerminal.className = terminalEl.className;
terminalEl.parentNode.replaceChild(newTerminal, terminalEl);
}
}
// Initial mount with error handling
try {
await mountConsole();
} catch (error) {
console.error('Failed to mount console:', error);
}
// Watch #terminal and tear down/re-mount as needed
// Store interval globally to prevent multiple intervals
window.consoleWatchInterval = setInterval(() => {
const now = Date.now();
// Debounce: only check every 500ms minimum
if (now - lastTerminalCheck < 500) return;
lastTerminalCheck = now;
const alive = !!document.getElementById('terminal');
if (!alive && mounted && !mounting) {
console.log('Terminal element not found, unmounting console');
unmountConsole();
}
if (alive && !mounted && !mounting) {
// Always check for stale elements when remounting, be more aggressive
if (elementsAlreadyInitialized()) {
console.log('Found stale console elements, performing aggressive cleanup...');
cleanupExistingElements(true); // Force cleanup
// Double-check after cleanup
if (elementsAlreadyInitialized()) {
console.warn('Elements still detected after cleanup, forcing DOM reset...');
// Nuclear option: replace the terminal element entirely
const terminalEl = document.getElementById('terminal');
if (terminalEl && terminalEl.parentNode) {
const newTerminal = document.createElement('div');
newTerminal.id = 'terminal';
newTerminal.className = terminalEl.className;
terminalEl.parentNode.replaceChild(newTerminal, terminalEl);
}
}
// Additional delay after aggressive cleanup
setTimeout(() => {
if (!mounted && !mounting) {
console.log('Attempting mount after aggressive cleanup...');
mountConsole().catch(error => {
console.error(
'Failed to remount console after cleanup:',
error);
mounting = false;
});
}
}, 200);
return;
}
console.log('Terminal element found, mounting console');
mountConsole().catch(error => {
console.error('Failed to remount console:', error);
mounting = false; // Reset flag on error
});
}
}, 2000); // Increased interval to 2 seconds to reduce checking frequency
// Add mutation observer to detect DOM changes that might indicate stale elements
let mutationObserver = null;
function setupMutationObserver() {
if (mutationObserver) {
mutationObserver.disconnect();
}
mutationObserver = new MutationObserver((mutations) => {
let shouldCheck = false;
mutations.forEach((mutation) => {
// Check if terminal or related elements were modified
if (mutation.type === 'childList') {
const target = mutation.target;
if (target.id === 'terminal' ||
target.closest('#terminal') ||
target.id === 'console-input' ||
target.closest('#console-input')) {
shouldCheck = true;
}
}
});
if (shouldCheck && !mounted && !mounting) {
console.log('DOM mutation detected, checking for stale elements...');
if (elementsAlreadyInitialized()) {
console.log('Stale elements detected via mutation observer, cleaning up...');
cleanupExistingElements(true);
}
}
});
// Observe the entire console section
const consoleSection = document.getElementById('console-section');
if (consoleSection) {
mutationObserver.observe(consoleSection, {
childList: true,
subtree: true,
attributes: false
});
}
}
// Setup mutation observer
setupMutationObserver();
// Initial mount with error handling
try {
await mountConsole();
} catch (error) {
console.error('Failed to mount console:', error);
}
// Watch #terminal and tear down/re-mount as needed
// Store interval globally to prevent multiple intervals
window.consoleWatchInterval = setInterval(() => {
const now = Date.now();
// Debounce: only check every 500ms minimum
if (now - lastTerminalCheck < 500) return;
lastTerminalCheck = now;
const alive = !!document.getElementById('terminal');
if (!alive && mounted && !mounting) {
console.log('Terminal element not found, unmounting console');
unmountConsole();
}
if (alive && !mounted && !mounting) {
// Always check for stale elements when remounting, be more aggressive
if (elementsAlreadyInitialized()) {
console.log('Found stale console elements, performing aggressive cleanup...');
cleanupExistingElements(true); // Force cleanup
// Double-check after cleanup
if (elementsAlreadyInitialized()) {
console.warn('Elements still detected after cleanup, forcing DOM reset...');
// Nuclear option: replace the terminal element entirely
const terminalEl = document.getElementById('terminal');
if (terminalEl && terminalEl.parentNode) {
const newTerminal = document.createElement('div');
newTerminal.id = 'terminal';
newTerminal.className = terminalEl.className;
terminalEl.parentNode.replaceChild(newTerminal, terminalEl);
}
}
// Additional delay after aggressive cleanup
setTimeout(() => {
if (!mounted && !mounting) {
console.log('Attempting mount after aggressive cleanup...');
mountConsole().catch(error => {
console.error(
'Failed to remount console after cleanup:',
error);
mounting = false;
});
}
}, 200);
return;
}
console.log('Terminal element found, mounting console');
mountConsole().catch(error => {
console.error('Failed to remount console:', error);
mounting = false; // Reset flag on error
});
}
}, 500); // Increased interval to 2 seconds to reduce checking frequency
Livewire.hook('element.updated', (el, component) => {
console.log('Livewire element updated:', el, component);
});
Livewire.hook('element.removed', (el, component) => {
console.log('Livewire element removed:', el, component);
})
})();
</script>
@endscript