feat: Implement WebSocket-based SSH terminal functionality and refactor the frontend using the Tabler.io framework.
Build Docker Container using Multistage Build / build (push) Failing after 4m17s
Build Docker Container using Multistage Build / build (push) Failing after 4m17s
This commit is contained in:
+543
-183
@@ -1,207 +1,567 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Server Management</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-100 text-gray-800">
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>Nexus Node | Server Management</title>
|
||||
<!-- CSS files -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler-flags.min.css" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler-payments.min.css" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/css/tabler-vendors.min.css" rel="stylesheet"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||||
<style>
|
||||
@import url('https://rsms.me/inter/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
.terminal-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Theme toggle fix */
|
||||
body[data-bs-theme="dark"] .hide-theme-dark { display: none !important; }
|
||||
body[data-bs-theme="dark"] .hide-theme-light { display: flex !important; }
|
||||
|
||||
<div class="container mx-auto p-8">
|
||||
<h1 class="text-4xl font-bold mb-8">Server Management</h1>
|
||||
|
||||
<div id="server-table-container" class="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<table class="min-w-full leading-normal">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Name</th>
|
||||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">IP</th>
|
||||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">MAC</th>
|
||||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th>
|
||||
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="server-table-body">
|
||||
<!-- Server rows will be inserted here -->
|
||||
</tbody>
|
||||
</table>
|
||||
body[data-bs-theme="light"] .hide-theme-light { display: none !important; }
|
||||
body[data-bs-theme="light"] .hide-theme-dark { display: flex !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler-theme.min.js"></script>
|
||||
<div class="page">
|
||||
<!-- Sidebar -->
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<h1 class="navbar-brand navbar-brand-autodark">
|
||||
<a href=".">
|
||||
<span class="text-primary"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-server-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path>
|
||||
<path d="M3 12m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path>
|
||||
<path d="M7 8l0 .01"></path>
|
||||
<path d="M7 16l0 .01"></path>
|
||||
</svg></span>
|
||||
Nexus Node
|
||||
</a>
|
||||
</h1>
|
||||
<div class="collapse navbar-collapse" id="sidebar-menu">
|
||||
<ul class="navbar-nav pt-lg-3">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="./">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from http://tabler-icons.io/i/home -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l-2 0l9 -9l9 9l-2 0" /><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" /><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" /></svg>
|
||||
</span>
|
||||
<span class="nav-link-title">
|
||||
Servers
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">
|
||||
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"></path>
|
||||
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">
|
||||
Settings (Coming Soon)
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="no-servers-message" class="hidden bg-white shadow-md rounded-lg p-8 text-center">
|
||||
<p class="text-gray-600">No servers found. Please check your configuration.</p>
|
||||
</aside>
|
||||
|
||||
<!-- Navbar (Mobile + Desktop Top Bar for Theme Toggle) -->
|
||||
<header class="navbar navbar-expand-md d-none d-lg-flex d-print-none" >
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-nav flex-row order-md-last">
|
||||
<div class="d-none d-md-flex">
|
||||
<a href="?theme=dark" id="btn-enable-dark" class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>
|
||||
</a>
|
||||
<a href="?theme=light" id="btn-enable-light" class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="navbar-menu">
|
||||
<div>
|
||||
<!-- Search or breadcrumbs could go here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-fluid">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Server Registry
|
||||
</h2>
|
||||
<div class="text-secondary mt-1">Manage your infrastructure</div>
|
||||
</div>
|
||||
<!-- Page title actions -->
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal" data-bs-target="#modal-server">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
Register Server
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal" data-bs-target="#modal-server" aria-label="Create new report">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<button id="add-server-button" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
|
||||
Add New Server
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="add-server-form-container" class="hidden mt-8 bg-white shadow-md rounded-lg p-8">
|
||||
<h2 class="text-2xl font-bold mb-4">Add New Server</h2>
|
||||
<form id="add-server-form">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="block text-gray-700 text-sm font-bold mb-2">Name</label>
|
||||
<input type="text" id="name" name="name" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="ip" class="block text-gray-700 text-sm font-bold mb-2">IP Address</label>
|
||||
<input type="text" id="ip" name="ip" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="mac" class="block text-gray-700 text-sm font-bold mb-2">MAC Address</label>
|
||||
<input type="text" id="mac" name="mac" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="ssh_user" class="block text-gray-700 text-sm font-bold mb-2">SSH User</label>
|
||||
<input type="text" id="ssh_user" name="ssh_user" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="ssh_pass" class="block text-gray-700 text-sm font-bold mb-2">SSH Password</label>
|
||||
<input type="password" id="ssh_pass" name="ssh_pass" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Save Server
|
||||
</button>
|
||||
<button type="button" id="cancel-add-server" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-fluid">
|
||||
<div class="row row-deck row-cards" id="server-list-container">
|
||||
<!-- Servers injected here -->
|
||||
</div>
|
||||
<!-- Empty State -->
|
||||
<div id="no-servers-message" class="empty d-none">
|
||||
<div class="empty-icon"><svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-server-off" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path><path d="M14.503 14.505a3 3 0 0 1 -2.503 -2.505m0 3h-6a3 3 0 0 1 -3 -3v-2a3 3 0 0 1 3 -3"></path><path d="M7 8l0 .01"></path><path d="M7 16l0 .01"></path><path d="M3 3l18 18"></path></svg></div>
|
||||
<p class="empty-title">No servers found</p>
|
||||
<p class="empty-subtitle text-secondary">
|
||||
Add a server to start monitoring your infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer footer-transparent d-print-none">
|
||||
<div class="container-fluid">
|
||||
<div class="row text-center align-items-center flex-row-reverse">
|
||||
<div class="col-12 col-lg-auto mt-3 mt-lg-0">
|
||||
<ul class="list-inline list-inline-dots mb-0">
|
||||
<li class="list-inline-item">
|
||||
Nexus Node © 2026
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Modal -->
|
||||
<div class="modal modal-blur fade" id="modal-server" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-title">New Server</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="server-form">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="oldName" name="oldName">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Server Name</label>
|
||||
<input type="text" class="form-control" name="name" id="input-name" placeholder="e.g. edge-server-01" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">IP Address</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" class="form-control" name="ip" id="input-ip" placeholder="192.168.1.10" required>
|
||||
<span class="input-group-text">
|
||||
<a href="#" class="link-secondary" title="Auto-find MAC" data-bs-toggle="tooltip" id="btn-find-mac" style="text-decoration: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path><path d="M21 21l-6 -6"></path></svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">MAC Address</label>
|
||||
<input type="text" class="form-control" name="mac" id="input-mac" placeholder="00:11:22:33:44:55" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label">SSH Credentials <span class="form-label-description">(Optional for Proxy Wake)</span></label>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" name="ssh_user" id="input-user" placeholder="Username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-3">
|
||||
<input type="password" class="form-control" name="ssh_pass" id="input-pass" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary" data-bs-dismiss="modal">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary ms-auto" data-bs-dismiss="modal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 5l0 14" /><path d="M5 12l14 0" /></svg>
|
||||
Save Server
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Modal -->
|
||||
<div class="modal modal-blur fade" id="modal-terminal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-full-width modal-dialog-centered" role="document">
|
||||
<div class="modal-content" style="height: 90vh;">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="terminal-title">Terminal</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="btn-close-terminal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0 bg-black d-flex flex-column">
|
||||
<div id="terminal-container" class="terminal-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toasts -->
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3">
|
||||
<!-- Toasts injected here via JS -->
|
||||
</div>
|
||||
|
||||
<!-- Core Libs -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta17/dist/js/tabler.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
fetchServers();
|
||||
let terminal = null;
|
||||
let socket = null;
|
||||
let fitAddon = null;
|
||||
|
||||
const addServerButton = document.getElementById("add-server-button");
|
||||
const addServerFormContainer = document.getElementById("add-server-form-container");
|
||||
const cancelAddServerButton = document.getElementById("cancel-add-server");
|
||||
const addServerForm = document.getElementById("add-server-form");
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
fetchServers();
|
||||
|
||||
addServerButton.addEventListener("click", () => {
|
||||
addServerFormContainer.classList.remove("hidden");
|
||||
addServerButton.classList.add("hidden");
|
||||
// Theme Toggle Logic
|
||||
const themeStorageKey = 'tablerTheme';
|
||||
const initialTheme = localStorage.getItem(themeStorageKey) || 'light';
|
||||
document.body.setAttribute('data-bs-theme', initialTheme);
|
||||
|
||||
const btnDark = document.getElementById('btn-enable-dark');
|
||||
const btnLight = document.getElementById('btn-enable-light');
|
||||
|
||||
if(btnDark) {
|
||||
btnDark.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
document.body.setAttribute('data-bs-theme', 'dark');
|
||||
localStorage.setItem(themeStorageKey, 'dark');
|
||||
});
|
||||
}
|
||||
|
||||
cancelAddServerButton.addEventListener("click", () => {
|
||||
addServerFormContainer.classList.add("hidden");
|
||||
addServerButton.classList.remove("hidden");
|
||||
addServerForm.reset();
|
||||
if(btnLight) {
|
||||
btnLight.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
document.body.setAttribute('data-bs-theme', 'light');
|
||||
localStorage.setItem(themeStorageKey, 'light');
|
||||
});
|
||||
}
|
||||
|
||||
addServerForm.addEventListener("submit", function(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(addServerForm);
|
||||
const serverData = Object.fromEntries(formData.entries());
|
||||
const form = document.getElementById("server-form");
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
const oldName = document.getElementById("oldName").value;
|
||||
|
||||
const method = oldName ? "PUT" : "POST";
|
||||
const url = oldName ? `/servers?oldName=${encodeURIComponent(oldName)}` : "/servers";
|
||||
|
||||
fetch("/servers", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(serverData),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
fetchServers();
|
||||
addServerFormContainer.classList.add("hidden");
|
||||
addServerButton.classList.remove("hidden");
|
||||
addServerForm.reset();
|
||||
} else {
|
||||
alert("Failed to add server.");
|
||||
}
|
||||
});
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
}).then(r => {
|
||||
if(r.ok) {
|
||||
fetchServers();
|
||||
showToast(oldName ? "Server updated" : "Server registered", "success");
|
||||
// Modal auto closes due to data-bs-dismiss, but we should reset form
|
||||
resetForm();
|
||||
} else {
|
||||
showToast("Operation failed", "danger");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function fetchServers() {
|
||||
fetch("/servers")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const tableContainer = document.getElementById("server-table-container");
|
||||
const noServersMessage = document.getElementById("no-servers-message");
|
||||
const tableBody = document.getElementById("server-table-body");
|
||||
document.getElementById('modal-server').addEventListener('hidden.bs.modal', resetForm);
|
||||
|
||||
tableBody.innerHTML = ""; // Clear existing rows
|
||||
document.getElementById("btn-find-mac").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.currentTarget;
|
||||
const originalHtml = btn.innerHTML;
|
||||
const ip = document.getElementById("input-ip").value;
|
||||
const user = document.getElementById("input-user").value;
|
||||
const pass = document.getElementById("input-pass").value;
|
||||
|
||||
if (data && data.length > 0) {
|
||||
tableContainer.classList.remove("hidden");
|
||||
noServersMessage.classList.add("hidden");
|
||||
|
||||
data.forEach(server => {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">${server.name}</td>
|
||||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">${server.ip}</td>
|
||||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">${server.mac}</td>
|
||||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm" id="status-${server.name}">Checking...</td>
|
||||
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
|
||||
<button onclick="wakeServer('${server.name}')" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Wake</button>
|
||||
<button onclick="shutdownServer('${server.name}')" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Shutdown</button>
|
||||
<button onclick="rebootServer('${server.name}')" class="bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded">Reboot</button>
|
||||
<button onclick="deleteServer('${server.name}')" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">Delete</button>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(row);
|
||||
checkServerStatus(server.name);
|
||||
});
|
||||
} else {
|
||||
tableContainer.classList.add("hidden");
|
||||
noServersMessage.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkServerStatus(serverName) {
|
||||
fetch(`/status?name=${serverName}`)
|
||||
.then(response => response.text())
|
||||
.then(status => {
|
||||
const statusCell = document.getElementById(`status-${serverName}`);
|
||||
statusCell.textContent = status;
|
||||
if (status === "Online") {
|
||||
statusCell.classList.add("text-green-500", "font-bold");
|
||||
statusCell.classList.remove("text-red-500");
|
||||
} else {
|
||||
statusCell.classList.add("text-red-500", "font-bold");
|
||||
statusCell.classList.remove("text-green-500");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wakeServer(serverName) {
|
||||
fetch(`/wake?name=${serverName}`).then(() => alert(`${serverName} wake signal sent.`));
|
||||
}
|
||||
|
||||
function shutdownServer(serverName) {
|
||||
if (confirm(`Are you sure you want to shutdown ${serverName}?`)) {
|
||||
fetch(`/shutdown?name=${serverName}`).then(() => alert(`${serverName} is shutting down.`));
|
||||
}
|
||||
}
|
||||
|
||||
function rebootServer(serverName) {
|
||||
if (confirm(`Are you sure you want to reboot ${serverName}?`)) {
|
||||
fetch(`/reboot?name=${serverName}`).then(() => alert(`${serverName} is rebooting.`));
|
||||
}
|
||||
}
|
||||
|
||||
function deleteServer(serverName) {
|
||||
if (confirm(`Are you sure you want to delete ${serverName}? This action cannot be undone.`)) {
|
||||
fetch(`/servers?name=${serverName}`, {
|
||||
method: 'DELETE',
|
||||
if(!ip) { showToast("IP Address required", "warning"); return; }
|
||||
|
||||
btn.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`;
|
||||
|
||||
fetch(`/find-mac?ip=${ip}&user=${encodeURIComponent(user)}&pass=${encodeURIComponent(pass)}`)
|
||||
.then(r => r.ok ? r.text() : r.text().then(t => {throw t}))
|
||||
.then(mac => {
|
||||
document.getElementById("input-mac").value = mac;
|
||||
showToast("MAC Address Found", "success");
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
fetchServers(); // Refresh the server list
|
||||
} else {
|
||||
alert(`Failed to delete ${serverName}.`);
|
||||
}
|
||||
});
|
||||
.catch(e => showToast(e, "danger"))
|
||||
.finally(() => btn.innerHTML = originalHtml);
|
||||
});
|
||||
|
||||
// Terminal cleanup
|
||||
document.getElementById('modal-terminal').addEventListener('hidden.bs.modal', function () {
|
||||
if(socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
if(terminal) {
|
||||
terminal.dispose();
|
||||
terminal = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh server status every 30 seconds
|
||||
setInterval(fetchServers, 30000);
|
||||
// Terminal resize observer
|
||||
new ResizeObserver(() => {
|
||||
if(fitAddon) fitAddon.fit();
|
||||
}).observe(document.getElementById('terminal-container'));
|
||||
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById("server-form").reset();
|
||||
document.getElementById("oldName").value = "";
|
||||
document.getElementById("modal-title").textContent = "New Server";
|
||||
}
|
||||
|
||||
function fetchServers() {
|
||||
fetch("/servers").then(r => r.json()).then(data => {
|
||||
const container = document.getElementById("server-list-container");
|
||||
const empty = document.getElementById("no-servers-message");
|
||||
container.innerHTML = "";
|
||||
|
||||
if(data && data.length > 0) {
|
||||
container.classList.remove("d-none");
|
||||
empty.classList.add("d-none");
|
||||
|
||||
data.sort((a,b) => a.name.localeCompare(b.name));
|
||||
|
||||
data.forEach(s => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "col-12 col-md-6 col-lg-4";
|
||||
div.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-status-top bg-secondary" id="status-bar-${s.name}"></div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="avatar rounded" id="status-avatar-${s.name}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-server" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path><path d="M3 12m0 3a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v2a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3z"></path><path d="M7 8l0 .01"></path><path d="M7 16l0 .01"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3 class="card-title text-truncate mb-1">${s.name}</h3>
|
||||
<div class="text-secondary text-truncate">${s.ip}</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a href="#" class="dropdown-item" onclick="editServer('${s.name}', '${s.ip}', '${s.mac}', '${(s.ssh_user||"").replace(/'/g, "\\'")}', '${(s.ssh_pass||"").replace(/'/g, "\\'")}')">Edit</a>
|
||||
<a href="#" class="dropdown-item text-danger" onclick="deleteServer('${s.name}')">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 row">
|
||||
<div class="col">Status: <span id="status-text-${s.name}" class="badge bg-secondary-lt">Checking</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="#" onclick="openTerminal('${s.name}')" class="btn btn-ghost-primary btn-icon" title="Terminal" data-bs-toggle="tooltip">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9l3 3l-3 3" /><path d="M13 15l3 0" /><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /></svg>
|
||||
</a>
|
||||
<a href="#" onclick="wakeServer('${s.name}')" class="btn btn-ghost-success btn-icon" title="Wake-on-LAN" data-bs-toggle="tooltip">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-power" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 6a7.75 7.75 0 1 0 10 0" /><path d="M12 4l0 8" /></svg>
|
||||
</a>
|
||||
<a href="#" onclick="rebootServer('${s.name}')" class="btn btn-ghost-warning btn-icon" title="Reboot" data-bs-toggle="tooltip">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /><path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" /></svg>
|
||||
</a>
|
||||
<a href="#" onclick="shutdownServer('${s.name}')" class="btn btn-ghost-danger btn-icon" title="Shutdown" data-bs-toggle="tooltip">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plug" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9.785 6l8.215 8.215l-2.054 2.054a5.81 5.81 0 1 1 -8.215 -8.215l2.054 -2.054z" /><path d="M4 20l3.5 -3.5" /><path d="M15 4l-3.5 3.5" /><path d="M20 9l-3.5 3.5" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
checkStatus(s.name);
|
||||
});
|
||||
} else {
|
||||
container.classList.add("d-none");
|
||||
empty.classList.remove("d-none");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkStatus(name) {
|
||||
fetch(`/status?name=${name}`).then(r => r.text()).then(status => {
|
||||
const badge = document.getElementById(`status-text-${name}`);
|
||||
const bar = document.getElementById(`status-bar-${name}`);
|
||||
const avatar = document.getElementById(`status-avatar-${name}`);
|
||||
|
||||
if(!badge) return;
|
||||
badge.textContent = status;
|
||||
badge.className = "badge";
|
||||
|
||||
if(status === "Online") {
|
||||
badge.classList.add("bg-success-lt");
|
||||
bar.className = "card-status-top bg-success";
|
||||
avatar.className = "avatar rounded bg-success-lt text-success";
|
||||
} else {
|
||||
badge.classList.add("bg-danger-lt");
|
||||
bar.className = "card-status-top bg-danger";
|
||||
avatar.className = "avatar rounded bg-danger-lt text-danger";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editServer(name, ip, mac, user, pass) {
|
||||
document.getElementById("modal-title").textContent = "Edit Server";
|
||||
document.getElementById("oldName").value = name;
|
||||
document.getElementById("input-name").value = name;
|
||||
document.getElementById("input-ip").value = ip;
|
||||
document.getElementById("input-mac").value = mac;
|
||||
document.getElementById("input-user").value = (user && user !== "undefined") ? user : "";
|
||||
document.getElementById("input-pass").value = (pass && pass !== "undefined") ? pass : "";
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('modal-server'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function deleteServer(name) {
|
||||
if(confirm(`Are you sure you want to delete ${name}?`)) {
|
||||
fetch(`/servers?name=${name}`, { method: 'DELETE' }).then(r => {
|
||||
fetchServers();
|
||||
showToast("Server deleted", "secondary");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function wakeServer(name) {
|
||||
fetch(`/wake?name=${name}`).then(() => showToast(`Wake signal sent to ${name}`, "success"));
|
||||
}
|
||||
function rebootServer(name) {
|
||||
if(confirm(`Reboot ${name}?`)) fetch(`/reboot?name=${name}`).then(() => showToast(`Rebooting ${name}`, "warning"));
|
||||
}
|
||||
function shutdownServer(name) {
|
||||
if(confirm(`Shutdown ${name}?`)) fetch(`/shutdown?name=${name}`).then(() => showToast(`Shutting down ${name}`, "danger"));
|
||||
}
|
||||
|
||||
function openTerminal(name) {
|
||||
document.getElementById("terminal-title").textContent = `Terminal: ${name}`;
|
||||
const modalEl = document.getElementById("modal-terminal");
|
||||
const modal = new bootstrap.Modal(modalEl);
|
||||
modal.show();
|
||||
|
||||
modalEl.addEventListener('shown.bs.modal', () => {
|
||||
const container = document.getElementById("terminal-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
theme: { background: "#000", foreground: "#fff" },
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace'
|
||||
});
|
||||
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(container);
|
||||
fitAddon.fit();
|
||||
|
||||
// Wait a bit for layout to settle
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
const rows = terminal.rows;
|
||||
const cols = terminal.cols;
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
socket = new WebSocket(`${protocol}//${window.location.host}/ssh?name=${encodeURIComponent(name)}&rows=${rows}&cols=${cols}`);
|
||||
|
||||
socket.onopen = () => terminal.write("\r\n--- Connected to Nexus Node Proxy ---\r\n");
|
||||
socket.onmessage = (e) => terminal.write(e.data);
|
||||
socket.onclose = () => terminal.write("\r\n--- Disconnected ---\r\n");
|
||||
|
||||
terminal.onData(data => {
|
||||
if(socket && socket.readyState === WebSocket.OPEN) socket.send(data);
|
||||
});
|
||||
}, 200);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
function showToast(msg, type="info") {
|
||||
const container = document.querySelector(".toast-container");
|
||||
const el = document.createElement("div");
|
||||
el.className = `toast align-items-center text-white bg-${type} border-0`;
|
||||
el.setAttribute("role", "alert");
|
||||
el.setAttribute("aria-live", "assertive");
|
||||
el.setAttribute("aria-atomic", "true");
|
||||
el.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${msg}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(el);
|
||||
const toast = new bootstrap.Toast(el);
|
||||
toast.show();
|
||||
el.addEventListener('hidden.bs.toast', () => el.remove());
|
||||
}
|
||||
|
||||
setInterval(fetchServers, 30000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user