fix(admin): repair management views and harden plugin handling
Build and publish Docker image / build-and-publish (push) Successful in 14s

This commit is contained in:
wfalkowski committed 2026-10-20 17:17:00 +00:00
1 parent d8862c9611
commit b7b36b1355
7 files changed
+92 -50

No files matched your search

+15 -10
View File
@@ -145,7 +145,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
demo_servers_enabled: Boolean(data.settings.demo_servers_enabled),
new_user_credits: parseInt(data.settings.new_user_credits, 10) || 0
});
showAlert('Success', 'Panel settings updated successfully', 'success');
showAlert('Settings saved', 'The panel settings are now active.', 'success');
} else {
showAlert('Error', data.error || 'Failed to update panel settings', 'error');
}
@@ -216,7 +216,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
}
}
showAlert('Success', `Pricing updated successfully${postNotice ? ' and announcement posted' : ''}`, 'success');
showAlert('Pricing saved', `New prices are active${postNotice ? ', and the announcement is posted.' : '.'}`, 'success');
setPostNotice(false); // Reset checkbox
} else {
showAlert('Error', data.error || 'Failed to update pricing', 'error');
@@ -529,7 +529,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
if (selectedUser?.id === userId) {
setSelectedUser(prev => ({ ...prev, role: isBanned ? 'user' : 'banned' }));
}
showAlert('Success', `User ${isBanned ? 'unbanned' : 'banned'} successfully`, 'success');
showAlert('User access updated', `This account is ${isBanned ? 'active' : 'suspended'}.`, 'success');
} else {
showAlert('Error', data.error || 'Operation failed', 'error');
}
@@ -551,7 +551,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
});
const data = await response.json();
if (data.success) {
showAlert('Success', 'User deleted successfully', 'success');
showAlert('User deleted', 'The account has been removed.', 'success');
fetchUsers();
setSelectedUser(null);
} else {
@@ -579,7 +579,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
});
const data = await response.json();
if (data.success) {
showAlert('Success', 'Password reset successfully', 'success');
showAlert('Password reset', 'The new password is ready to use.', 'success');
setNewPassword('');
} else {
showAlert('Error', data.error || 'Failed to reset password', 'error');
@@ -604,7 +604,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
});
const data = await response.json();
if (data.success) {
showAlert('Success', '2FA disabled successfully', 'success');
showAlert('Two-factor authentication disabled', 'The user can sign in without a verification code.', 'success');
// Update local state to reflect change (optional, depends if we get data back)
} else {
showAlert('Error', data.error || 'Failed to disable 2FA', 'error');
@@ -639,7 +639,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
));
setSelectedUser(prev => ({ ...prev, credits: data.new_credits }));
setCreditAmount('');
showAlert('Success', 'Credits updated successfully', 'success');
showAlert('Credits updated', 'The new balance is available to the user.', 'success');
fetchUserHistory(selectedUser.id); // Refresh history
} else {
showAlert('Error', data.error || 'Failed to update credits', 'error');
@@ -832,20 +832,25 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
</ActionButton>
</div>
<p className="mb-5 max-w-3xl text-sm leading-relaxed text-dash-textMuted">
Review every server across user accounts. Select a server to open its controls, or refresh the list to check for recent changes.
</p>
<div>
{isLoadingAllServers ? (
<DashboardLoadingSpinner label="Loading servers..." />
) : allServers.length === 0 ? (
<div className="text-center py-20 rounded-2xl border border-dashed border-dash-border bg-white/[0.01]">
<div className="text-center py-20 border-2 border-dashed border-dash-border bg-dash-surfaceElevated">
<Server className="w-12 h-12 mx-auto mb-4 opacity-20 text-dash-textMuted" />
<p className="font-medium text-dash-textMuted">No servers found in the system.</p>
<p className="font-bold text-dash-text">No servers to manage</p>
<p className="mt-2 text-sm text-dash-textMuted">Servers appear here after an account creates one.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{allServers.map(server => (
<div
key={server.id}
className="group relative rounded-2xl border border-dash-border bg-dash-surfaceElevated/30 p-5 transition-all duration-300 hover:border-primary/40 hover:bg-dash-surfaceElevated/50"
className="group relative border-2 border-dash-border bg-dash-surfaceElevated p-5 shadow-[5px_5px_0_0_rgba(0,0,0,0.65)] transition-colors hover:border-primary"
>
<div className="flex justify-between items-start mb-4">
<div className="min-w-0">
+39 -24
View File
@@ -26,7 +26,6 @@ const NodeTerminalModal = ({ node, onClose }) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const [copied, setCopied] = useState(false);
const outputRef = useRef(null);
const inputRef = useRef(null);
const token = localStorage.getItem('anchorhost_token');
const {
@@ -113,35 +112,47 @@ const NodeTerminalModal = ({ node, onClose }) => {
const handleKeyDown = (e) => {
if (!isConnected) return;
if (e.key === 'Enter') {
e.preventDefault();
handleTerminalInput('\r');
} else if (e.key === 'Backspace') {
e.preventDefault();
handleTerminalInput('\b');
} else if (e.key === 'Tab') {
e.preventDefault();
handleTerminalInput('\t');
} else if (e.key === 'Escape') {
e.preventDefault();
handleTerminalInput('\x1b');
} else if (e.ctrlKey && e.key === 'c') {
} else if (e.ctrlKey && e.key.toLowerCase() === 'c') {
e.preventDefault();
handleTerminalInput('\x03');
} else if (e.ctrlKey && e.key === 'd') {
} else if (e.ctrlKey && e.key.toLowerCase() === 'd') {
e.preventDefault();
handleTerminalInput('\x04');
} else if (e.ctrlKey && e.key === 'l') {
} else if (e.ctrlKey && e.key.toLowerCase() === 'l') {
e.preventDefault();
setTerminalOutput('');
handleTerminalInput('\x0c');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
handleTerminalInput('\x1b[A');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
handleTerminalInput('\x1b[B');
} else if (e.key === 'ArrowRight') {
e.preventDefault();
handleTerminalInput('\x1b[C');
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
handleTerminalInput('\x1b[D');
}
};
const handleKeyPress = (e) => {
if (!isConnected) return;
if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
} else if (e.key === 'Home') {
e.preventDefault();
handleTerminalInput('\x1b[H');
} else if (e.key === 'End') {
e.preventDefault();
handleTerminalInput('\x1b[F');
} else if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
handleTerminalInput(e.key);
}
};
@@ -199,24 +210,24 @@ const NodeTerminalModal = ({ node, onClose }) => {
};
const modalContent = (
<div className="fixed inset-0 bg-black/80 z-[9999] flex items-center justify-center p-2 sm:p-4 md:p-6 backdrop-blur-md animate-in fade-in duration-200">
<div className="fixed inset-0 bg-black/90 z-[9999] flex items-center justify-center p-2 sm:p-4 md:p-6 animate-in fade-in duration-200">
<div
className={`bg-[#0d1117] border border-dash-border/80 rounded-2xl shadow-2xl flex flex-col overflow-hidden transition-all duration-300 ${
className={`bg-dash-surface border-2 border-dash-border shadow-[6px_6px_0_0_rgba(0,0,0,0.8)] flex flex-col overflow-hidden transition-all duration-300 ${
isFullscreen ? 'w-full h-full rounded-none' : 'w-full max-w-5xl h-[85vh]'
}`}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-3.5 border-b border-dash-border/40 bg-dash-surfaceElevated/60 backdrop-blur-sm">
<div className="flex items-center justify-between px-5 py-3.5 border-b-2 border-dash-border bg-dash-surfaceElevated">
<div className="flex items-center space-x-3 min-w-0">
<div className="p-2 rounded-xl bg-primary/10 border border-primary/20 text-primary">
<div className="p-2 bg-primary/10 border-2 border-primary/30 text-primary">
<TerminalIcon className="w-5 h-5" />
</div>
<div className="min-w-0">
<div className="flex items-center space-x-2">
<h3 className="font-bold text-sm md:text-base text-dash-text truncate">
Node Shell: {node.name}
Host terminal · {node.name}
</h3>
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border ${
<span className={`px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider border-2 ${
node.role?.includes('Control')
? 'bg-brightMagenta/10 border-brightMagenta/20 text-brightMagenta'
: 'bg-primary/10 border-primary/20 text-primary'
@@ -252,7 +263,7 @@ const NodeTerminalModal = ({ node, onClose }) => {
<button
onClick={connectTerminal}
disabled={isLoading}
className="px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all bg-primary hover:bg-primary/90 text-white flex items-center space-x-1.5 shadow-sm disabled:opacity-50"
className="px-3.5 py-1.5 text-xs font-bold transition-all bg-primary hover:bg-primary/90 text-white flex items-center space-x-1.5 border-2 border-dash-border shadow-[3px_3px_0_0_rgba(0,0,0,0.6)] disabled:opacity-50"
>
{isLoading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Power className="w-3.5 h-3.5" />}
<span>{isLoading ? 'Connecting' : 'Connect'}</span>
@@ -260,7 +271,7 @@ const NodeTerminalModal = ({ node, onClose }) => {
) : (
<button
onClick={disconnectTerminal}
className="px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all bg-brightRed/10 hover:bg-brightRed/20 border border-brightRed/30 text-brightRed flex items-center space-x-1.5"
className="px-3.5 py-1.5 text-xs font-bold transition-all bg-brightRed/10 hover:bg-brightRed/20 border-2 border-brightRed/50 text-brightRed flex items-center space-x-1.5"
>
<Square className="w-3.5 h-3.5 fill-current" />
<span>Disconnect</span>
@@ -304,11 +315,15 @@ const NodeTerminalModal = ({ node, onClose }) => {
{/* Terminal Screen */}
<div
ref={outputRef}
onClick={() => inputRef.current?.focus()}
className="flex-1 p-4 md:p-6 font-mono text-xs md:text-[13px] overflow-y-auto custom-scrollbar bg-black/90 text-gray-200 cursor-text select-text whitespace-pre-wrap break-all leading-relaxed"
tabIndex={0}
onClick={() => outputRef.current?.focus()}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress}
aria-label={`Interactive root terminal for node ${node.name}. Click here and type commands.`}
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"
tabIndex={0}
autoFocus
>
{terminalOutput ? parseAnsi(terminalOutput) : (
<div className="text-dash-textMuted/60 italic">
@@ -321,10 +336,10 @@ const NodeTerminalModal = ({ node, onClose }) => {
</div>
{/* Footer info banner */}
<div className="px-5 py-2.5 border-t border-dash-border/30 bg-dash-surfaceElevated/40 text-[11px] text-dash-textMuted/60 flex items-center justify-between">
<div className="px-5 py-2.5 border-t-2 border-dash-border bg-dash-surfaceElevated text-[11px] text-dash-textMuted flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="font-mono">root@{node.name}:~#</span>
<span className="opacity-40">| Host namespace privileged session</span>
<span className="text-brightYellow">| Privileged host shell</span>
</div>
<div className="hidden sm:flex items-center space-x-4">
<span>Ctrl+C: Cancel</span>
+7 -4
View File
@@ -279,11 +279,14 @@ function PluginDetails() {
{plugin.description && (
<div className="rounded-xl border p-6 mb-6" style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.brightBlack }}>
<h2 className="text-xl font-semibold mb-4" style={{ color: colors.brightWhite }}>Description</h2>
<div
className="prose prose-invert max-w-none"
<p
className="max-w-none whitespace-pre-wrap leading-relaxed"
style={{ color: colors.foreground }}
dangerouslySetInnerHTML={{ __html: plugin.description }}
/>
>
{typeof plugin.description === 'string'
? new DOMParser().parseFromString(plugin.description, 'text/html').body.textContent
: ''}
</p>
</div>
)}
+17 -5
View File
@@ -32,7 +32,19 @@ import {
MinecraftSettings
} from '../../components/DashboardComponents';
const normalizeServerPorts = (ports) => {
if (Array.isArray(ports)) return ports.filter((port) => port && typeof port === 'object');
if (typeof ports !== 'string' || !ports.trim()) return [];
try {
const parsed = JSON.parse(ports);
return Array.isArray(parsed) ? parsed.filter((port) => port && typeof port === 'object') : [];
} catch {
return [];
}
};
const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
const serverPorts = normalizeServerPorts(server?.ports);
const [activeTab, setActiveTab] = useState('overview');
const [isTerminalFullscreen, setIsTerminalFullscreen] = useState(false);
const [serverMetrics, setServerMetrics] = useState({ cpu: { display: '0m', value: 0 }, memory: { display: '0 Mi', value: 0 }, cpu_percentage: 0, memory_percentage: 0 });
@@ -674,12 +686,12 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<p className="text-base font-mono font-bold tracking-tight text-white truncate">
{server.ip || '85.237.179.121'}:{server.ports?.[0]?.external || '---'}
{server.ip || '85.237.179.121'}:{serverPorts[0]?.external || '---'}
</p>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(`${server.ip || '85.237.179.121'}:${server.ports?.[0]?.external || ''}`);
navigator.clipboard.writeText(`${server.ip || '85.237.179.121'}:${serverPorts[0]?.external || ''}`);
showAlert('Copied', 'Primary address copied', 'success');
}}
className="px-3 py-1.5 rounded-lg bg-primary text-white text-[10px] font-black uppercase tracking-widest hover:brightness-110 active:scale-95 transition-all shadow-lg shadow-primary/20"
@@ -698,7 +710,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
</button>
</div>
<div className="grid grid-cols-1 gap-2 max-h-[140px] overflow-y-auto pr-1 custom-scrollbar">
{server.ports && server.ports.map((port, idx) => (
{serverPorts.map((port, idx) => (
<div key={idx} className="flex items-center justify-between p-3 rounded-xl bg-white/[0.03] border border-white/5 hover:border-white/10 transition-all group">
<div className="flex items-center gap-3">
<div className="p-1.5 rounded-lg bg-white/5 text-[9px] font-black text-dash-textMuted border border-white/5">
@@ -953,7 +965,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
</tr>
</thead>
<tbody className="divide-y divide-white/5">
{server.ports && server.ports.map((port, idx) => (
{serverPorts.map((port, idx) => (
<tr key={idx} className="group hover:bg-white/[0.01] transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
@@ -1000,7 +1012,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
))}
</tbody>
</table>
{(!server.ports || server.ports.length === 0) && (
{serverPorts.length === 0 && (
<div className="py-20 text-center">
<NetworkIcon className="w-12 h-12 mx-auto mb-4 opacity-10" />
<p className="text-sm font-bold opacity-20 uppercase tracking-[0.2em]">No network mappings active</p>
+7
View File
@@ -453,8 +453,15 @@ body > div:not(#root) [class*="rounded-"] { border-radius: 2px !important; }
body > div:not(#root) :not(button):not(a)[class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
body > div:not(#root) :is(button, a)[class*="shadow-"] { box-shadow: none !important; transform: none !important; }
body > div:not(#root) [class*="backdrop-blur"] { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
body > div:not(#root) [class*="bg-dash-surfaceElevated/"] { background-color: var(--ah-panel) !important; }
body > div:not(#root) :is(button, a, input, textarea, select):focus-visible { outline: 2px solid var(--ah-accent); outline-offset: 2px; }
.dashboard-app, .admin-app { background: var(--ah-background) !important; color: var(--ah-text); }
.admin-app [class*="backdrop-blur"] { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
.admin-app [class*="rounded-"] { border-radius: 2px !important; }
.admin-app :not(button):not(a)[class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
.admin-app [class*="hover:scale"] { transform: none !important; }
.admin-app :is(button, a, input, textarea, select) { border-radius: 2px; }
.admin-app :is(button, a, input, textarea, select):focus-visible { outline: 2px solid var(--ah-accent); outline-offset: 2px; }
.dashboard-app .bg-dash-surface, .admin-app .bg-dash-surface { background-color: var(--ah-surface); }
.dashboard-app .bg-dash-surfaceElevated, .admin-app .bg-dash-surfaceElevated { background-color: var(--ah-panel); }
.dashboard-app .border-dash-border, .admin-app .border-dash-border { border-color: var(--ah-border); }
+6 -6
View File
@@ -123,7 +123,7 @@ const AdminPage = () => {
<div className="admin-app flex h-screen overflow-hidden bg-dash-surface font-sans selection:bg-primary/30 text-dash-textMuted antialiased">
{/* Sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated/50 backdrop-blur-md pt-[env(safe-area-inset-top,0px)] ${
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated pt-[env(safe-area-inset-top,0px)] ${
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
@@ -257,7 +257,7 @@ const AdminPage = () => {
{/* Main Content Area */}
<main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
{/* Mobile Header */}
<header className="md:hidden flex items-center justify-between p-4 border-b border-dash-border bg-dash-surfaceElevated/80 backdrop-blur-md pt-[calc(1rem+env(safe-area-inset-top,0px))]">
<header className="md:hidden flex items-center justify-between p-4 border-b border-dash-border bg-dash-surfaceElevated pt-[calc(1rem+env(safe-area-inset-top,0px))]">
<div className="flex items-center space-x-3">
<div className="p-2 rounded-lg bg-brightRed/10 border border-brightRed/20 shadow-sm">
<Shield className="w-5 h-5 text-brightRed" />
@@ -283,9 +283,9 @@ const AdminPage = () => {
{/* Alert Modal */}
{alertModal.open && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-[9999] p-4 backdrop-blur-sm animate-in fade-in duration-300">
<div className="rounded-2xl w-full max-w-md border border-dash-border bg-dash-surfaceElevated shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className={`p-6 border-b flex justify-between items-center ${
<div className="fixed inset-0 bg-black/90 flex items-center justify-center z-[9999] p-4 animate-in fade-in duration-200">
<div className="w-full max-w-md border-2 border-dash-border bg-dash-surfaceElevated shadow-[6px_6px_0_0_rgba(0,0,0,0.8)] overflow-hidden animate-in zoom-in-95 duration-200">
<div className={`p-6 border-b-2 flex justify-between items-center ${
alertModal.type === 'error' ? 'border-brightRed/20' :
alertModal.type === 'success' ? 'border-brightGreen/20' : 'border-dash-border'
}`}>
@@ -316,7 +316,7 @@ const AdminPage = () => {
onClick={() => setAlertModal({ ...alertModal, open: false })}
className="w-full sm:w-auto font-bold"
>
Confirm Action
Close
</ActionButton>
</div>
</div>
+1 -1
View File
@@ -1907,7 +1907,7 @@ const handleWebSocketMessage = useCallback((data) => {
className="dashboard-app flex h-screen overflow-hidden bg-dash-surface font-sans selection:bg-primary/30 text-dash-text antialiased"
>
<aside
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-all duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated/50 backdrop-blur-xl pt-[env(safe-area-inset-top,0px)] ${
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-all duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated pt-[env(safe-area-inset-top,0px)] ${
isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>