refactor: Redesign the server list from individual cards to a table layout and refine terminal modal styling.
Build Docker Container using Multistage Build / build (push) Failing after 1m38s

This commit is contained in:
Matthias Hinrichs
2026-02-18 01:13:48 +01:00
parent e68e355e3f
commit da26078383
2 changed files with 64 additions and 65 deletions
+56 -65
View File
@@ -20,12 +20,13 @@
font-feature-settings: "cv03", "cv04", "cv11";
}
.terminal-container {
height: 100%;
height: 70vh;
max-height: 70vh !important;
width: 100%;
background: #000;
padding: 10px;
padding: 0 !important;
border-radius: 4px;
flex-grow: 1;
overflow: hidden;
}
/* Theme toggle fix */
@@ -144,8 +145,23 @@
<!-- 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 class="card">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>IP Address</th>
<th>MAC Address</th>
<th class="w-1">Actions</th>
</tr>
</thead>
<tbody id="server-list">
</tbody>
</table>
<div id="loading-spinner" class="text-center p-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
<!-- Empty State -->
<div id="no-servers-message" class="empty d-none">
@@ -248,7 +264,7 @@
<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 class="modal-body p-0 bg-black">
<div id="terminal-container" class="terminal-container"></div>
</div>
</div>
@@ -373,73 +389,54 @@
}
function fetchServers() {
fetch("/servers").then(r => r.json()).then(data => {
const container = document.getElementById("server-list-container");
const empty = document.getElementById("no-servers-message");
fetch("/servers")
.then(r => r.json())
.then(data => {
const spinner = document.getElementById("loading-spinner");
const container = document.getElementById("server-list");
const empty = document.getElementById("no-servers-message"); // Re-targeting correct empty div if name changed in previous step, assuming "no-servers-message"
if(spinner) spinner.classList.add("d-none");
container.innerHTML = "";
if(data && data.length > 0) {
container.classList.remove("d-none");
container.closest(".card").classList.remove("d-none"); // Show card
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>
const tr = document.createElement("tr");
tr.innerHTML = `
<td><span id="status-text-${s.name}" class="badge bg-secondary-lt">Checking</span></td>
<td><div class="font-weight-medium">${s.name}</div></td>
<td class="text-secondary">${s.ip}</td>
<td class="text-secondary">${s.mac}</td>
<td>
<div class="btn-list flex-nowrap">
<a href="#" onclick="openTerminal('${s.name}')" class="btn 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-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>
<div class="dropdown">
<button class="btn btn-icon dropdown-toggle align-text-top" data-bs-boundary="viewport" data-bs-toggle="dropdown"></button>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" onclick="editServer('${s.name}', '${s.ip}', '${s.mac}', '${(s.ssh_user||"").replace(/'/g, "\\'")}', '${(s.ssh_pass||"").replace(/'/g, "\\'")}')">Edit</a>
<a class="dropdown-item" onclick="rebootServer('${s.name}')">Reboot</a>
<a class="dropdown-item" onclick="shutdownServer('${s.name}')">Shutdown</a>
<a class="dropdown-item text-danger" onclick="deleteServer('${s.name}')">Delete</a>
</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>
</td>
`;
container.appendChild(div);
container.appendChild(tr);
checkStatus(s.name);
});
} else {
container.classList.add("d-none");
container.closest(".card").classList.add("d-none"); // Hide card
empty.classList.remove("d-none");
}
});
@@ -448,8 +445,6 @@
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;
@@ -457,12 +452,8 @@
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";
}
});
}
+8
View File
@@ -86,6 +86,14 @@ func (ws *WebServer) handleSSH(w http.ResponseWriter, r *http.Request) {
fmt.Sscanf(r.URL.Query().Get("cols"), "%d", &cols)
}
// Sanity check for dimensions
if rows <= 0 {
rows = 24
}
if cols <= 0 {
cols = 80
}
if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil {
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] Request for PTY failed: %v\r\n", err)))
return