diff --git a/src/features/admin/AdminSection.jsx b/src/features/admin/AdminSection.jsx
index d05dfc7..1221fc9 100644
--- a/src/features/admin/AdminSection.jsx
+++ b/src/features/admin/AdminSection.jsx
@@ -10,11 +10,13 @@ import {
Globe, Info, MessageSquare, Sliders, Terminal
} from "lucide-react";
import ServerDetailsModal from '../servers/ServerDetailsModal';
+import ServerCard from '../servers/ServerCard';
import NodeTerminalModal from './NodeTerminalModal';
import TicketSystem from '../support/TicketSystem';
import { colors } from '../../theme/colors';
import branding from '../../branding';
import { API_BASE_URL } from '../../config/api';
+import { usePanelCurrency } from '../../hooks/usePanelCurrency';
import {
StatTile,
ActionButton,
@@ -26,6 +28,7 @@ import {
} from '../../components/DashboardComponents';
const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
+ const { calculateCurrencyValue, getCurrencySymbol } = usePanelCurrency();
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
@@ -662,6 +665,26 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
setSelectedServerForManagement(server);
};
+ const handleAdminServerAction = async (serverId, action) => {
+ try {
+ const token = localStorage.getItem('anchorhost_token');
+ const response = await fetch(`${API_BASE_URL}/api/admin/servers/${encodeURIComponent(serverId)}/${action}`, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ const data = await response.json();
+ if (!response.ok || !data.success) {
+ throw new Error(data.error || `Could not ${action} this server.`);
+ }
+
+ const actionMessage = action === 'start' ? 'The server is starting.' : 'The server is stopping.';
+ showAlert('Server action sent', data.message || actionMessage, 'success');
+ fetchAllServers();
+ } catch (error) {
+ showAlert('Server action failed', error.message || 'Check the server status and try again.', 'error');
+ }
+ };
+
const filteredUsers = users.filter(u =>
(u.username && u.username.toLowerCase().includes(searchQuery.toLowerCase())) ||
(u.email && u.email.toLowerCase().includes(searchQuery.toLowerCase())) ||
@@ -848,60 +871,18 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
) : (
{allServers.map(server => (
-
-
-
-
-
- {server.owner ? server.owner.substring(0, 2).toUpperCase() : '??'}
-
-
{server.owner}
-
-
{server.name}
-
{server.image}
-
-
-
- {server.status}
-
-
handleManageServer(server)}
- className="!p-2 !min-h-0 !rounded-lg opacity-0 group-hover:opacity-100 transition-opacity"
- title="Detailed Management"
- >
-
-
-
-
-
-
-
-
- {server.cpu}
-
-
-
- {server.ram}
-
-
-
- {server.storage}
-
-
-
-
- ID: {server.id.substring(0, 8)}...
- {new Date(server.created_at).toLocaleDateString()}
-
-
+
))}
)}
diff --git a/src/features/admin/NodeTerminalModal.jsx b/src/features/admin/NodeTerminalModal.jsx
index eb829de..5425bbc 100644
--- a/src/features/admin/NodeTerminalModal.jsx
+++ b/src/features/admin/NodeTerminalModal.jsx
@@ -1,5 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
-import branding from '../../branding';
import { createPortal } from 'react-dom';
import {
Terminal as TerminalIcon,
@@ -12,13 +11,13 @@ import {
Trash2,
Copy,
Check,
- Server,
- Monitor
} from "lucide-react";
import { colors } from '../../theme/colors';
import useWebSocket from '../../hooks/useWebSocket';
import { WS_BASE_URL } from '../../config/api';
+const MAX_TERMINAL_OUTPUT_CHARS = 200_000;
+
const NodeTerminalModal = ({ node, onClose }) => {
const [terminalOutput, setTerminalOutput] = useState('');
const [isConnected, setIsConnected] = useState(false);
@@ -27,6 +26,10 @@ const NodeTerminalModal = ({ node, onClose }) => {
const [copied, setCopied] = useState(false);
const outputRef = useRef(null);
const token = localStorage.getItem('anchorhost_token');
+ const appendTerminalOutput = useCallback((text) => {
+ if (!text) return;
+ setTerminalOutput((previous) => `${previous}${text}`.slice(-MAX_TERMINAL_OUTPUT_CHARS));
+ }, []);
const {
sendMessage,
@@ -58,12 +61,12 @@ const NodeTerminalModal = ({ node, onClose }) => {
const connectTerminal = useCallback(() => {
if (!node || !sendMessage) return;
setIsLoading(true);
- setTerminalOutput(prev => prev + `\r\n\x1b[36m[${branding.name}]\x1b[0m Requesting shell session on node \x1b[1m${node.name}\x1b[0m...\r\n`);
+ appendTerminalOutput(`\r\nConnecting to ${node.name}...\r\n`);
sendMessage({
type: 'node_terminal_connect',
nodeName: node.name
});
- }, [node, sendMessage]);
+ }, [appendTerminalOutput, node, sendMessage]);
const disconnectTerminal = useCallback(() => {
if (!node || !sendMessage) return;
@@ -91,14 +94,19 @@ const NodeTerminalModal = ({ node, onClose }) => {
if (data.type === 'node_terminal_output') {
if (!isConnected) setIsConnected(true);
setIsLoading(false);
- setTerminalOutput(prev => prev + cleanOutput(data.output || ''));
- } else if (data.type === 'error' && data.nodeName === node.name) {
- setTerminalOutput(prev => prev + `\r\n\x1b[31m[Error] ${data.error}\x1b[0m\r\n`);
+ appendTerminalOutput(cleanOutput(data.output || ''));
+ } else if (data.type === 'node_terminal_error') {
+ setIsConnected(false);
setIsLoading(false);
+ appendTerminalOutput(cleanOutput(data.output || '\r\nUnable to open a host login session.\r\n'));
+ } else if (data.type === 'node_terminal_disconnected') {
+ setIsConnected(false);
+ setIsLoading(false);
+ appendTerminalOutput(cleanOutput(data.output || '\r\nHost session closed.\r\n'));
}
}
}
- }, [lastJsonMessage, node.name, isConnected, cleanOutput]);
+ }, [lastJsonMessage, node.name, isConnected, cleanOutput, appendTerminalOutput]);
const handleTerminalInput = (input) => {
if (!isConnected || !sendMessage) return;
@@ -116,7 +124,10 @@ const NodeTerminalModal = ({ node, onClose }) => {
handleTerminalInput('\r');
} else if (e.key === 'Backspace') {
e.preventDefault();
- handleTerminalInput('\b');
+ handleTerminalInput('\x7f');
+ } else if (e.key === 'Delete') {
+ e.preventDefault();
+ handleTerminalInput('\x1b[3~');
} else if (e.key === 'Tab') {
e.preventDefault();
handleTerminalInput('\t');
@@ -157,6 +168,15 @@ const NodeTerminalModal = ({ node, onClose }) => {
}
};
+ const handlePaste = (e) => {
+ if (!isConnected) return;
+ const pastedText = e.clipboardData.getData('text/plain');
+ if (pastedText) {
+ e.preventDefault();
+ handleTerminalInput(pastedText);
+ }
+ };
+
const ansiToStyle = {
0: { color: 'inherit', fontWeight: 'normal', textDecoration: 'none' },
1: { fontWeight: 'bold' },
@@ -210,124 +230,98 @@ const NodeTerminalModal = ({ node, onClose }) => {
};
const modalContent = (
-
+
- {/* Header */}
-
-
-
-
-
-
-
-
- Host terminal · {node.name}
-
-
- {node.role || 'Node'}
-
-
-
-
-
-
- {isConnected ? 'Connected' : isLoading ? 'Connecting...' : 'Disconnected'}
-
-
- {node.os?.image && (
-
-
- {node.os.image}
-
- )}
- {node.specs?.cpu && (
-
-
- {node.specs.cpu} / {node.specs.ram}
-
- )}
-
-
+
{/* Terminal Screen */}
outputRef.current?.focus()}
onKeyDown={handleKeyDown}
- aria-label={`Interactive root terminal for node ${node.name}. Click here and type commands.`}
+ onPaste={handlePaste}
+ aria-label={`Host login terminal for ${node.name}`}
role="textbox"
aria-multiline="true"
aria-readonly="false"
- className="flex-1 p-4 md:p-6 font-mono text-xs md:text-[13px] overflow-y-auto custom-scrollbar bg-black text-gray-200 cursor-text select-none whitespace-pre-wrap break-all leading-relaxed outline-none focus-visible:ring-2 focus-visible:ring-primary"
+ className="min-h-0 flex-1 overflow-auto bg-black p-3 font-mono text-xs leading-5 text-gray-200 outline-none focus-visible:ring-2 focus-visible:ring-primary sm:p-4 sm:text-[13px] sm:leading-6"
tabIndex={0}
autoFocus
>
{terminalOutput ? parseAnsi(terminalOutput) : (
-
- Initializing interactive shell session to node {node.name}...
+
+ {isLoading ? `Connecting to ${node.name}…` : 'Waiting for the host login prompt…'}
)}
{isConnected && (
@@ -335,18 +329,12 @@ const NodeTerminalModal = ({ node, onClose }) => {
)}
- {/* Footer info banner */}
-
-
- root@{node.name}:~#
- | Privileged host shell
-
-
- Ctrl+C: Cancel
- Ctrl+L: Clear
- Ctrl+D: Exit
-
-
+
);
diff --git a/src/features/servers/ServerCard.jsx b/src/features/servers/ServerCard.jsx
index 2433299..4cf5f88 100644
--- a/src/features/servers/ServerCard.jsx
+++ b/src/features/servers/ServerCard.jsx
@@ -11,6 +11,7 @@ import {
Play,
Server,
Square,
+ Settings,
Terminal,
} from 'lucide-react';
import { ActionButton, getServerStatusInfo } from '../../components/DashboardComponents.jsx';
@@ -47,6 +48,8 @@ export default function ServerCard({
onConvertDemo,
onOpenConsole,
onServerAction,
+ owner,
+ isAdminView = false,
}) {
const statusInfo = getServerStatusInfo(server.status);
const { icon: ServerIcon, color: serverColor } = serverTypeIcons[server.type] || serverTypeIcons.vps;
@@ -95,6 +98,7 @@ export default function ServerCard({
{server.name}
{isDemo &&
DEMO}
+ {owner &&
Owner: {owner}
}
{isDatabase ? `${dbType} db` : `${server.type} node`}
{isDemo && timeRemaining && (
@@ -152,10 +156,10 @@ export default function ServerCard({
) : (
<>
onOpenConsole(server)} className="flex-1 !min-h-0 !py-3">
-
- Console
+ {isAdminView ? : }
+ {isAdminView ? 'Manage' : 'Console'}
- {isDemo && (
+ {isDemo && !isAdminView && (
onConvertDemo(server.id)}
diff --git a/src/index.css b/src/index.css
index be4d1e1..fdd6555 100644
--- a/src/index.css
+++ b/src/index.css
@@ -478,6 +478,7 @@ body > div:not(#root) :is(button, a, input, textarea, select):focus-visible { ou
.ah-server-card-heading { min-width: 0; flex: 1; }
.ah-server-card-heading > .flex { min-width: 0; align-items: center; }
.ah-server-name { min-width: 0; max-width: 100%; overflow: hidden; color: var(--ah-emphasis); font-size: 17px; font-weight: 800; line-height: 1.25; letter-spacing: -.02em; text-overflow: ellipsis; white-space: nowrap; }
+.ah-server-owner { overflow: hidden; margin: 4px 0 0; color: var(--ah-text); font-size: 12px; font-weight: 650; line-height: 1.3; text-overflow: ellipsis; white-space: nowrap; }
.ah-server-demo-tag { flex: 0 0 auto; padding: 2px 6px; border: 1px solid var(--ah-warning); background: var(--ah-background); color: var(--ah-warning); font-size: 10px; font-weight: 800; letter-spacing: .04em; }
.ah-server-type-label { overflow: hidden; margin: 5px 0 0; color: var(--ah-muted); font-size: 11px; font-weight: 700; line-height: 1.3; text-overflow: ellipsis; text-transform: uppercase; letter-spacing: .035em; }
.ah-server-demo-time { display: inline-flex; align-items: center; gap: 6px; width: fit-content; margin-top: 8px; padding: 4px 7px; border: 1px solid var(--ah-warning); background: var(--ah-background); }