feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js

- Added main application entry point (app.jsx) and bootstrap file for React setup.
- Created Wiki index page (Index.jsx) for documentation and file management.
- Implemented utility functions for formatting and status handling in utils.js.
- Set up Blade template (app.blade.php) for Inertia.js integration.
- Defined API routes for metrics and terminal access in api.php.
- Established console commands for scheduled tasks in console.php.
- Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php.
- Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments.
- Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js.
- Set up Vite for asset bundling and development server in vite.config.js.
This commit is contained in:
I-Fun
2026-04-07 20:29:32 +07:00
commit c649f055d8
126 changed files with 8122 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { Users, Shield, Plus, Edit, Trash2, UserCheck } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { timeAgo } from '@/lib/utils';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
import { useState } from 'react';
export default function AclIndex({ users, roles }) {
const [tab, setTab] = useState('users');
const userColumns = [
{ header: 'User', accessor: 'name', render: row => (
<div className="flex items-center gap-3">
<img src={row.avatar_url} className="w-8 h-8 rounded-full ring-1 ring-white/20" alt={row.name} />
<div>
<p className="text-sm font-medium text-white">{row.name}</p>
<p className="text-xs text-white/40">{row.email}</p>
</div>
</div>
)},
{ header: 'Auth', accessor: 'auth_source', render: row => (
<span className={cn('text-xs px-2 py-0.5 rounded capitalize',
row.auth_source === 'ldap' ? 'bg-purple-500/15 text-purple-400' : 'bg-white/5 text-white/50'
)}>{row.auth_source}</span>
)},
{ header: 'Roles', key: 'roles', render: row => (
<div className="flex flex-wrap gap-1">
{(row.roles ?? []).map(r => (
<span key={r} className="text-xs px-2 py-0.5 rounded bg-cyan-500/15 text-cyan-400">{r}</span>
))}
</div>
)},
{ header: 'Status', accessor: 'is_active', render: row => (
<StatusBadge status={row.is_active ? 'active' : 'offline'} />
)},
{ header: 'Last Login', accessor: 'last_login_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.last_login_at)}</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex gap-1" onClick={e => e.stopPropagation()}>
<a href={`/acl/users/${row.id}/edit`}><Button size="sm" variant="ghost" icon={Edit} /></a>
<Button size="sm" variant="ghost" icon={Trash2}
onClick={() => confirm('Delete this user?') && router.delete(`/acl/users/${row.id}`)} />
</div>
)},
];
const roleColumns = [
{ header: 'Role', accessor: 'name', render: row => (
<div>
<p className="text-sm font-medium text-white capitalize">{row.name}</p>
<p className="text-xs text-white/30">{row.permissions_count ?? 0} permissions</p>
</div>
)},
{ header: 'Users', key: 'users', render: row => (
<span className="text-sm text-white/60">{row.users_count ?? 0}</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex gap-1">
<a href={`/acl/roles/${row.id}/edit`}><Button size="sm" variant="ghost" icon={Edit} /></a>
</div>
)},
];
return (
<AppLayout title="ACL">
<Head title="Access Control" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Access Control</h2>
<p className="text-sm text-white/40">Users, roles, and permissions management</p>
</div>
<div className="flex gap-2">
{tab === 'users' && <a href="/acl/users/create"><Button icon={Plus}>Add User</Button></a>}
{tab === 'roles' && <a href="/acl/roles/create"><Button icon={Plus}>Add Role</Button></a>}
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Total Users" value={users?.total ?? 0} icon={Users} color="cyan" />
<StatCard title="Active" value={users?.data?.filter(u => u.is_active).length ?? 0} icon={UserCheck} color="green" />
<StatCard title="LDAP Users" value={users?.data?.filter(u => u.auth_source === 'ldap').length ?? 0} icon={Shield} color="purple" />
<StatCard title="Roles" value={roles?.length ?? 0} icon={Shield} color="blue" />
</div>
<Tabs value={tab} onValueChange={setTab}>
<TabsList className="flex gap-1 p-1 bg-white/5 rounded-xl w-fit border border-white/10 mb-4">
{[{ value: 'users', label: 'Users' }, { value: 'roles', label: 'Roles & Permissions' }].map(t => (
<TabsTrigger key={t.value} value={t.value}
className={cn('px-5 py-2 rounded-lg text-sm font-medium transition-all',
tab === t.value ? 'bg-white/10 text-white' : 'text-white/50 hover:text-white'
)}>{t.label}</TabsTrigger>
))}
</TabsList>
<TabsContent value="users">
<GlassCard className="p-5">
<DataTable columns={userColumns} data={users?.data ?? []}
onRowClick={row => router.visit(`/acl/users/${row.id}/edit`)}
emptyMessage="No users found." />
</GlassCard>
</TabsContent>
<TabsContent value="roles">
<GlassCard className="p-5">
<DataTable columns={roleColumns} data={roles ?? []}
emptyMessage="No roles defined." />
</GlassCard>
</TabsContent>
</Tabs>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,93 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { Settings, Users, Activity, Shield, Database, Globe, Server, ChevronRight } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { motion } from 'framer-motion';
import { timeAgo } from '@/lib/utils';
const adminSections = [
{ title: 'General Settings', href: '/admin/settings', icon: Settings, desc: 'App name, theme, session timeout' },
{ title: 'LDAP / SSO', href: '/admin/ldap', icon: Globe, desc: 'Configure LDAP/AD authentication' },
{ title: 'Users', href: '/acl/users', icon: Users, desc: 'Manage user accounts' },
{ title: 'Roles & ACL', href: '/acl/roles', icon: Shield, desc: 'Roles and permission management' },
{ title: 'Audit Log', href: '/admin/audit-log', icon: Activity, desc: 'User activity and system events' },
{ title: 'System Health', href: '/admin/system-health',icon: Server, desc: 'Database, queues, storage status' },
];
export default function AdminIndex({ stats, recent_activity }) {
return (
<AppLayout title="Administration">
<Head title="Administration" />
<div className="space-y-5">
<div>
<h2 className="text-xl font-bold text-white">Administration</h2>
<p className="text-sm text-white/40">System settings, users, and maintenance</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{adminSections.map(s => {
const Icon = s.icon;
return (
<a key={s.href} href={s.href}>
<GlassCard className="p-5 cursor-pointer group" hover glow="cyan">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center group-hover:border-cyan-500/30 group-hover:bg-cyan-500/10 transition-all">
<Icon className="w-5 h-5 text-white/50 group-hover:text-cyan-400 transition-colors" />
</div>
<div>
<p className="text-sm font-medium text-white group-hover:text-cyan-300 transition-colors">{s.title}</p>
<p className="text-xs text-white/30">{s.desc}</p>
</div>
</div>
<ChevronRight className="w-4 h-4 text-white/20 group-hover:text-white/50 transition-colors" />
</div>
</GlassCard>
</a>
);
})}
</div>
{/* System stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
{[
{ label: 'Total Users', value: stats?.users ?? 0 },
{ label: 'Active Sessions', value: stats?.sessions ?? 0 },
{ label: 'Queue Jobs', value: stats?.queued ?? 0 },
{ label: 'DB Size', value: stats?.db_size ?? '—' },
].map(s => (
<GlassCard key={s.label} className="p-4 text-center">
<p className="text-2xl font-bold text-white">{s.value}</p>
<p className="text-xs text-white/40 mt-0.5">{s.label}</p>
</GlassCard>
))}
</div>
{/* Recent activity */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Recent Activity</h3>
<a href="/admin/audit-log"><Button size="sm" variant="ghost">View All →</Button></a>
</div>
<div className="space-y-2">
{(recent_activity ?? []).map((log, i) => (
<div key={i} className="flex items-start gap-3 py-2 border-b border-white/[0.05] last:border-0">
<div className="w-7 h-7 rounded-full bg-white/5 border border-white/10 flex items-center justify-center text-xs shrink-0">
{log.causer?.name?.charAt(0)?.toUpperCase() ?? '?'}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-white/80">{log.description}</p>
<p className="text-xs text-white/30">{log.causer?.name ?? 'System'} · {timeAgo(log.created_at)}</p>
</div>
</div>
))}
{(!recent_activity || recent_activity.length === 0) && (
<p className="text-sm text-white/30 text-center py-4">No recent activity</p>
)}
</div>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,146 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, useForm, router } from '@inertiajs/react';
import { useState } from 'react';
import { Globe, CheckCircle, XCircle, RefreshCw } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
function Input({ className, ...props }) {
return <input className={cn('w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/60 transition-all', className)} {...props} />;
}
function Field({ label, desc, children }) {
return (
<div className="flex items-start justify-between gap-8 py-4 border-b border-white/[0.06] last:border-0">
<div className="flex-1">
<p className="text-sm font-medium text-white">{label}</p>
{desc && <p className="text-xs text-white/40 mt-0.5">{desc}</p>}
</div>
<div className="w-72 shrink-0">{children}</div>
</div>
);
}
export default function LdapConfig({ config }) {
const { data, setData, put, processing } = useForm({
host: config?.host ?? '',
port: config?.port ?? 389,
use_ssl: config?.use_ssl ?? false,
use_tls: config?.use_tls ?? false,
base_dn: config?.base_dn ?? '',
bind_dn: config?.bind_dn ?? '',
bind_password: '',
user_search_base: config?.user_search_base ?? '',
user_filter: config?.user_filter ?? '(sAMAccountName={username})',
admin_group_dn: config?.admin_group_dn ?? '',
is_active: config?.is_active ?? false,
});
const [testResult, setTestResult] = useState(null);
const [testing, setTesting] = useState(false);
const [syncing, setSyncing] = useState(false);
const test = async () => {
setTesting(true);
setTestResult(null);
try {
const res = await fetch('/admin/ldap/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify(data),
});
setTestResult(await res.json());
} catch (e) {
setTestResult({ success: false, message: e.message });
} finally {
setTesting(false);
}
};
const sync = () => {
setSyncing(true);
router.post('/admin/ldap/sync', {}, { onFinish: () => setSyncing(false) });
};
return (
<AppLayout title="LDAP Configuration">
<Head title="LDAP" />
<div className="max-w-3xl space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">LDAP / Active Directory</h2>
<p className="text-sm text-white/40">Integrate with your directory service for SSO</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" icon={RefreshCw} loading={syncing} onClick={sync}>Sync Users</Button>
</div>
</div>
<form onSubmit={e => { e.preventDefault(); put('/admin/ldap'); }}>
<div className="space-y-4">
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Connection</h3>
<Field label="Enable LDAP" desc="Allow users to authenticate via LDAP">
<label className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={data.is_active} onChange={e => setData('is_active', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-sm text-white/70">Enabled</span>
</label>
</Field>
<Field label="LDAP Host">
<Input value={data.host} onChange={e => setData('host', e.target.value)} placeholder="ldap.company.com" />
</Field>
<Field label="Port">
<Input type="number" value={data.port} onChange={e => setData('port', parseInt(e.target.value))} />
</Field>
<Field label="Encryption">
<div className="flex gap-3">
{[['use_ssl', 'LDAPS (SSL)'], ['use_tls', 'STARTTLS']].map(([k, l]) => (
<label key={k} className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={data[k]} onChange={e => setData(k, e.target.checked)}
className="w-3.5 h-3.5 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-xs text-white/60">{l}</span>
</label>
))}
</div>
</Field>
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Bind / Authentication</h3>
<Field label="Base DN"><Input value={data.base_dn} onChange={e => setData('base_dn', e.target.value)} placeholder="DC=company,DC=com" /></Field>
<Field label="Bind DN"><Input value={data.bind_dn} onChange={e => setData('bind_dn', e.target.value)} placeholder="CN=ldap-reader,CN=Users,DC=company,DC=com" /></Field>
<Field label="Bind Password"><Input type="password" value={data.bind_password} onChange={e => setData('bind_password', e.target.value)} placeholder="Leave blank to keep existing" /></Field>
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">User Search</h3>
<Field label="User Search Base"><Input value={data.user_search_base} onChange={e => setData('user_search_base', e.target.value)} placeholder="CN=Users,DC=company,DC=com" /></Field>
<Field label="User Filter" desc="Use {username} as placeholder">
<Input value={data.user_filter} onChange={e => setData('user_filter', e.target.value)} />
</Field>
<Field label="Admin Group DN" desc="Members of this group get admin role">
<Input value={data.admin_group_dn} onChange={e => setData('admin_group_dn', e.target.value)} placeholder="CN=Infra-Admins,CN=Groups,..." />
</Field>
</GlassCard>
{testResult && (
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}
className={cn('flex items-center gap-3 p-3 rounded-xl border text-sm',
testResult.success ? 'bg-green-500/10 border-green-500/30 text-green-300' : 'bg-red-500/10 border-red-500/30 text-red-300'
)}>
{testResult.success ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
{testResult.message}
</motion.div>
)}
<div className="flex items-center justify-between pt-2">
<Button type="button" variant="secondary" loading={testing} onClick={test}>Test Connection</Button>
<Button type="submit" loading={processing}>Save Configuration</Button>
</div>
</div>
</form>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,115 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, useForm } from '@inertiajs/react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { cn } from '@/lib/utils';
function Field({ label, desc, error, children }) {
return (
<div className="flex items-start justify-between gap-8 py-4 border-b border-white/[0.06] last:border-0">
<div className="flex-1">
<p className="text-sm font-medium text-white">{label}</p>
{desc && <p className="text-xs text-white/40 mt-0.5">{desc}</p>}
{error && <p className="text-xs text-red-400 mt-1">{error}</p>}
</div>
<div className="w-72 shrink-0">{children}</div>
</div>
);
}
function Input({ className, ...props }) {
return (
<input className={cn(
'w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/20',
'focus:outline-none focus:border-cyan-500/60 transition-all',
className
)} {...props} />
);
}
function Select({ className, children, ...props }) {
return (
<select className={cn(
'w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white',
'focus:outline-none focus:border-cyan-500/60 transition-all',
'[&>option]:bg-[#0f1f35]',
className
)} {...props}>{children}</select>
);
}
export default function AdminSettings({ settings }) {
const { data, setData, put, processing, errors } = useForm({
'app.name': settings?.['app.name'] ?? 'Infra Manager',
'app.theme': settings?.['app.theme'] ?? 'dark',
'security.session_timeout': settings?.['security.session_timeout'] ?? 480,
'security.mfa_required': settings?.['security.mfa_required'] ?? false,
'mail.driver': settings?.['mail.driver'] ?? 'log',
'mail.from': settings?.['mail.from'] ?? '',
});
return (
<AppLayout title="Settings">
<Head title="Settings" />
<div className="max-w-3xl space-y-5">
<div>
<h2 className="text-xl font-bold text-white">General Settings</h2>
<p className="text-sm text-white/40">Application-wide configuration</p>
</div>
<form onSubmit={e => { e.preventDefault(); put('/admin/settings'); }}>
<div className="space-y-4">
{/* App */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Application</h3>
<Field label="App Name" desc="Displayed in the browser tab and sidebar">
<Input value={data['app.name']} onChange={e => setData('app.name', e.target.value)} />
</Field>
<Field label="Theme" desc="Interface color scheme">
<Select value={data['app.theme']} onChange={e => setData('app.theme', e.target.value)}>
<option value="dark">Dark (Default)</option>
<option value="darker">Darker</option>
</Select>
</Field>
</GlassCard>
{/* Security */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Security</h3>
<Field label="Session Timeout" desc="Minutes before inactive users are logged out">
<Input type="number" value={data['security.session_timeout']} onChange={e => setData('security.session_timeout', e.target.value)} />
</Field>
<Field label="Require MFA" desc="Force MFA for all users">
<label className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={data['security.mfa_required']}
onChange={e => setData('security.mfa_required', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-sm text-white/70">Enabled</span>
</label>
</Field>
</GlassCard>
{/* Mail */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Email / Notifications</h3>
<Field label="Mail Driver">
<Select value={data['mail.driver']} onChange={e => setData('mail.driver', e.target.value)}>
<option value="log">Log (dev only)</option>
<option value="smtp">SMTP</option>
<option value="sendmail">Sendmail</option>
</Select>
</Field>
<Field label="From Address" desc="Default sender email for notifications">
<Input type="email" value={data['mail.from']} onChange={e => setData('mail.from', e.target.value)} placeholder="noreply@example.com" />
</Field>
</GlassCard>
<div className="flex justify-end">
<Button type="submit" loading={processing}>Save Settings</Button>
</div>
</div>
</form>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,154 @@
import { useForm, Head } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Zap, Lock, Mail, Eye, EyeOff, AlertCircle } from 'lucide-react';
import { useState } from 'react';
import { Button } from '@/Components/ui/Button';
import { cn } from '@/lib/utils';
export default function Login({ ldapEnabled }) {
const { data, setData, post, processing, errors } = useForm({
email: '',
password: '',
remember: false,
});
const [showPass, setShowPass] = useState(false);
const submit = (e) => {
e.preventDefault();
post('/login');
};
return (
<>
<Head title="Sign In" />
<div className="min-h-screen bg-[#050a0e] flex items-center justify-center p-4 relative overflow-hidden">
{/* Background effects */}
<div className="absolute inset-0 bg-grid-pattern opacity-50" />
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-cyan-500/5 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-purple-500/5 rounded-full blur-3xl" />
{/* Meteors */}
{Array.from({ length: 8 }).map((_, i) => (
<motion.div
key={i}
className="absolute w-0.5 h-16 bg-gradient-to-b from-cyan-400/60 to-transparent rounded-full"
style={{ top: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, rotate: -45 }}
animate={{ x: [0, -300], y: [0, 300], opacity: [0, 1, 0] }}
transition={{ duration: 2 + Math.random() * 3, repeat: Infinity, delay: i * 0.7, ease: 'linear' }}
/>
))}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="w-full max-w-md relative z-10"
>
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-cyan-500 to-purple-600 mb-4 shadow-[0_0_40px_rgba(0,212,255,0.3)]">
<Zap className="w-8 h-8 text-white" />
</div>
<h1 className="text-2xl font-bold text-white">Infra Manager</h1>
<p className="text-sm text-white/40 mt-1">Unified Infrastructure Control Plane</p>
</div>
{/* Card */}
<div className="relative rounded-2xl border border-white/10 bg-white/[0.03] backdrop-blur-xl p-8">
{/* Gradient border glow */}
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-cyan-500/10 via-transparent to-purple-500/10 pointer-events-none" />
<form onSubmit={submit} className="relative space-y-5">
<div>
<label className="block text-xs font-medium text-white/60 mb-1.5">Email Address</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
<input
type="email"
value={data.email}
onChange={e => setData('email', e.target.value)}
placeholder="admin@infra.local"
required
className={cn(
'w-full pl-10 pr-4 py-2.5 bg-white/5 border rounded-xl text-sm text-white placeholder:text-white/20',
'focus:outline-none focus:border-cyan-500/60 focus:bg-white/[0.07] transition-all',
errors.email ? 'border-red-500/60' : 'border-white/10'
)}
/>
</div>
{errors.email && (
<p className="flex items-center gap-1 mt-1.5 text-xs text-red-400">
<AlertCircle className="w-3 h-3" />{errors.email}
</p>
)}
</div>
<div>
<label className="block text-xs font-medium text-white/60 mb-1.5">Password</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
<input
type={showPass ? 'text' : 'password'}
value={data.password}
onChange={e => setData('password', e.target.value)}
placeholder="••••••••"
required
className={cn(
'w-full pl-10 pr-10 py-2.5 bg-white/5 border rounded-xl text-sm text-white placeholder:text-white/20',
'focus:outline-none focus:border-cyan-500/60 focus:bg-white/[0.07] transition-all',
errors.password ? 'border-red-500/60' : 'border-white/10'
)}
/>
<button
type="button"
onClick={() => setShowPass(s => !s)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
>
{showPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={data.remember}
onChange={e => setData('remember', e.target.checked)}
className="w-3.5 h-3.5 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0 focus:ring-offset-0"
/>
<span className="text-xs text-white/50">Remember me</span>
</label>
</div>
<Button
type="submit"
variant="primary"
size="lg"
loading={processing}
className="w-full"
>
Sign In
</Button>
{ldapEnabled && (
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-white/10" />
</div>
<div className="relative flex justify-center text-xs">
<span className="bg-[#07111d] px-2 text-white/30">or</span>
</div>
</div>
)}
</form>
</div>
<p className="text-center text-xs text-white/20 mt-6">
Default: admin@infra.local / Admin@123456
</p>
</motion.div>
</div>
</>
);
}

View File

@@ -0,0 +1,114 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { HardDrive, Plus, Download, RotateCcw, Calendar, CheckCircle, XCircle, Clock } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { formatBytes, timeAgo } from '@/lib/utils';
export default function BackupIndex({ backups, policies, stats }) {
const backupColumns = [
{ header: 'Target', accessor: 'target_name', render: row => (
<div>
<p className="text-sm text-white font-medium">{row.target_name}</p>
<p className="text-xs text-white/30 capitalize">{row.target_type}</p>
</div>
)},
{ header: 'Size', accessor: 'size_bytes', render: row => (
<span className="text-sm text-white/70 font-mono">{row.size_bytes ? formatBytes(row.size_bytes) : '—'}</span>
)},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: 'Created', accessor: 'created_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.created_at)}</span>
)},
{ header: 'Expires', accessor: 'expires_at', render: row => (
row.expires_at
? <span className={`text-xs ${new Date(row.expires_at) < new Date() ? 'text-red-400' : 'text-white/40'}`}>{timeAgo(row.expires_at)}</span>
: <span className="text-xs text-white/20">Never</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex gap-1" onClick={e => e.stopPropagation()}>
{row.status === 'success' && (
<>
<Button size="sm" variant="ghost" icon={Download} title="Download" />
<Button size="sm" variant="ghost" icon={RotateCcw} title="Restore"
onClick={() => confirm('Restore from this backup?') && router.post(`/backups/${row.id}/restore`)} />
</>
)}
<Button size="sm" variant="ghost" icon={XCircle} className="hover:text-red-400"
onClick={() => confirm('Delete this backup?') && router.delete(`/backups/${row.id}`)} />
</div>
)},
];
const policyColumns = [
{ header: 'Policy', accessor: 'name', render: row => (
<div>
<p className="text-sm text-white font-medium">{row.name}</p>
<p className="text-xs text-white/30 capitalize">{row.target_type} · {row.schedule}</p>
</div>
)},
{ header: 'Retention', accessor: 'retention_value', render: row => (
<span className="text-sm text-white/70">{row.retention_value} {row.retention_type === 'count' ? 'backups' : 'days'}</span>
)},
{ header: 'Storage', accessor: 'storage_type', render: row => (
<span className="text-xs px-2 py-0.5 rounded bg-white/5 text-white/60 uppercase">{row.storage_type}</span>
)},
{ header: 'Status', accessor: 'is_active', render: row => (
<StatusBadge status={row.is_active ? 'active' : 'stopped'} />
)},
];
return (
<AppLayout title="Backup & DR">
<Head title="Backup & DR" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Backup & Disaster Recovery</h2>
<p className="text-sm text-white/40">Manage backups, schedules, and restore points</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" icon={Calendar}>Schedules</Button>
<Button icon={Plus}>Create Policy</Button>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Total Backups" value={stats?.total ?? 0} icon={HardDrive} color="cyan" />
<StatCard title="Successful" value={stats?.success ?? 0} icon={CheckCircle} color="green" />
<StatCard title="Failed" value={stats?.failed ?? 0} icon={XCircle} color="red" />
<StatCard title="Storage Used" value={stats?.total_size ?? '0 B'} icon={HardDrive} color="purple" />
</div>
{/* DR Status */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{[
{ label: 'RTO', value: '< 4h', desc: 'Recovery Time Objective', color: 'text-green-400' },
{ label: 'RPO', value: '< 1h', desc: 'Recovery Point Objective', color: 'text-cyan-400' },
{ label: 'Last DR Test', value: 'N/A', desc: 'Last tested', color: 'text-white/40' },
].map(d => (
<GlassCard key={d.label} className="p-5 text-center">
<p className="text-xs text-white/40 uppercase tracking-wider mb-1">{d.label}</p>
<p className={`text-3xl font-bold ${d.color}`}>{d.value}</p>
<p className="text-xs text-white/30 mt-1">{d.desc}</p>
</GlassCard>
))}
</div>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-4">Backup Policies</h3>
<DataTable columns={policyColumns} data={policies ?? []}
emptyMessage="No backup policies. Create one to start protecting your infrastructure." />
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-4">Backup History</h3>
<DataTable columns={backupColumns} data={backups?.data ?? []}
emptyMessage="No backups found." />
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,150 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { Cloud, Plus, RefreshCw, Server, HardDrive, Database, Globe } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { timeAgo } from '@/lib/utils';
import { motion } from 'framer-motion';
import { useState } from 'react';
const PROVIDERS = {
aws: { label: 'Amazon Web Services', color: '#FF9900', icon: '☁' },
alibaba: { label: 'Alibaba Cloud', color: '#FF6A00', icon: '☁' },
tencent: { label: 'Tencent Cloud', color: '#00A3FF', icon: '☁' },
gcp: { label: 'Google Cloud', color: '#4285F4', icon: '☁' },
azure: { label: 'Microsoft Azure', color: '#0078D4', icon: '☁' },
digitalocean: { label: 'DigitalOcean', color: '#0080FF', icon: '☁' },
vultr: { label: 'Vultr', color: '#007BFC', icon: '☁' },
hetzner: { label: 'Hetzner', color: '#D50C2D', icon: '☁' },
};
const RESOURCE_ICONS = { ec2: Server, s3: HardDrive, rds: Database, vpc: Globe };
export default function CloudIndex({ accounts, resources, stats }) {
const [syncLoading, setSyncLoading] = useState(null);
const [selectedAccount, setSelectedAccount] = useState(null);
const sync = (id) => {
setSyncLoading(id);
router.post(`/cloud/accounts/${id}/sync`, {}, { onFinish: () => setSyncLoading(null) });
};
const accountColumns = [
{ header: 'Account', accessor: 'name', render: row => {
const p = PROVIDERS[row.provider] ?? {};
return (
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center text-base font-bold"
style={{ color: p.color }}>{row.provider?.toUpperCase().slice(0, 2)}</div>
<div>
<p className="font-medium text-white">{row.name}</p>
<p className="text-xs" style={{ color: p.color ?? '#fff' }}>{p.label ?? row.provider}</p>
</div>
</div>
);
}},
{ header: 'Account ID', accessor: 'account_id', render: row => (
<span className="font-mono text-xs text-white/50">{row.account_id ?? '—'}</span>
)},
{ header: 'Region', accessor: 'region', render: row => (
<span className="text-xs text-white/60">{row.region ?? '—'}</span>
)},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: 'Last Sync', accessor: 'last_sync_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.last_sync_at)}</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
<button onClick={() => sync(row.id)} disabled={syncLoading === row.id}
className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-cyan-400 transition-colors">
<RefreshCw className={`w-3.5 h-3.5 ${syncLoading === row.id ? 'animate-spin' : ''}`} />
</button>
</div>
)},
];
const resourceColumns = [
{ header: 'Resource', accessor: 'name', render: row => {
const Icon = RESOURCE_ICONS[row.resource_type] ?? Cloud;
return (
<div className="flex items-center gap-3">
<Icon className="w-4 h-4 text-white/40" />
<div>
<p className="text-sm text-white">{row.name}</p>
<p className="font-mono text-xs text-white/30">{row.resource_id}</p>
</div>
</div>
);
}},
{ header: 'Type', accessor: 'resource_type', render: row => (
<span className="font-mono text-xs px-2 py-0.5 rounded bg-white/5 text-white/60">{row.resource_type}</span>
)},
{ header: 'Region', accessor: 'region', render: row => (
<span className="text-xs text-white/60">{row.region}</span>
)},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: 'Cost/mo', accessor: 'monthly_cost', render: row => (
row.monthly_cost ? <span className="text-xs text-green-400 font-mono">${row.monthly_cost}</span> : <span className="text-xs text-white/20">—</span>
)},
];
return (
<AppLayout title="Cloud">
<Head title="Cloud Management" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Cloud Management</h2>
<p className="text-sm text-white/40">AWS, Alibaba, Tencent, GCP, Azure and more</p>
</div>
<a href="/cloud/accounts/create">
<Button icon={Plus}>Add Account</Button>
</a>
</div>
{/* Provider cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{Object.entries(PROVIDERS).slice(0, 4).map(([key, p]) => {
const count = accounts?.filter(a => a.provider === key).length ?? 0;
return (
<GlassCard key={key} className="p-4">
<div className="flex items-center gap-3">
<div className="text-2xl font-bold" style={{ color: p.color }}>
{key.toUpperCase().slice(0, 2)}
</div>
<div>
<p className="text-sm text-white font-medium">{key.toUpperCase()}</p>
<p className="text-xs text-white/40">{count} account{count !== 1 ? 's' : ''}</p>
</div>
</div>
</GlassCard>
);
})}
</div>
{/* Accounts */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-4">Cloud Accounts</h3>
<DataTable
columns={accountColumns}
data={accounts ?? []}
onRowClick={row => router.visit(`/cloud/accounts/${row.id}`)}
emptyMessage="No cloud accounts. Add your first account to get started."
/>
</GlassCard>
{/* Resources */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-4">Cloud Resources</h3>
<DataTable
columns={resourceColumns}
data={resources?.data ?? []}
emptyMessage="No resources synced yet. Sync a cloud account to see resources."
/>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,131 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Box, Play, Square, RotateCcw, Trash2, Terminal, BarChart2, Plus } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { useState } from 'react';
import { cn } from '@/lib/utils';
export default function ContainerIndex({ containers, endpoints }) {
const [actionLoading, setActionLoading] = useState(null);
const [selectedEndpoint, setSelectedEndpoint] = useState(null);
const action = (containerId, act) => {
setActionLoading(`${containerId}-${act}`);
router.post(`/containers/${containerId}/action`, { action: act }, {
onFinish: () => setActionLoading(null),
});
};
const filtered = selectedEndpoint
? (containers.data ?? []).filter(c => c.endpoint_id === selectedEndpoint)
: (containers.data ?? []);
const columns = [
{ header: 'Container', accessor: 'name', render: row => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-blue-500/10 border border-blue-500/20 flex items-center justify-center">
<Box className="w-4 h-4 text-blue-400" />
</div>
<div>
<p className="font-medium text-white font-mono text-sm">{row.name}</p>
<p className="text-xs text-white/30 truncate max-w-48">{row.image}</p>
</div>
</div>
)},
{ header: 'Status', accessor: 'state', render: row => <StatusBadge status={row.state} /> },
{ header: 'Ports', key: 'ports', render: row => (
<div className="flex flex-wrap gap-1">
{(row.ports ?? []).slice(0, 3).map((p, i) => (
<span key={i} className="font-mono text-xs px-1.5 py-0.5 rounded bg-white/5 text-white/50">
{p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}` : p.PrivatePort}
</span>
))}
{(row.ports ?? []).length > 3 && <span className="text-xs text-white/30">+{row.ports.length - 3}</span>}
</div>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
{row.state === 'running' ? (
<>
<button onClick={() => action(row.id, 'stop')} disabled={actionLoading?.startsWith(row.id)}
className="p-1.5 rounded-lg hover:bg-red-500/20 text-white/40 hover:text-red-400 transition-colors" title="Stop">
<Square className="w-3.5 h-3.5" />
</button>
<button onClick={() => action(row.id, 'restart')} disabled={actionLoading?.startsWith(row.id)}
className="p-1.5 rounded-lg hover:bg-yellow-500/20 text-white/40 hover:text-yellow-400 transition-colors" title="Restart">
<RotateCcw className="w-3.5 h-3.5" />
</button>
</>
) : (
<button onClick={() => action(row.id, 'start')} disabled={actionLoading?.startsWith(row.id)}
className="p-1.5 rounded-lg hover:bg-green-500/20 text-white/40 hover:text-green-400 transition-colors" title="Start">
<Play className="w-3.5 h-3.5" />
</button>
)}
<a href={`/containers/${row.id}/logs`} className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-white transition-colors" title="Logs">
<Terminal className="w-3.5 h-3.5" />
</a>
<a href={`/containers/${row.id}/stats`} className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-white transition-colors" title="Stats">
<BarChart2 className="w-3.5 h-3.5" />
</a>
</div>
)},
];
const running = (containers.data ?? []).filter(c => c.state === 'running').length;
const stopped = (containers.data ?? []).filter(c => c.state !== 'running').length;
return (
<AppLayout title="Containers">
<Head title="Containers" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Containers</h2>
<p className="text-sm text-white/40">Manage Docker containers across all endpoints</p>
</div>
<div className="flex gap-2">
<a href="/stacks"><Button variant="secondary">Stacks</Button></a>
<a href="/stacks/create"><Button icon={Plus}>Deploy Stack</Button></a>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Endpoints" value={endpoints?.length ?? 0} color="cyan" />
<StatCard title="Total" value={containers.total ?? 0} color="blue" />
<StatCard title="Running" value={running} color="green" />
<StatCard title="Stopped" value={stopped} color="red" />
</div>
{/* Endpoint filter tabs */}
{endpoints?.length > 0 && (
<div className="flex items-center gap-2 flex-wrap">
<button onClick={() => setSelectedEndpoint(null)}
className={cn('px-3 py-1.5 rounded-lg text-xs font-medium border transition-all',
!selectedEndpoint ? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400' : 'border-white/10 text-white/40 hover:text-white hover:border-white/20'
)}>All Endpoints</button>
{endpoints.map(e => (
<button key={e.id} onClick={() => setSelectedEndpoint(e.id)}
className={cn('px-3 py-1.5 rounded-lg text-xs font-medium border transition-all',
selectedEndpoint === e.id ? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400' : 'border-white/10 text-white/40 hover:text-white hover:border-white/20'
)}>{e.name}</button>
))}
</div>
)}
<GlassCard className="p-5">
<DataTable
columns={columns}
data={filtered}
onRowClick={row => router.visit(`/containers/${row.id}`)}
emptyMessage="No containers found. Add a Docker endpoint to get started."
/>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,238 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head } from '@inertiajs/react';
import { motion } from 'framer-motion';
import {
Server, Monitor, Box, Cloud, Shield, AlertTriangle,
Activity, TrendingUp, Cpu, MemoryStick, HardDrive, Wifi
} from 'lucide-react';
import { StatCard, GlassCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { ProgressBar } from '@/Components/ui/ProgressBar';
import { timeAgo, getStatusDot } from '@/lib/utils';
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip,
ResponsiveContainer, PieChart, Pie, Cell
} from 'recharts';
const FADE_UP = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } };
const CONTAINER = {
hidden: {},
show: { transition: { staggerChildren: 0.05 } }
};
// Mock chart data - replace with real metrics
const cpuData = Array.from({ length: 20 }, (_, i) => ({
t: `${i}m`, cpu: Math.round(20 + Math.random() * 50), mem: Math.round(40 + Math.random() * 40)
}));
const PROVIDER_COLORS = { aws: '#FF9900', alibaba: '#FF6A00', tencent: '#00A3FF', gcp: '#4285F4', azure: '#0078D4' };
export default function Dashboard({ stats, hypervisors, endpoints, cloud_accounts, recent_alerts }) {
return (
<AppLayout title="Dashboard">
<Head title="Dashboard" />
<motion.div variants={CONTAINER} initial="hidden" animate="show" className="space-y-6">
{/* ── Header ──────────────────────────────────────── */}
<motion.div variants={FADE_UP} className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Infrastructure Overview</h2>
<p className="text-sm text-white/40 mt-0.5">Real-time view across all platforms</p>
</div>
<div className="flex items-center gap-2 text-xs text-white/30">
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
Live
</div>
</motion.div>
{/* ── Top stats ────────────────────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-3">
<StatCard title="Hypervisors" value={stats.hypervisors} subtitle={`${stats.hypervisors_online} online`} icon={Server} color="cyan" />
<StatCard title="Virtual Machines" value={stats.vms} subtitle={`${stats.vms_running} running`} icon={Monitor} color="green" />
<StatCard title="Containers" value={stats.containers} subtitle={`${stats.containers_running} running`} icon={Box} color="purple" />
<StatCard title="Cloud Accounts" value={stats.cloud_accounts} subtitle="Connected" icon={Cloud} color="blue" />
<StatCard title="Critical Alerts" value={stats.critical_alerts} subtitle={`${stats.warning_alerts} warnings`} icon={AlertTriangle} color={stats.critical_alerts > 0 ? 'red' : 'green'} />
<StatCard title="Security Score" value={`${stats.security_score}%`} subtitle="Avg across infra" icon={Shield} color={stats.security_score >= 80 ? 'green' : stats.security_score >= 60 ? 'yellow' : 'red'} />
</motion.div>
{/* ── Charts Row ───────────────────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* CPU / Memory chart */}
<GlassCard className="p-5 lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-semibold text-white">Resource Utilization</h3>
<p className="text-xs text-white/30">Avg CPU & Memory — last 20 min</p>
</div>
<div className="flex items-center gap-4 text-xs">
<span className="flex items-center gap-1.5 text-cyan-400"><span className="w-2 h-0.5 bg-cyan-400 rounded" />CPU</span>
<span className="flex items-center gap-1.5 text-purple-400"><span className="w-2 h-0.5 bg-purple-400 rounded" />Memory</span>
</div>
</div>
<ResponsiveContainer width="100%" height={180}>
<AreaChart data={cpuData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="cpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#00d4ff" stopOpacity={0.3} />
<stop offset="95%" stopColor="#00d4ff" stopOpacity={0} />
</linearGradient>
<linearGradient id="mem" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#9d4edd" stopOpacity={0.3} />
<stop offset="95%" stopColor="#9d4edd" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="t" tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} domain={[0, 100]} />
<Tooltip
contentStyle={{ backgroundColor: '#0f1f35', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '12px' }}
labelStyle={{ color: 'rgba(255,255,255,0.6)' }}
/>
<Area type="monotone" dataKey="cpu" stroke="#00d4ff" strokeWidth={2} fill="url(#cpu)" />
<Area type="monotone" dataKey="mem" stroke="#9d4edd" strokeWidth={2} fill="url(#mem)" />
</AreaChart>
</ResponsiveContainer>
</GlassCard>
{/* VM Status Donut */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-1">VM Status</h3>
<p className="text-xs text-white/30 mb-4">Across all hypervisors</p>
<ResponsiveContainer width="100%" height={140}>
<PieChart>
<Pie data={[
{ name: 'Running', value: stats.vms_running },
{ name: 'Stopped', value: stats.vms_stopped },
{ name: 'Other', value: Math.max(0, stats.vms - stats.vms_running - stats.vms_stopped) },
]}
cx="50%" cy="50%" innerRadius={45} outerRadius={65}
paddingAngle={3} dataKey="value"
>
<Cell fill="#00d4ff" />
<Cell fill="#334155" />
<Cell fill="#9d4edd" />
</Pie>
<Tooltip contentStyle={{ backgroundColor: '#0f1f35', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '12px' }} />
</PieChart>
</ResponsiveContainer>
<div className="space-y-1.5 mt-2">
{[{ label: 'Running', val: stats.vms_running, color: 'bg-cyan-400' },
{ label: 'Stopped', val: stats.vms_stopped, color: 'bg-slate-600' }].map(s => (
<div key={s.label} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2"><span className={`w-2 h-2 rounded-full ${s.color}`} /><span className="text-white/50">{s.label}</span></div>
<span className="text-white font-medium">{s.val}</span>
</div>
))}
</div>
</GlassCard>
</motion.div>
{/* ── Hypervisors & Endpoints ───────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Hypervisors */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Hypervisors</h3>
<a href="/hypervisors" className="text-xs text-cyan-400 hover:text-cyan-300">View all →</a>
</div>
<div className="space-y-2">
{hypervisors?.slice(0, 6).map(h => (
<a key={h.id} href={`/hypervisors/${h.id}`}
className="flex items-center gap-3 p-3 rounded-lg bg-white/[0.02] hover:bg-white/[0.05] border border-white/[0.05] transition-colors group"
>
<span className={`w-2 h-2 rounded-full ${getStatusDot(h.status)} shrink-0`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-white truncate group-hover:text-cyan-300 transition-colors">{h.name}</p>
<p className="text-xs text-white/30 capitalize">{h.type} · {timeAgo(h.last_sync_at)}</p>
</div>
<StatusBadge status={h.status} />
</a>
))}
{(!hypervisors || hypervisors.length === 0) && (
<p className="text-sm text-white/30 text-center py-6">No hypervisors configured</p>
)}
</div>
</GlassCard>
{/* Docker Endpoints */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Docker Endpoints</h3>
<a href="/containers" className="text-xs text-cyan-400 hover:text-cyan-300">View all →</a>
</div>
<div className="space-y-2">
{endpoints?.slice(0, 6).map(e => (
<div key={e.id} className="flex items-center gap-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.05]">
<span className={`w-2 h-2 rounded-full ${getStatusDot(e.status)} shrink-0`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-white truncate">{e.name}</p>
<p className="text-xs text-white/30 capitalize">{e.type}</p>
</div>
<StatusBadge status={e.status} />
</div>
))}
{(!endpoints || endpoints.length === 0) && (
<p className="text-sm text-white/30 text-center py-6">No Docker endpoints</p>
)}
</div>
</GlassCard>
</motion.div>
{/* ── Cloud & Alerts ────────────────────────────────── */}
<motion.div variants={FADE_UP} className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Cloud accounts */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Cloud Accounts</h3>
<a href="/cloud" className="text-xs text-cyan-400 hover:text-cyan-300">Manage →</a>
</div>
<div className="grid grid-cols-2 gap-2">
{cloud_accounts?.map(acc => (
<div key={acc.id} className="flex items-center gap-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.05]">
<div className="w-8 h-8 rounded-lg bg-white/10 flex items-center justify-center text-xs font-bold"
style={{ color: PROVIDER_COLORS[acc.provider] ?? '#fff' }}>
{acc.provider?.toUpperCase().slice(0, 2)}
</div>
<div className="min-w-0">
<p className="text-xs font-medium text-white truncate">{acc.name}</p>
<StatusBadge status={acc.status} className="mt-0.5 text-[10px]" />
</div>
</div>
))}
{(!cloud_accounts || cloud_accounts.length === 0) && (
<p className="col-span-2 text-sm text-white/30 text-center py-6">No cloud accounts</p>
)}
</div>
</GlassCard>
{/* Recent Alerts */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Active Alerts</h3>
<a href="/monitoring/alerts" className="text-xs text-cyan-400 hover:text-cyan-300">View all →</a>
</div>
<div className="space-y-2">
{recent_alerts?.map(alert => (
<div key={alert.id} className="flex items-start gap-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.05]">
<AlertTriangle className={`w-4 h-4 mt-0.5 shrink-0 ${alert.severity === 'critical' ? 'text-red-400' : 'text-yellow-400'}`} />
<div className="min-w-0 flex-1">
<p className="text-sm text-white truncate">{alert.title}</p>
<p className="text-xs text-white/30">{alert.target_name} · {timeAgo(alert.created_at)}</p>
</div>
<StatusBadge status={alert.severity} />
</div>
))}
{(!recent_alerts || recent_alerts.length === 0) && (
<div className="flex flex-col items-center py-6 gap-2">
<Shield className="w-8 h-8 text-green-400/50" />
<p className="text-sm text-white/30">All clear — no active alerts</p>
</div>
)}
</div>
</GlassCard>
</motion.div>
</motion.div>
</AppLayout>
);
}

View File

@@ -0,0 +1,39 @@
import { Head } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Zap, AlertTriangle } from 'lucide-react';
const MESSAGES = {
403: { title: 'Access Denied', desc: 'You do not have permission to access this resource.' },
404: { title: 'Page Not Found', desc: 'The page you are looking for does not exist.' },
500: { title: 'Server Error', desc: 'Something went wrong on our end. Please try again.' },
503: { title: 'Unavailable', desc: 'The service is temporarily unavailable.' },
};
export default function Error({ status }) {
const { title, desc } = MESSAGES[status] ?? { title: 'Error', desc: 'An unexpected error occurred.' };
return (
<>
<Head title={`${status} — ${title}`} />
<div className="min-h-screen bg-[#050a0e] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-grid-pattern opacity-30" />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center relative z-10"
>
<div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-red-500/10 border border-red-500/20 mb-6">
<AlertTriangle className="w-10 h-10 text-red-400" />
</div>
<p className="text-8xl font-black text-white/10 mb-4">{status}</p>
<h1 className="text-2xl font-bold text-white mb-2">{title}</h1>
<p className="text-white/50 mb-8">{desc}</p>
<a href="/" className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-cyan-500 text-black font-semibold hover:bg-cyan-400 transition-colors">
<Zap className="w-4 h-4" />
Back to Dashboard
</a>
</motion.div>
</div>
</>
);
}

View File

@@ -0,0 +1,163 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, useForm, router } from '@inertiajs/react';
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Server, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { cn } from '@/lib/utils';
const TYPES = [
{ value: 'vcenter', label: 'VMware vCenter', port: 443 },
{ value: 'esxi', label: 'VMware ESXi', port: 443 },
{ value: 'proxmox', label: 'Proxmox VE', port: 8006 },
{ value: 'hyper-v', label: 'Hyper-V', port: 5985 },
{ value: 'xen', label: 'XenServer', port: 443 },
{ value: 'kvm', label: 'KVM/QEMU', port: 22 },
];
function Field({ label, error, children }) {
return (
<div className="space-y-1.5">
<label className="block text-xs font-medium text-white/60">{label}</label>
{children}
{error && <p className="text-xs text-red-400">{error}</p>}
</div>
);
}
function Input({ className, ...props }) {
return (
<input className={cn(
'w-full px-3 py-2.5 bg-white/5 border border-white/10 rounded-xl text-sm text-white placeholder:text-white/20',
'focus:outline-none focus:border-cyan-500/60 focus:bg-white/[0.07] transition-all',
className
)} {...props} />
);
}
export default function HypervisorCreate() {
const { data, setData, post, processing, errors } = useForm({
name: '', type: 'vcenter', host: '', port: 443,
username: '', password: '', ssl_verify: true, datacenter: '',
});
const [testResult, setTestResult] = useState(null);
const [testing, setTesting] = useState(false);
const handleTypeChange = (type) => {
const t = TYPES.find(t => t.value === type);
setData(d => ({ ...d, type, port: t?.port ?? 443 }));
};
const testConn = async () => {
setTesting(true);
setTestResult(null);
try {
const res = await fetch('/hypervisors/test-temp', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify(data),
});
const json = await res.json();
setTestResult(json);
} catch (e) {
setTestResult({ success: false, message: 'Connection failed: ' + e.message });
} finally {
setTesting(false);
}
};
return (
<AppLayout title="Add Hypervisor">
<Head title="Add Hypervisor" />
<div className="max-w-2xl space-y-5">
<div>
<h2 className="text-xl font-bold text-white">Add Hypervisor</h2>
<p className="text-sm text-white/40">Connect a new virtualization platform</p>
</div>
<GlassCard className="p-6">
<form onSubmit={e => { e.preventDefault(); post('/hypervisors'); }} className="space-y-5">
{/* Type selector */}
<div className="space-y-1.5">
<label className="block text-xs font-medium text-white/60">Hypervisor Type</label>
<div className="grid grid-cols-3 gap-2">
{TYPES.map(t => (
<button key={t.value} type="button" onClick={() => handleTypeChange(t.value)}
className={cn(
'px-3 py-2 rounded-xl border text-xs font-medium transition-all',
data.type === t.value
? 'border-cyan-500/50 bg-cyan-500/10 text-cyan-400'
: 'border-white/10 bg-white/5 text-white/50 hover:border-white/20 hover:text-white'
)}>
{t.label}
</button>
))}
</div>
</div>
<Field label="Display Name" error={errors.name}>
<Input value={data.name} onChange={e => setData('name', e.target.value)} placeholder="Production vCenter" required />
</Field>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<Field label="Hostname / IP" error={errors.host}>
<Input value={data.host} onChange={e => setData('host', e.target.value)} placeholder="vcenter.example.com" required />
</Field>
</div>
<Field label="Port" error={errors.port}>
<Input type="number" value={data.port} onChange={e => setData('port', parseInt(e.target.value))} />
</Field>
</div>
<div className="grid grid-cols-2 gap-3">
<Field label="Username" error={errors.username}>
<Input value={data.username} onChange={e => setData('username', e.target.value)} placeholder="administrator@vsphere.local" required />
</Field>
<Field label="Password" error={errors.password}>
<Input type="password" value={data.password} onChange={e => setData('password', e.target.value)} required />
</Field>
</div>
<Field label="Datacenter (optional)" error={errors.datacenter}>
<Input value={data.datacenter} onChange={e => setData('datacenter', e.target.value)} placeholder="Datacenter name" />
</Field>
<label className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={data.ssl_verify} onChange={e => setData('ssl_verify', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-sm text-white/70">Verify SSL certificate</span>
</label>
{/* Test Result */}
{testResult && (
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}
className={cn(
'flex items-center gap-3 p-3 rounded-xl border text-sm',
testResult.success
? 'bg-green-500/10 border-green-500/30 text-green-300'
: 'bg-red-500/10 border-red-500/30 text-red-300'
)}>
{testResult.success
? <CheckCircle className="w-4 h-4 shrink-0" />
: <XCircle className="w-4 h-4 shrink-0" />}
{testResult.message}
</motion.div>
)}
<div className="flex items-center justify-between pt-2">
<Button type="button" variant="secondary" onClick={testConn} loading={testing}>
{!testing && 'Test Connection'}
</Button>
<div className="flex items-center gap-3">
<a href="/hypervisors"><Button type="button" variant="ghost">Cancel</Button></a>
<Button type="submit" loading={processing}>Add Hypervisor</Button>
</div>
</div>
</form>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,114 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Plus, RefreshCw, Server, TestTube, Edit, Trash2, Eye } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { timeAgo } from '@/lib/utils';
import { useState } from 'react';
const TYPE_LABELS = {
vcenter: 'VMware vCenter', esxi: 'VMware ESXi', proxmox: 'Proxmox VE',
'hyper-v': 'Hyper-V', xen: 'XenServer', kvm: 'KVM',
};
const TYPE_COLORS = {
vcenter: 'text-blue-400', esxi: 'text-blue-300', proxmox: 'text-orange-400',
'hyper-v': 'text-blue-500', kvm: 'text-green-400',
};
export default function HypervisorIndex({ hypervisors }) {
const [syncing, setSyncing] = useState(null);
const sync = (id) => {
setSyncing(id);
router.post(`/hypervisors/${id}/sync`, {}, {
onFinish: () => setSyncing(null),
});
};
const columns = [
{ header: 'Name', accessor: 'name', render: row => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center">
<Server className="w-4 h-4 text-white/50" />
</div>
<div>
<p className="font-medium text-white">{row.name}</p>
<p className={`text-xs ${TYPE_COLORS[row.type] ?? 'text-white/40'}`}>{TYPE_LABELS[row.type] ?? row.type}</p>
</div>
</div>
)},
{ header: 'Host', accessor: 'host', render: row => (
<span className="font-mono text-xs text-white/60">{row.host}:{row.port}</span>
)},
{ header: 'VMs', key: 'vms', render: row => (
<span className="text-white font-medium">{row.virtual_machines_count ?? 0}</span>
)},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: 'Last Sync', accessor: 'last_sync_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.last_sync_at)}</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
<button onClick={() => sync(row.id)} disabled={syncing === row.id}
className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-cyan-400 transition-colors"
title="Sync">
<RefreshCw className={`w-3.5 h-3.5 ${syncing === row.id ? 'animate-spin' : ''}`} />
</button>
<a href={`/hypervisors/${row.id}`} className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-white transition-colors" title="View">
<Eye className="w-3.5 h-3.5" />
</a>
<a href={`/hypervisors/${row.id}/edit`} className="p-1.5 rounded-lg hover:bg-white/10 text-white/40 hover:text-white transition-colors" title="Edit">
<Edit className="w-3.5 h-3.5" />
</a>
<button onClick={() => { if (confirm('Remove this hypervisor?')) router.delete(`/hypervisors/${row.id}`); }}
className="p-1.5 rounded-lg hover:bg-red-500/20 text-white/40 hover:text-red-400 transition-colors" title="Delete">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
)},
];
return (
<AppLayout title="Hypervisors">
<Head title="Hypervisors" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Hypervisors</h2>
<p className="text-sm text-white/40">Manage vCenter, Proxmox, ESXi, Hyper-V and more</p>
</div>
<a href="/hypervisors/create">
<Button icon={Plus}>Add Hypervisor</Button>
</a>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[
{ label: 'Total', value: hypervisors.total ?? 0, color: 'text-white' },
{ label: 'Online', value: hypervisors.data?.filter(h => h.status === 'online').length ?? 0, color: 'text-green-400' },
{ label: 'Offline', value: hypervisors.data?.filter(h => h.status === 'offline').length ?? 0, color: 'text-red-400' },
{ label: 'Error', value: hypervisors.data?.filter(h => h.status === 'error').length ?? 0, color: 'text-orange-400' },
].map(s => (
<GlassCard key={s.label} className="p-4 text-center">
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
<p className="text-xs text-white/40 mt-0.5">{s.label}</p>
</GlassCard>
))}
</div>
<GlassCard className="p-5">
<DataTable
columns={columns}
data={hypervisors.data ?? []}
onRowClick={row => router.visit(`/hypervisors/${row.id}`)}
emptyMessage="No hypervisors configured. Add your first one!"
/>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,161 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head } from '@inertiajs/react';
import { KeyRound, Lock, Certificate, Key, Plus, Eye, EyeOff, Copy, AlertCircle } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { timeAgo } from '@/lib/utils';
import { useState } from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils';
export default function IdentityIndex({ passwords, certificates, ssh_keys, stats }) {
const [activeTab, setActiveTab] = useState('passwords');
const passwordColumns = [
{ header: 'Title', accessor: 'title', render: row => (
<div>
<p className="text-sm text-white font-medium">{row.title}</p>
{row.url && <p className="text-xs text-white/30 truncate max-w-48">{row.url}</p>}
</div>
)},
{ header: 'Username', accessor: 'username', render: row => (
<span className="font-mono text-xs text-white/60">{row.username ?? '—'}</span>
)},
{ header: 'Category', accessor: 'category', render: row => (
row.category ? <span className="text-xs px-2 py-0.5 rounded bg-white/5 text-white/60">{row.category}</span> : null
)},
{ header: 'Expires', accessor: 'expires_at', render: row => (
row.expires_at
? <span className={`text-xs ${new Date(row.expires_at) < new Date() ? 'text-red-400' : 'text-white/40'}`}>{timeAgo(row.expires_at)}</span>
: <span className="text-xs text-white/20">Never</span>
)},
{ header: '', key: 'actions', render: row => (
<div className="flex gap-1">
<Button size="sm" variant="ghost" icon={Eye} onClick={() => {}}>Reveal</Button>
<Button size="sm" variant="ghost" icon={Copy} onClick={() => {}}>Copy</Button>
</div>
)},
];
const certColumns = [
{ header: 'Domain', accessor: 'domain', render: row => (
<div>
<p className="text-sm text-white font-medium font-mono">{row.domain}</p>
<p className="text-xs text-white/30">{row.issuer ?? 'Self-signed'}</p>
</div>
)},
{ header: 'Type', accessor: 'type', render: row => (
<span className="text-xs px-2 py-0.5 rounded bg-white/5 text-white/60 capitalize">{row.type?.replace('-', ' ')}</span>
)},
{ header: 'Expires', accessor: 'expires_at', render: row => {
const daysLeft = Math.ceil((new Date(row.expires_at) - new Date()) / 86400000);
return (
<div>
<p className={`text-sm font-medium ${daysLeft < 30 ? 'text-red-400' : daysLeft < 60 ? 'text-yellow-400' : 'text-white/70'}`}>
{daysLeft > 0 ? `${daysLeft}d left` : 'Expired'}
</p>
<p className="text-xs text-white/30">{new Date(row.expires_at).toLocaleDateString()}</p>
</div>
);
}},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: '', key: 'actions', render: row => (
<Button size="sm" variant="secondary">Download</Button>
)},
];
return (
<AppLayout title="Identity & Secrets">
<Head title="Identity & Secrets" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Identity & Secrets</h2>
<p className="text-sm text-white/40">Passwords, certificates, SSH keys, and secrets</p>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Passwords" value={stats?.passwords ?? 0} icon={Lock} color="cyan" />
<StatCard title="Certificates" value={stats?.certs ?? 0} icon={KeyRound} color="green" />
<StatCard title="Expiring Soon" value={stats?.expiring ?? 0} icon={AlertCircle} color="red" />
<StatCard title="SSH Keys" value={stats?.ssh_keys ?? 0} icon={Key} color="purple" />
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex gap-1 p-1 bg-white/5 rounded-xl w-fit border border-white/10 mb-4">
{[
{ value: 'passwords', label: 'Passwords', icon: Lock },
{ value: 'certificates', label: 'Certificates', icon: KeyRound },
{ value: 'ssh-keys', label: 'SSH Keys', icon: Key },
].map(t => {
const Icon = t.icon;
return (
<TabsTrigger key={t.value} value={t.value}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
activeTab === t.value
? 'bg-white/10 text-white'
: 'text-white/50 hover:text-white'
)}>
<Icon className="w-3.5 h-3.5" />
{t.label}
</TabsTrigger>
);
})}
</TabsList>
<TabsContent value="passwords">
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Password Vault</h3>
<Button icon={Plus} size="sm">Add Password</Button>
</div>
<DataTable columns={passwordColumns} data={passwords ?? []}
emptyMessage="No passwords stored. Add your first entry." />
</GlassCard>
</TabsContent>
<TabsContent value="certificates">
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">SSL/TLS Certificates</h3>
<div className="flex gap-2">
<Button variant="secondary" size="sm">Generate Self-Signed</Button>
<Button icon={Plus} size="sm">Import Cert</Button>
</div>
</div>
<DataTable columns={certColumns} data={certificates ?? []}
emptyMessage="No certificates. Import or generate your first certificate." />
</GlassCard>
</TabsContent>
<TabsContent value="ssh-keys">
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">SSH Keys</h3>
<div className="flex gap-2">
<Button variant="secondary" size="sm">Generate Key Pair</Button>
<Button icon={Plus} size="sm">Import Key</Button>
</div>
</div>
<DataTable
columns={[
{ header: 'Name', accessor: 'name', render: r => <p className="text-sm text-white">{r.name}</p> },
{ header: 'Algorithm', accessor: 'algorithm', render: r => <span className="font-mono text-xs text-white/60">{r.algorithm?.toUpperCase()} {r.key_size}</span> },
{ header: 'Fingerprint', accessor: 'fingerprint', render: r => <span className="font-mono text-xs text-white/40 truncate max-w-40 block">{r.fingerprint}</span> },
{ header: '', key: 'a', render: r => <Button size="sm" variant="ghost" icon={Copy}>Copy Public Key</Button> },
]}
data={ssh_keys ?? []}
emptyMessage="No SSH keys. Generate or import a key pair."
/>
</GlassCard>
</TabsContent>
</Tabs>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,131 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Activity, Bell, CheckCircle, AlertTriangle, XCircle, Plus } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { DataTable } from '@/Components/ui/DataTable';
import { ProgressBar } from '@/Components/ui/ProgressBar';
import { timeAgo } from '@/lib/utils';
import {
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
ResponsiveContainer, AreaChart, Area
} from 'recharts';
// Demo metric data
const metricStream = Array.from({ length: 30 }, (_, i) => ({
t: `-${30 - i}m`,
cpu: 30 + Math.sin(i * 0.3) * 20 + Math.random() * 10,
mem: 55 + Math.cos(i * 0.2) * 15 + Math.random() * 8,
net: Math.abs(Math.sin(i * 0.5) * 400 + Math.random() * 100),
}));
export default function MonitoringIndex({ alerts, alert_stats, dashboards }) {
const firing = alerts?.filter(a => a.status === 'firing') ?? [];
const acked = alerts?.filter(a => a.status === 'acknowledged') ?? [];
const alertColumns = [
{ header: 'Alert', accessor: 'title', render: row => (
<div>
<p className="text-sm text-white">{row.title}</p>
<p className="text-xs text-white/30">{row.target_name}</p>
</div>
)},
{ header: 'Severity', accessor: 'severity', render: row => (
<StatusBadge status={row.severity} />
)},
{ header: 'Status', accessor: 'status', render: row => (
<StatusBadge status={row.status} />
)},
{ header: 'Fired', accessor: 'created_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.created_at)}</span>
)},
{ header: '', key: 'actions', render: row => (
row.status === 'firing' && (
<Button size="sm" variant="secondary" onClick={() => {}}>Acknowledge</Button>
)
)},
];
return (
<AppLayout title="Monitoring">
<Head title="Monitoring" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Unified Monitoring</h2>
<p className="text-sm text-white/40">Real-time metrics, alerts, and dashboards</p>
</div>
<div className="flex gap-2">
<a href="/monitoring/dashboards"><Button variant="secondary">Dashboards</Button></a>
<a href="/monitoring/alerts"><Button variant="secondary" icon={Bell}>Alert Rules</Button></a>
</div>
</div>
{/* Alert Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Firing" value={firing.length} color="red" icon={XCircle} />
<StatCard title="Acknowledged" value={acked.length} color="yellow" icon={Bell} />
<StatCard title="Resolved 24h" value={alert_stats?.resolved_24h ?? 0} color="green" icon={CheckCircle} />
<StatCard title="Total Rules" value={alert_stats?.total_rules ?? 0} color="blue" icon={Activity} />
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-1">CPU & Memory</h3>
<p className="text-xs text-white/30 mb-4">Average across all VMs — last 30 min</p>
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={metricStream} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="cpu2" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#00d4ff" stopOpacity={0.3} />
<stop offset="95%" stopColor="#00d4ff" stopOpacity={0} />
</linearGradient>
<linearGradient id="mem2" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#9d4edd" stopOpacity={0.3} />
<stop offset="95%" stopColor="#9d4edd" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="t" tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} interval={5} />
<YAxis tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} domain={[0, 100]} />
<Tooltip contentStyle={{ backgroundColor: '#0f1f35', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '12px' }} />
<Area type="monotone" dataKey="cpu" stroke="#00d4ff" fill="url(#cpu2)" strokeWidth={1.5} name="CPU %" />
<Area type="monotone" dataKey="mem" stroke="#9d4edd" fill="url(#mem2)" strokeWidth={1.5} name="Mem %" />
</AreaChart>
</ResponsiveContainer>
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white mb-1">Network I/O</h3>
<p className="text-xs text-white/30 mb-4">Mbps across all endpoints</p>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={metricStream} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="t" tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} interval={5} />
<YAxis tick={{ fill: 'rgba(255,255,255,0.3)', fontSize: 10 }} axisLine={false} tickLine={false} />
<Tooltip contentStyle={{ backgroundColor: '#0f1f35', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '12px' }} />
<Line type="monotone" dataKey="net" stroke="#00ff88" strokeWidth={1.5} dot={false} name="Net Mbps" />
</LineChart>
</ResponsiveContainer>
</GlassCard>
</div>
{/* Active Alerts Table */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Active Alerts</h3>
<Button icon={Plus} size="sm">Create Rule</Button>
</div>
<DataTable
columns={alertColumns}
data={alerts ?? []}
emptyMessage="No active alerts — all systems operational"
/>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,138 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { motion } from 'framer-motion';
import { Shield, AlertTriangle, CheckCircle, TrendingUp, Scan, Lock, Bug } from 'lucide-react';
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
import { StatusBadge } from '@/Components/ui/StatusBadge';
import { Button } from '@/Components/ui/Button';
import { ProgressBar } from '@/Components/ui/ProgressBar';
import { DataTable } from '@/Components/ui/DataTable';
import { timeAgo, getSecurityGradeColor } from '@/lib/utils';
import { RadarChart, Radar, PolarGrid, PolarAngleAxis, ResponsiveContainer } from 'recharts';
import { cn } from '@/lib/utils';
const radarData = [
{ subject: 'Patching', A: 85 },
{ subject: 'Access Ctrl', A: 72 },
{ subject: 'Encryption', A: 90 },
{ subject: 'Network', A: 65 },
{ subject: 'Monitoring', A: 78 },
{ subject: 'Compliance', A: 60 },
];
function ScoreRing({ score, grade }) {
const color = grade === 'A' ? '#00ff88' : grade === 'B' ? '#00d4ff' : grade === 'C' ? '#ffd700' : grade === 'D' ? '#ff6b35' : '#ff2d55';
const r = 60, circumference = 2 * Math.PI * r;
const dash = (score / 100) * circumference;
return (
<div className="relative inline-flex items-center justify-center">
<svg className="w-40 h-40 -rotate-90">
<circle cx="80" cy="80" r={r} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="8" />
<circle cx="80" cy="80" r={r} fill="none" stroke={color} strokeWidth="8"
strokeDasharray={`${dash} ${circumference}`}
strokeLinecap="round"
style={{ filter: `drop-shadow(0 0 8px ${color}60)` }}
/>
</svg>
<div className="absolute text-center">
<p className="text-4xl font-bold" style={{ color }}>{score}</p>
<p className="text-xs text-white/40">/ 100</p>
</div>
</div>
);
}
export default function SecurityIndex({ scores, scans, stats }) {
const avgScore = scores?.avg ?? 72;
const grade = avgScore >= 90 ? 'A' : avgScore >= 75 ? 'B' : avgScore >= 60 ? 'C' : avgScore >= 45 ? 'D' : 'F';
const scanColumns = [
{ header: 'Target', accessor: 'target_name', render: row => (
<div>
<p className="text-sm text-white">{row.target_name}</p>
<p className="text-xs text-white/30 capitalize">{row.target_type} · {row.scan_type}</p>
</div>
)},
{ header: 'Score', accessor: 'score', render: row => (
<div className="flex items-center gap-2">
<span className={`text-lg font-bold ${getSecurityGradeColor(row.grade)}`}>{row.grade}</span>
<span className="text-sm text-white/60">{row.score ?? '—'}</span>
</div>
)},
{ header: 'Findings', key: 'findings', render: row => (
<div className="flex items-center gap-1 text-xs">
{row.critical_count > 0 && <span className="px-1.5 py-0.5 rounded bg-red-500/20 text-red-400">{row.critical_count}C</span>}
{row.high_count > 0 && <span className="px-1.5 py-0.5 rounded bg-orange-500/20 text-orange-400">{row.high_count}H</span>}
{row.medium_count > 0 && <span className="px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-400">{row.medium_count}M</span>}
{row.low_count > 0 && <span className="px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400">{row.low_count}L</span>}
</div>
)},
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
{ header: 'Scanned', accessor: 'completed_at', render: row => (
<span className="text-xs text-white/40">{timeAgo(row.completed_at)}</span>
)},
];
return (
<AppLayout title="Security">
<Head title="Security Dashboard" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Cybersecurity Dashboard</h2>
<p className="text-sm text-white/40">Security posture across all infrastructure</p>
</div>
<div className="flex gap-2">
<a href="/security/scans"><Button variant="secondary" icon={Scan}>Scans</Button></a>
<a href="/security/compliance"><Button variant="secondary" icon={CheckCircle}>Compliance</Button></a>
</div>
</div>
{/* Score + Radar */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<GlassCard className="p-6 flex flex-col items-center justify-center gap-3" glow="green">
<h3 className="text-sm font-semibold text-white/60">Overall Security Score</h3>
<ScoreRing score={avgScore} grade={grade} />
<div className="flex items-center gap-2">
<span className={cn('text-3xl font-bold', getSecurityGradeColor(grade))}>Grade {grade}</span>
</div>
<p className="text-xs text-white/30 text-center">Based on {scans?.total ?? 0} scans across infrastructure</p>
</GlassCard>
<GlassCard className="p-5 lg:col-span-2">
<h3 className="text-sm font-semibold text-white mb-1">Security Radar</h3>
<p className="text-xs text-white/30 mb-3">Coverage across security domains</p>
<ResponsiveContainer width="100%" height={220}>
<RadarChart cx="50%" cy="50%" outerRadius={80} data={radarData}>
<PolarGrid stroke="rgba(255,255,255,0.1)" />
<PolarAngleAxis dataKey="subject" tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 11 }} />
<Radar name="Score" dataKey="A" stroke="#00d4ff" fill="#00d4ff" fillOpacity={0.15} strokeWidth={2} />
</RadarChart>
</ResponsiveContainer>
</GlassCard>
</div>
{/* Stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard title="Critical" value={stats?.critical ?? 0} color="red" icon={AlertTriangle} />
<StatCard title="High" value={stats?.high ?? 0} color="orange" icon={Bug} />
<StatCard title="Medium" value={stats?.medium ?? 0} color="orange" />
<StatCard title="Low" value={stats?.low ?? 0} color="blue" />
</div>
{/* Scan results table */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Recent Scans</h3>
<Button icon={Scan} size="sm" onClick={() => router.post('/security/scans')}>Run Scan</Button>
</div>
<DataTable
columns={scanColumns}
data={scans?.data ?? []}
emptyMessage="No scans yet. Run your first security scan."
/>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,113 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { BookOpen, Plus, FileText, Folder, Upload, Search } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { motion } from 'framer-motion';
import { timeAgo } from '@/lib/utils';
import { useState } from 'react';
export default function WikiIndex({ spaces, recent_pages, files }) {
const [search, setSearch] = useState('');
const filtered = (recent_pages ?? []).filter(p =>
p.title.toLowerCase().includes(search.toLowerCase())
);
return (
<AppLayout title="Wiki">
<Head title="Wiki & Documentation" />
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">Wiki & Documents</h2>
<p className="text-sm text-white/40">Centralized documentation, runbooks, and files</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" icon={Upload}>Upload File</Button>
<a href="/wiki/pages/create"><Button icon={Plus}>New Page</Button></a>
</div>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-white/30" />
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search pages, documents..."
className="w-full pl-12 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/50 transition-all"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Spaces */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-white">Spaces</h3>
<Button size="sm" variant="ghost" icon={Plus}>New Space</Button>
</div>
<div className="space-y-2">
{(spaces ?? []).map(space => (
<a key={space.id} href={`/wiki/spaces/${space.slug}`}>
<GlassCard className="p-4 cursor-pointer" hover>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg flex items-center justify-center text-lg"
style={{ backgroundColor: `${space.color}20`, color: space.color }}>
<Folder className="w-5 h-5" />
</div>
<div>
<p className="text-sm font-medium text-white">{space.name}</p>
<p className="text-xs text-white/30">{space.pages_count ?? 0} pages</p>
</div>
</div>
</GlassCard>
</a>
))}
{(!spaces || spaces.length === 0) && (
<GlassCard className="p-6 text-center">
<BookOpen className="w-8 h-8 text-white/20 mx-auto mb-2" />
<p className="text-sm text-white/30">No spaces yet</p>
</GlassCard>
)}
</div>
</div>
{/* Recent Pages */}
<div className="lg:col-span-2 space-y-3">
<h3 className="text-sm font-semibold text-white">
{search ? `Search: "${search}"` : 'Recent Pages'}
</h3>
<div className="space-y-2">
{filtered.map(page => (
<a key={page.id} href={`/wiki/pages/${page.id}`}>
<GlassCard className="p-4 cursor-pointer" hover>
<div className="flex items-start gap-3">
<FileText className="w-4 h-4 text-white/30 mt-0.5 shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white">{page.title}</p>
<div className="flex items-center gap-3 mt-1">
<p className="text-xs text-white/30">{page.space?.name}</p>
<span className="text-white/10">·</span>
<p className="text-xs text-white/30">Updated {timeAgo(page.updated_at)}</p>
</div>
</div>
</div>
</GlassCard>
</a>
))}
{filtered.length === 0 && (
<GlassCard className="p-8 text-center">
<FileText className="w-8 h-8 text-white/20 mx-auto mb-2" />
<p className="text-sm text-white/30">
{search ? 'No pages match your search' : 'No pages yet. Create your first page!'}
</p>
</GlassCard>
)}
</div>
</div>
</div>
</div>
</AppLayout>
);
}