[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useOmni } from "@/components/OmniSyncProvider";
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
licenseNumber?: string;
|
||||
licenses?: string | Record<string, string>;
|
||||
brandColor?: string;
|
||||
platformName?: string;
|
||||
securityTier?: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
licenses?: string | Record<string, string>;
|
||||
byokEnabled?: boolean;
|
||||
byokKey?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Feature {
|
||||
key: string;
|
||||
name: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
interface Package {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
features: string;
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const {} = useOmni();
|
||||
const [tenant, setTenant] = useState<Tenant | null>(null);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [systemFeatures, setSystemFeatures] = useState<Feature[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
const [debugError, setDebugError] = useState<string | null>(null);
|
||||
|
||||
const [showAddUserModal, setShowAddUserModal] = useState(false);
|
||||
const [showLimitsModal, setShowLimitsModal] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [userLicenses, setUserLicenses] = useState<Record<string, string>>({});
|
||||
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [packages, setPackages] = useState<Package[]>([]);
|
||||
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
||||
|
||||
// BYOK States
|
||||
const [orgByokEnabled, setOrgByokEnabled] = useState(false);
|
||||
const [orgByokKey, setOrgByokKey] = useState("");
|
||||
const [userByokEnabled, setUserByokEnabled] = useState(false);
|
||||
const [userByokKey, setUserByokKey] = useState("");
|
||||
|
||||
// Quantum Toast System
|
||||
const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null);
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
setToast({ message, type });
|
||||
setTimeout(() => setToast(null), 4000);
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
} catch (e) {
|
||||
console.error("Logout failed", e);
|
||||
window.location.href = '/';
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const resp = await fetch("/api/admin", { cache: "no-store" });
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
window.location.href = window.location.pathname.replace('/admin', '/dashboard');
|
||||
} else {
|
||||
setTenant(data.tenant);
|
||||
setOrgByokEnabled(data.tenant.byokEnabled || false);
|
||||
setOrgByokKey(data.tenant.byokKey || "");
|
||||
setUsers(data.users);
|
||||
setSystemFeatures(data.systemFeatures || []);
|
||||
|
||||
// Fetch Packages for Billing
|
||||
const pResp = await fetch("/api/superadmin/packages");
|
||||
const pData = await pResp.json();
|
||||
setPackages(pData.packages || []);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setDebugError(e.message || "Unknown error occurred");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await fetchData();
|
||||
};
|
||||
init();
|
||||
|
||||
}, []);
|
||||
|
||||
const handleAddUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const resp = await fetch("/api/admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "add_user", email: newEmail, password: newPassword, role: "user" })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
setUsers([...users, data.user]);
|
||||
setShowAddUserModal(false);
|
||||
setNewEmail(""); setNewPassword("");
|
||||
showToast("Employee added successfully.");
|
||||
} else {
|
||||
showToast("Failed: " + data.error, "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Server error.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenLimits = (u: User) => {
|
||||
setSelectedUser(u);
|
||||
try {
|
||||
let parsed: Record<string, string> = {};
|
||||
if (typeof u.licenses === 'string') {
|
||||
const js = JSON.parse(u.licenses);
|
||||
if (Array.isArray(js)) js.forEach((k: string) => parsed[k] = "GRANTED");
|
||||
else parsed = js;
|
||||
} else if (u.licenses && typeof u.licenses === 'object') {
|
||||
if (Array.isArray(u.licenses)) u.licenses.forEach((k: string) => parsed[k] = "GRANTED");
|
||||
else parsed = u.licenses;
|
||||
}
|
||||
setUserLicenses(parsed);
|
||||
setUserByokEnabled(u.byokEnabled || false);
|
||||
setUserByokKey(u.byokKey || "");
|
||||
} catch(_e) { setUserLicenses({}); }
|
||||
setShowLimitsModal(true);
|
||||
};
|
||||
|
||||
const handleSaveLimits = async () => {
|
||||
try {
|
||||
const resp = await fetch("/api/admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "update_user_licenses",
|
||||
targetUserId: selectedUser?.id,
|
||||
newLicenses: userLicenses,
|
||||
byokEnabled: userByokEnabled,
|
||||
byokKey: userByokKey
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
showToast("User limits applied successfully.");
|
||||
setShowLimitsModal(false);
|
||||
fetchData();
|
||||
try {
|
||||
const bc = new BroadcastChannel('omni_channel');
|
||||
bc.postMessage({ type: 'REFRESH_QUANTUM_TOKEN' });
|
||||
} catch(e){}
|
||||
} else {
|
||||
const d = await resp.json();
|
||||
showToast(d.error || "Error saving limits.", "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Error saving limits. Periksa koneksi Anda.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveOrgByok = async () => {
|
||||
try {
|
||||
const resp = await fetch("/api/admin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "update_tenant_byok",
|
||||
byokEnabled: orgByokEnabled,
|
||||
byokKey: orgByokKey
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
showToast("Organizational BYOK applied successfully.");
|
||||
fetchData();
|
||||
} else {
|
||||
const d = await resp.json();
|
||||
showToast("Error: " + d.error, "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Error saving BYOK.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckout = async (packageId: string) => {
|
||||
setCheckoutLoading(true);
|
||||
try {
|
||||
const resp = await fetch("/api/admin/checkout", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ packageId })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.url) {
|
||||
window.location.assign(data.url);
|
||||
} else {
|
||||
showToast("Checkout Error: " + (data.error || "Unknown error"), "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Payment gateway connection failed.", "error");
|
||||
} finally {
|
||||
setCheckoutLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-blue-600 font-medium">Loading Admin Workspace...</div>;
|
||||
if (debugError) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-red-600 font-medium">Error: {debugError}</div>;
|
||||
if (!tenant) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-red-600 font-medium">Error: Tenant not found</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f4f5f9] text-gray-900 font-sans pb-20 relative">
|
||||
|
||||
{/* Quantum Toast Notification System */}
|
||||
{toast && (
|
||||
<div className={`fixed top-20 right-8 z-[100] px-6 py-4 rounded-xl shadow-2xl backdrop-blur-xl border flex items-center gap-4 animate-in slide-in-from-right-8 fade-in duration-300 ${toast.type === 'success' ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-700' : 'bg-red-500/10 border-red-500/30 text-red-700'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${toast.type === 'success' ? 'bg-emerald-500 text-white' : 'bg-red-500 text-white'}`}>
|
||||
{toast.type === 'success' ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path></svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-black uppercase tracking-widest opacity-80 mb-0.5">{toast.type === 'success' ? 'System Success' : 'System Alert'}</div>
|
||||
<div className="text-sm font-medium">{toast.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HEADER ZOOM-LIKE */}
|
||||
<header className="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-8 sticky top-0 z-40 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="w-7 h-7 text-blue-600" fill="currentColor" viewBox="0 0 24 24"><path d="M4 4h10a2 2 0 0 1 2 2v3.5l4-3v11l-4-3V18a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"></path></svg>
|
||||
<h1 className="text-xl font-bold tracking-tight text-gray-800">Workspace Admin <span className="text-sm font-normal text-gray-500 ml-2">{tenant.name}</span></h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<a href="/admin/telemetry" className="px-3 py-1.5 bg-gradient-to-r from-[#25D366] to-[#0b5cff] text-white text-xs font-bold rounded-md hover:opacity-90 transition-opacity flex items-center gap-2 shadow-sm">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
TENANT COMMAND CENTER
|
||||
</a>
|
||||
<button onClick={() => router.push('/dashboard')} className="text-sm text-gray-600 hover:text-gray-900 font-medium transition-colors">Go to App</button>
|
||||
<button onClick={handleLogout} className="text-sm text-blue-600 hover:text-blue-800 font-semibold transition-colors">Sign Out</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-6 py-8 flex flex-col md:flex-row gap-8">
|
||||
|
||||
{/* SIDEBAR */}
|
||||
<div className="w-full md:w-64 shrink-0">
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-4 sticky top-24">
|
||||
<div className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3 px-2">Account Info</div>
|
||||
<div className="mb-6 px-2">
|
||||
<div className="font-bold text-gray-900">{tenant.name}</div>
|
||||
<div className="text-xs text-gray-500 font-mono mt-1">LIC: {tenant.licenseNumber || 'PENDING'}</div>
|
||||
|
||||
{/* Security & Network Feature Badges */}
|
||||
{(() => {
|
||||
let lic: Record<string, string> = {};
|
||||
try { lic = typeof tenant.licenses === 'string' ? JSON.parse(tenant.licenses || '{}') : (tenant.licenses || {}); } catch {}
|
||||
const feats = [
|
||||
{ key: 'tls_global_ca', label: "\ud83c\udf10 Let's Encrypt", cls: 'bg-emerald-50 text-emerald-700 border-emerald-200' },
|
||||
{ key: 'tls_sovereign', label: '\ud83d\udee1\ufe0f Private CA (X)', cls: 'bg-cyan-50 text-cyan-700 border-cyan-200' },
|
||||
{ key: 'ipv6_dual_stack', label: '\ud83c\udf10 IPv6 Dual-Stack', cls: 'bg-violet-50 text-violet-700 border-violet-200' },
|
||||
];
|
||||
const visible = feats.filter(f => lic[f.key] && lic[f.key] !== 'HIDDEN');
|
||||
if (visible.length === 0) return null;
|
||||
return (
|
||||
<div className="mt-3 space-y-1.5">
|
||||
{visible.map(f => {
|
||||
const state = lic[f.key];
|
||||
if (state === 'GRANTED') return (
|
||||
<div key={f.key} className={`flex items-center gap-1.5 text-[10px] font-bold px-2 py-1 rounded-md border ${f.cls}`}>
|
||||
<span className="relative flex h-1.5 w-1.5"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span><span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500"></span></span>
|
||||
{f.label}
|
||||
</div>
|
||||
);
|
||||
if (state === 'UPSELL') return (
|
||||
<button key={f.key} onClick={() => setActiveTab('billing')} className="flex items-center gap-1.5 text-[10px] font-bold px-2 py-1 rounded-md bg-amber-50 text-amber-600 border border-amber-200 hover:bg-amber-100 transition-colors w-full text-left">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd"></path></svg>
|
||||
{f.label} <span className="ml-auto text-[8px] uppercase tracking-wider">Upgrade</span>
|
||||
</button>
|
||||
);
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<nav className="space-y-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`w-full text-left px-3 py-2 rounded-md font-medium text-sm transition-all ${activeTab === 'users' ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
||||
>
|
||||
User Management
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('billing')}
|
||||
className={`w-full text-left px-3 py-2 rounded-md font-medium text-sm transition-all ${activeTab === 'billing' ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
||||
>
|
||||
Billing & Plans
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('security')}
|
||||
className={`w-full text-left px-3 py-2 rounded-md font-medium text-sm transition-all ${activeTab === 'security' ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
||||
>
|
||||
Security & BYOK
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN CONTENT */}
|
||||
<div className="flex-1 space-y-6">
|
||||
{activeTab === 'users' && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="px-6 py-5 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold text-gray-800">Users ({users.length})</h2>
|
||||
<button
|
||||
onClick={() => setShowAddUserModal(true)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors shadow-sm"
|
||||
>
|
||||
+ Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 bg-white">
|
||||
<th className="py-3 px-6 text-gray-500 font-semibold text-xs uppercase tracking-wider">Email / ID</th>
|
||||
<th className="py-3 px-6 text-gray-500 font-semibold text-xs uppercase tracking-wider">Role</th>
|
||||
<th className="py-3 px-6 text-gray-500 font-semibold text-xs uppercase tracking-wider">Joined Date</th>
|
||||
<th className="py-3 px-6 text-right text-gray-500 font-semibold text-xs uppercase tracking-wider">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{users.map((u, i) => (
|
||||
<tr key={i} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<td className="py-4 px-6 text-gray-900 font-medium">{u.email}</td>
|
||||
<td className="py-4 px-6">
|
||||
<span className={`px-2.5 py-1 rounded-md text-xs font-semibold ${u.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'}`}>
|
||||
{u.role.toUpperCase()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-6 text-gray-500">{new Date(u.createdAt).toLocaleDateString('en-US')}</td>
|
||||
<td className="py-4 px-6 text-right">
|
||||
<button
|
||||
onClick={() => handleOpenLimits(u)}
|
||||
className="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 hover:text-blue-600 rounded text-xs font-semibold transition-colors focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
Feature Limits
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'billing' && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Billing & Subscription</h2>
|
||||
<p className="text-gray-500 text-sm mb-8">Choose the right plan to unlock Quantum XCU modules for your team.</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{packages.map(pkg => (
|
||||
<div key={pkg.id} className="border border-gray-200 rounded-2xl p-6 hover:border-blue-500 transition-all group relative overflow-hidden">
|
||||
<div className="font-bold text-lg text-gray-900 mb-1">{pkg.name}</div>
|
||||
<div className="text-2xl font-black text-blue-600 mb-4">{pkg.price}</div>
|
||||
<ul className="text-sm text-gray-500 space-y-2 mb-8">
|
||||
{JSON.parse(pkg.features || '[]').slice(0, 4).map((f: string, idx: number) => (
|
||||
<li key={idx} className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-emerald-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd"></path></svg>
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
disabled={checkoutLoading}
|
||||
onClick={() => handleCheckout(pkg.id)}
|
||||
className="w-full py-3 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 transition-all disabled:opacity-50"
|
||||
>
|
||||
{checkoutLoading ? "Connecting..." : "Upgrade Now"}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-8">
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white shadow-lg">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Quantum Security Suite</h2>
|
||||
<p className="text-gray-500 text-sm">Manage organizational cryptographic sovereignty and BYOK settings.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TLS SOVEREIGN MODULE - ?? la Carte */}
|
||||
{(() => {
|
||||
let tlsState = 'HIDDEN';
|
||||
try {
|
||||
const lic = typeof tenant.licenses === 'string' ? JSON.parse(tenant.licenses || '{}') : (tenant.licenses || {});
|
||||
tlsState = lic['tls_sovereign'] || 'HIDDEN';
|
||||
} catch {}
|
||||
if (tlsState === 'HIDDEN') return null;
|
||||
return (
|
||||
<div className={`mb-8 rounded-2xl border overflow-hidden ${
|
||||
tlsState === 'GRANTED'
|
||||
? 'bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 border-slate-700/50'
|
||||
: 'bg-gradient-to-br from-amber-50 to-orange-50 border-amber-200'
|
||||
}`}>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg ${
|
||||
tlsState === 'GRANTED'
|
||||
? 'bg-gradient-to-br from-cyan-500 to-blue-600 shadow-cyan-500/20'
|
||||
: 'bg-gradient-to-br from-amber-400 to-orange-500 shadow-amber-500/20'
|
||||
}`}>
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`text-lg font-bold ${
|
||||
tlsState === 'GRANTED' ? 'text-white' : 'text-amber-900'
|
||||
}`}>TLS Sovereign Module</h3>
|
||||
<p className={`text-xs ${
|
||||
tlsState === 'GRANTED' ? 'text-slate-400' : 'text-amber-700'
|
||||
}`}>?? la Carte ??? Sovereign Communication Infrastructure</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tlsState === 'GRANTED' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-2 ${
|
||||
tenant.securityTier === 'SOVEREIGN'
|
||||
? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/20'
|
||||
: tenant.securityTier === 'CLIENT_CA'
|
||||
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
||||
}`}>
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
{tenant.securityTier === 'SOVEREIGN' ? '\ud83d\udee1\ufe0f SOVEREIGN \u2014 Private CA (X)' : tenant.securityTier === 'CLIENT_CA' ? '\ud83c\udfdb\ufe0f CLIENT CA \u2014 Custom PKI' : '\ud83c\udf10 STANDARD \u2014 Let\'s Encrypt'}
|
||||
</div>
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
||||
</span>
|
||||
<span className="text-[10px] text-emerald-400 font-bold uppercase">Active</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="text-[10px] text-slate-500 font-bold uppercase mb-1">Mode</div>
|
||||
<div className="text-sm text-white font-bold">{tenant.securityTier === 'SOVEREIGN' ? 'Private CA (Offline-Ready)' : tenant.securityTier === 'CLIENT_CA' ? 'Client PKI (Custom CA)' : 'Let\'s Encrypt (Global)'}</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
||||
<div className="text-[10px] text-slate-500 font-bold uppercase mb-1">Validity</div>
|
||||
<div className="text-sm text-white font-bold">{tenant.securityTier === 'SOVEREIGN' ? '30 Years (RSA-4096)' : tenant.securityTier === 'CLIENT_CA' ? 'Per CA Policy' : 'Auto-Renew (90 Days)'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-2">
|
||||
{tenant.securityTier === 'SOVEREIGN'
|
||||
? 'Organisasi Anda menggunakan Private CA dari X. Hubungi Supreme Admin untuk panduan install sertifikat di device.'
|
||||
: tenant.securityTier === 'CLIENT_CA'
|
||||
? 'Organisasi Anda menggunakan CA sendiri (custom PKI). Sertifikat dan key di-manage oleh infrastruktur PKI internal organisasi.'
|
||||
: 'Koneksi menggunakan sertifikat Let\'s Encrypt yang dipercaya semua browser. Untuk beralih ke Private CA, hubungi Supreme Admin.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* UPSELL STATE */
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-amber-800 leading-relaxed">
|
||||
Tingkatkan ke <strong>TLS Sovereign</strong> untuk mendapatkan infrastruktur komunikasi yang sepenuhnya tertutup
|
||||
dengan Private CA, zero-trust security, dan kemampuan operasi offline tanpa bergantung internet global.
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="bg-white rounded-xl p-3 border border-amber-200 text-center">
|
||||
<svg className="w-6 h-6 text-amber-600 mx-auto mb-1" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
<div className="text-[10px] font-bold text-amber-800">Private CA</div>
|
||||
<div className="text-[9px] text-amber-600">RSA-4096</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-amber-200 text-center">
|
||||
<svg className="w-6 h-6 text-amber-600 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636a9 9 0 010 12.728M5.636 18.364a9 9 0 010-12.728" /></svg>
|
||||
<div className="text-[10px] font-bold text-amber-800">Offline-Ready</div>
|
||||
<div className="text-[9px] text-amber-600">Zero Internet</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-3 border border-amber-200 text-center">
|
||||
<svg className="w-6 h-6 text-amber-600 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
||||
<div className="text-[10px] font-bold text-amber-800">30 Years</div>
|
||||
<div className="text-[9px] text-amber-600">Certificate Life</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-full py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-white rounded-xl font-bold text-sm hover:shadow-lg hover:shadow-amber-500/20 transition-all hover:scale-[1.01] flex items-center justify-center gap-2">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
Upgrade ke TLS Sovereign
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="bg-blue-50/50 border border-blue-100 rounded-2xl p-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-bold text-blue-900 mb-2 uppercase tracking-wide">Organizational BYOK</h3>
|
||||
<p className="text-sm text-blue-700 leading-relaxed max-w-xl">
|
||||
Aktifkan kedaulatan data total dengan menggunakan kunci enkripsi milik Anda sendiri.
|
||||
Kunci ini akan digunakan untuk mengenkripsi semua transmisi video, audio, dan chat dalam organisasi Anda.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 w-full md:w-72">
|
||||
<label className="flex items-center gap-3 cursor-pointer bg-white px-4 py-3 rounded-2xl border border-blue-200 shadow-sm transition-all hover:border-blue-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={orgByokEnabled}
|
||||
onChange={e => setOrgByokEnabled(e.target.checked)}
|
||||
className="w-5 h-5 text-blue-600 rounded-lg focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-xs font-black text-gray-700 uppercase tracking-widest">Enable BYOK Mode</span>
|
||||
</label>
|
||||
{orgByokEnabled && (
|
||||
<div className="space-y-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<label className="text-[10px] font-black text-blue-600 uppercase ml-1">Custom Quantum Key</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Encryption Key..."
|
||||
value={orgByokKey}
|
||||
onChange={e => setOrgByokKey(e.target.value)}
|
||||
className="w-full text-xs font-mono bg-white border border-blue-200 rounded-2xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-blue-100 flex justify-end">
|
||||
<button
|
||||
onClick={handleSaveOrgByok}
|
||||
className="px-8 py-3 bg-blue-600 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:bg-blue-700 transition-all shadow-lg hover:shadow-blue-500/20 active:scale-95"
|
||||
>
|
||||
Deploy Security Policy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<div className="flex items-center gap-2 text-emerald-600 mb-4">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd"></path></svg>
|
||||
<span className="text-xs font-black uppercase tracking-widest">Quantum Health Status: Protected</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 font-medium max-w-2xl italic">
|
||||
Catatan: Setiap perubahan pada kebijakan keamanan akan dicatat dalam Quantum Ledger dan memerlukan waktu sinkronisasi hingga 50ms ke seluruh node global.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
{/* ADD USER MODAL */}
|
||||
{showAddUserModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white p-8 rounded-xl shadow-2xl w-[400px]">
|
||||
<h2 className="text-xl font-bold mb-6 text-gray-900">Add New User</h2>
|
||||
<form onSubmit={handleAddUser}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
|
||||
<input type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500" required />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Temporary Password</label>
|
||||
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500" required minLength={6} />
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button type="button" onClick={() => setShowAddUserModal(false)} className="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</button>
|
||||
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Add User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* USER LIMITS MODAL (BYOK) */}
|
||||
{showLimitsModal && selectedUser && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white w-full max-w-2xl rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<h2 className="text-xl font-bold text-gray-900">User Feature Limits</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">Manage module access for <strong className="text-gray-800">{selectedUser.email}</strong>.</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
{/* USER LEVEL BYOK SECTION */}
|
||||
<div className="mb-10 bg-emerald-50/50 p-6 rounded-2xl border border-emerald-100">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-black text-emerald-900 uppercase tracking-widest mb-1">User BYOK Sovereignty</h3>
|
||||
<p className="text-[10px] text-emerald-700 font-medium">Berikan kedaulatan enkripsi individual kepada user ini. Kunci ini akan melampaui kunci organisasi.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full md:w-64">
|
||||
<label className="flex items-center gap-2 cursor-pointer bg-white px-3 py-2 rounded-xl border border-emerald-200">
|
||||
<input type="checkbox" checked={userByokEnabled} onChange={e => setUserByokEnabled(e.target.checked)} className="w-4 h-4 text-emerald-600 rounded" />
|
||||
<span className="text-[10px] font-bold text-gray-700">Enable Individual BYOK</span>
|
||||
</label>
|
||||
{userByokEnabled && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Individual Quantum Key..."
|
||||
value={userByokKey}
|
||||
onChange={e => setUserByokKey(e.target.value)}
|
||||
className="text-[10px] font-mono bg-white border border-emerald-200 rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{(() => {
|
||||
// Get tenant's granted features
|
||||
let tenantGranted: Record<string, string> = {};
|
||||
try {
|
||||
const raw = tenant?.licenses;
|
||||
if (typeof raw === 'string') {
|
||||
const js = JSON.parse(raw);
|
||||
if (Array.isArray(js)) js.forEach((k: string) => tenantGranted[k] = "GRANTED");
|
||||
else tenantGranted = js;
|
||||
} else if (raw && typeof raw === 'object') {
|
||||
if (Array.isArray(raw)) raw.forEach((k: string) => tenantGranted[k] = "GRANTED");
|
||||
else tenantGranted = raw;
|
||||
}
|
||||
} catch(_e) {}
|
||||
|
||||
// Render features by category
|
||||
return ['JVC', 'JC', 'XCU', 'XTM', 'IAM'].map(mod => {
|
||||
const filtered = systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM'));
|
||||
if (filtered.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={mod} className="mb-8 last:mb-0">
|
||||
<div className="text-[10px] font-black text-gray-400 mb-3 tracking-[0.2em] border-l-4 border-blue-500 pl-3 uppercase">{mod} CAPABILITIES</div>
|
||||
<div className="space-y-3">
|
||||
{filtered.map((feat: Feature) => {
|
||||
const tStatus = tenantGranted[feat.key] || 'HIDDEN';
|
||||
|
||||
if (tStatus === 'HIDDEN') return null; // Invisible to tenant completely
|
||||
|
||||
if (tStatus === 'UPSELL') {
|
||||
return (
|
||||
<div key={feat.key} className="flex justify-between items-center bg-gray-50 p-4 rounded-lg border border-gray-200 opacity-70">
|
||||
<div>
|
||||
<div className="font-bold text-gray-600 flex items-center gap-2 text-sm">
|
||||
<svg className="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd"></path></svg>
|
||||
{feat.name}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-500 font-mono mt-0.5">{feat.key}</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setActiveTab('billing')}
|
||||
className="text-[10px] font-bold text-blue-600 px-3 py-1 bg-blue-50 border border-blue-200 rounded cursor-pointer hover:bg-blue-100 transition-colors uppercase tracking-wider"
|
||||
>
|
||||
Upgrade Plan
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If GRANTED by Superadmin
|
||||
const currentVal = userLicenses[feat.key] || 'GRANTED';
|
||||
return (
|
||||
<div key={feat.key} className="flex justify-between items-center bg-white p-4 rounded-lg border border-gray-200">
|
||||
<div>
|
||||
<div className="font-bold text-gray-900 text-sm">{feat.name}</div>
|
||||
<div className="text-[10px] text-gray-500 font-mono mt-0.5">{feat.key}</div>
|
||||
</div>
|
||||
<select
|
||||
value={currentVal}
|
||||
onChange={(e) => setUserLicenses({...userLicenses, [feat.key]: e.target.value})}
|
||||
className={`text-[10px] font-bold px-3 py-1.5 rounded outline-none border focus:ring-2 focus:ring-blue-500 ${
|
||||
currentVal === 'GRANTED' ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'
|
||||
}`}
|
||||
>
|
||||
<option value="GRANTED">✅ ALLOWED</option>
|
||||
<option value="HIDDEN">🚫 BLOCKED</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-3">
|
||||
<button onClick={() => setShowLimitsModal(false)} className="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</button>
|
||||
<button onClick={handleSaveLimits} className="px-6 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Save Limits</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
|
||||
export default function TenantTelemetry() {
|
||||
const { t } = useDictionary();
|
||||
const [logs, setLogs] = useState<{ id: string; time: string; action: string; target: string; status: string }[]>([]);
|
||||
const [activeConnections, setActiveConnections] = useState(342);
|
||||
const [bandwidth, setBandwidth] = useState(1.2);
|
||||
const [targetKillId, setTargetKillId] = useState("");
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// WebGL-Style Waveform Simulation using Canvas for Zero-Error absolute performance
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let time = 0;
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
};
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const draw = () => {
|
||||
time += 0.05;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Draw multiple waves
|
||||
for (let i = 0; i < 2; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, centerY);
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const frequency = 0.015 + (i * 0.005);
|
||||
const amplitude = 20 + (i * 10) + Math.sin(time + x * 0.02) * 5;
|
||||
const y = Math.sin(x * frequency + time + i * 2) * amplitude;
|
||||
ctx.lineTo(x, centerY + y);
|
||||
}
|
||||
|
||||
// WhatsApp Emerald to Zoom Blue Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
gradient.addColorStop(0, "rgba(37, 211, 102, 0.5)"); // WhatsApp Green
|
||||
gradient.addColorStop(1, "rgba(11, 92, 255, 0.5)"); // Zoom Blue
|
||||
|
||||
ctx.strokeStyle = gradient;
|
||||
ctx.lineWidth = 2 + i;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch real telemetry data from PANOPTICON backend
|
||||
useEffect(() => {
|
||||
const fetchTelemetry = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/telemetry");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Transform quantum logs into UI log format
|
||||
const formattedLogs = data.logs.map((log: any) => ({
|
||||
id: log.id,
|
||||
time: log.nanoTimestamp ? new Date(log.nanoTimestamp).toISOString().split("T")[1].substring(0, 8) : "00:00:00",
|
||||
action: log.action,
|
||||
target: log.targetId,
|
||||
status: log.action.includes("KILL") ? "TERMINATED" : "OK",
|
||||
}));
|
||||
setLogs(formattedLogs);
|
||||
|
||||
setActiveConnections(data.stats.totalTelemetryRecords);
|
||||
setBandwidth(parseFloat(data.stats.totalBandwidthBytes) / 1024 / 1024 / 1024);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("PANOPTICON Sync Error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTelemetry();
|
||||
const interval = setInterval(fetchTelemetry, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const executeLiveKill = async () => {
|
||||
if (!targetKillId) return;
|
||||
|
||||
// Spectactular Kill Trigger Effect
|
||||
document.body.style.animation = "shake 0.5s cubic-bezier(.36,.07,.19,.97) both";
|
||||
setTimeout(() => document.body.style.animation = "", 500);
|
||||
|
||||
const res = await fetch("/api/telemetry/live-kill", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
targetType: "USER",
|
||||
targetId: targetKillId,
|
||||
reason: "TENANT_ADMIN_OVERRIDE"
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setLogs(prev => [{
|
||||
id: "KILL_" + Date.now(),
|
||||
time: new Date().toISOString().split("T")[1].substring(0, 8),
|
||||
action: "LIVE_KILL_EXECUTE",
|
||||
target: targetKillId,
|
||||
status: "TERMINATED"
|
||||
}, ...prev]);
|
||||
setTargetKillId("");
|
||||
alert("XCU MoQ Ejection Berhasil. Sesi Staf Dihanguskan.");
|
||||
} else {
|
||||
alert("Akses Ditolak: Hanya Berlaku Untuk Grup Internal.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050914] text-gray-200 font-sans selection:bg-blue-500/30 overflow-hidden relative">
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||
}
|
||||
.matrix-bg {
|
||||
background-image:
|
||||
linear-gradient(rgba(11, 92, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(37, 211, 102, 0.03) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* Decorative Grid */}
|
||||
<div className="absolute inset-0 matrix-bg pointer-events-none z-0"></div>
|
||||
|
||||
<div className="relative z-10 p-6 md:p-10 max-w-7xl mx-auto flex flex-col gap-8 h-screen">
|
||||
|
||||
{/* HEADER */}
|
||||
<header className="flex justify-between items-center border-b border-white/10 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-[#25D366] to-[#0b5cff]">
|
||||
TENANT COMMAND CENTER
|
||||
</h1>
|
||||
<p className="text-sm font-mono text-green-400/80 uppercase tracking-widest mt-1">
|
||||
JUMPA.ID • Sandboxed Yurisdiction
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="px-4 py-2 rounded-lg bg-blue-500/10 border border-blue-500/30 flex items-center gap-2 shadow-[0_0_15px_rgba(11,92,255,0.2)]">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
||||
<span className="font-mono text-blue-400 text-sm font-bold">GROUP STABLE</span>
|
||||
</div>
|
||||
<a href="/admin" className="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-sm font-bold transition-all">
|
||||
Tutup Dasbor
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* TOP METRICS */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl p-6 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-green-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
||||
<p className="text-sm font-mono text-gray-500 uppercase">Active Staff Streams</p>
|
||||
<p className="text-5xl font-black mt-2 text-white font-mono tracking-tighter">
|
||||
{activeConnections.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl p-6 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
||||
<p className="text-sm font-mono text-gray-500 uppercase">Group Bandwidth (Gbps)</p>
|
||||
<p className="text-5xl font-black mt-2 text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-500 font-mono tracking-tighter">
|
||||
{bandwidth.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-red-500/20 rounded-2xl p-6 relative shadow-[0_0_30px_rgba(255,0,0,0.1)]">
|
||||
<p className="text-sm font-bold text-red-500 uppercase mb-4 flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L1 21h22M12 6l7.5 13h-15M11 10h2v5h-2M11 16h2v2h-2"/></svg>
|
||||
GUILLOTINE (STAFF ONLY)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={targetKillId}
|
||||
onChange={(e) => setTargetKillId(e.target.value)}
|
||||
placeholder="Target ID (Staf Anda)"
|
||||
className="w-full bg-black/50 border border-red-500/30 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-red-500 text-white placeholder-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={executeLiveKill}
|
||||
className="bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm tracking-widest shadow-[0_0_15px_rgba(255,0,0,0.4)] transition-all hover:scale-105 active:scale-95"
|
||||
>
|
||||
KILL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VISUALIZATION & LOGS */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-1 min-h-0">
|
||||
|
||||
{/* Waveform Canvas */}
|
||||
<div className="lg:col-span-2 bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl relative overflow-hidden flex flex-col">
|
||||
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-black/40">
|
||||
<p className="text-xs font-mono text-green-400">TENANT LOCAL XCU TELEMETRY</p>
|
||||
<div className="flex gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full opacity-70 mix-blend-screen"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hacker Log Stream */}
|
||||
<div className="bg-black/60 backdrop-blur-xl border border-white/5 rounded-2xl flex flex-col overflow-hidden relative shadow-inner">
|
||||
<div className="absolute inset-x-0 top-0 h-8 bg-gradient-to-b from-green-500/10 to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="p-4 border-b border-white/5 bg-[#0a0a0a]">
|
||||
<p className="text-xs font-mono text-gray-400 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-ping"></span>
|
||||
TENANT TRACE LOGS
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 font-mono text-xs flex flex-col gap-1">
|
||||
{logs.map((log, i) => (
|
||||
<div key={log.id + i} className="flex gap-3 hover:bg-white/5 px-2 py-1 rounded transition-colors group">
|
||||
<span className="text-gray-600">[{log.time}]</span>
|
||||
<span className={log.status === 'TERMINATED' ? 'text-red-500 font-bold' : log.status === 'WARN' ? 'text-yellow-500' : 'text-green-400'}>
|
||||
{log.action}
|
||||
</span>
|
||||
<span className="text-blue-300 ml-auto opacity-70 group-hover:opacity-100">{log.target}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user