[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]

This commit is contained in:
TSM.ID
2026-05-25 03:51:34 +07:00
parent e820143b3c
commit 8f1a37129a
354 changed files with 0 additions and 0 deletions
@@ -0,0 +1,696 @@
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
import { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import { useCommercial } from '../context/CommercialContext';
import DangerModal from './DangerModal';
import { useModuleRegistry } from '../context/ModuleRegistryContext';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default function TenantArchitect() {
const { tiers } = useCommercial();
const { registry } = useModuleRegistry();
const API_BASE = import.meta.env.VITE_XCU_API_URL || '';
// Load Tenants dari API Gateway (xcu_iam PostgreSQL)
const [tenants, setTenants] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isProvisioning, setIsProvisioning] = useState(false);
const [apiError, setApiError] = useState(null);
const { cryptographicKey } = useAuth();
// State for Form (Create/Edit)
const [editingId, setEditingId] = useState(null);
const [newTenantName, setNewTenantName] = useState('');
const [customTierName, setCustomTierName] = useState('');
const [selectedModules, setSelectedModules] = useState({});
const [showProvisionForm, setShowProvisionForm] = useState(false);
const handleAutoFill = (tierId) => {
const selectedTier = tiers.find(t => t.id === tierId);
if (!selectedTier) return;
// Convert array of modules to object { m1: true, m2: true }
const newSelection = {};
selectedTier.modules.forEach(mod => {
newSelection[mod] = true;
});
setSelectedModules(newSelection);
};
// Omni-Dimensional Search States
const [moduleSearchQuery, setModuleSearchQuery] = useState('');
const debouncedModuleQuery = useDebounce(moduleSearchQuery, 250);
const [tenantSearchQuery, setTenantSearchQuery] = useState('');
const debouncedTenantQuery = useDebounce(tenantSearchQuery, 250);
// Fetch tenants dari API Gateway
const fetchTenants = async () => {
try {
const res = await fetch(`${API_BASE}/xcu-api/v1/tenants`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
// Transform API data to component format
const mapped = data.map(t => ({
id: t.tenant_key || t.id,
name: t.tenant_name || t.name,
status: t.status || 'PROVISIONED',
baseTierId: t.base_tier_id || t.baseTierId || null,
customModules: t.custom_modules || t.customModules || [],
customTierName: t.custom_tier_name || t.customTierName || '',
created: t.created_at ? t.created_at.split('T')[0] : (t.created || new Date().toISOString().split('T')[0])
}));
setTenants(mapped);
setApiError(null);
console.log(`[XCU TENANT] Loaded ${mapped.length} tenants from API`);
} catch (err) {
console.warn('[XCU TENANT] API offline, using fallback:', err.message);
setApiError(err.message);
// Fallback: seed JUMPA.ID jika API offline
if (tenants.length === 0) {
setTenants([{
id: 'AEGIS-TENANT-80AA3EEBE8-X',
name: 'JUMPA.ID ENTERPRISE',
status: 'PROVISIONED',
baseTierId: 'tier_jumpa_omni',
customModules: [],
customTierName: '',
created: '2026-05-08'
}]);
}
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTenants();
}, []);
// Fungsi Pembantu: Dapatkan Modul Asli untuk Tenant (Auto-Sync dengan Blueprint)
const resolveTenantModules = (tenant) => {
if (tenant.baseTierId) {
const tier = tiers.find(t => t.id === tenant.baseTierId);
if (tier) return tier.modules;
}
return tenant.customModules || [];
};
const resolveTierBadge = (tenant) => {
const activeModules = resolveTenantModules(tenant);
if (!activeModules || activeModules.length === 0) return { name: 'BLIND (0 MODUL)', color: 'var(--accent-red)' };
if (tenant.baseTierId) {
const tierIndex = tiers.findIndex(t => t.id === tenant.baseTierId);
if (tierIndex !== -1) {
let themeColor = 'var(--accent-cyan)';
if (tierIndex === 1) themeColor = 'var(--accent-yellow)';
if (tierIndex === 2) themeColor = 'var(--accent-red)';
if (tierIndex === 3) themeColor = 'var(--accent-green)';
if (tierIndex >= 4) themeColor = 'var(--text-main)';
return { name: tiers[tierIndex].name, color: themeColor };
}
}
const finalName = tenant.customTierName ? `CUSTOM FORGE: ${tenant.customTierName}` : 'CUSTOM FORGE';
return { name: finalName, color: 'var(--accent-purple)' };
};
const toggleModule = (moduleId) => {
setSelectedModules(prev => ({
...prev,
[moduleId]: !prev[moduleId]
}));
};
const handleEditClick = (tenant) => {
setEditingId(tenant.id);
setNewTenantName(tenant.name);
setCustomTierName(tenant.customTierName || '');
setShowProvisionForm(true);
// Konversi array modul kembali ke object untuk state
const modState = {};
tenant.modules.forEach(m => modState[m] = true);
setSelectedModules(modState);
// Auto-scroll ke atas
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const [selectedTenants, setSelectedTenants] = useState([]);
const [deleteModal, setDeleteModal] = useState({ isOpen: false, tenantIds: [] });
const handleDeleteClick = (tenantId) => {
setDeleteModal({ isOpen: true, tenantIds: [tenantId] });
};
const handleBulkDeleteClick = () => {
setDeleteModal({ isOpen: true, tenantIds: selectedTenants });
};
const toggleSelectTenant = (id) => {
setSelectedTenants(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]);
};
const confirmDelete = async () => {
if (deleteModal.tenantIds.length > 0) {
// Delete via API for each tenant
for (const tenantId of deleteModal.tenantIds) {
try {
await fetch(`${API_BASE}/xcu-api/v1/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' });
} catch (err) {
console.error('[XCU TENANT] Delete error:', err.message);
}
}
// Refresh from API
await fetchTenants();
if (deleteModal.tenantIds.includes(editingId)) {
cancelEdit();
}
setSelectedTenants([]);
}
setDeleteModal({ isOpen: false, tenantIds: [] });
};
const cancelDelete = () => {
setDeleteModal({ isOpen: false, tenantIds: [] });
};
const cancelEdit = () => {
setEditingId(null);
setNewTenantName('');
setCustomTierName('');
setSelectedModules({});
setShowProvisionForm(false);
};
const saveTenant = async () => {
if (!newTenantName) return alert("Nama Tenant tidak boleh kosong.");
setIsProvisioning(true);
// Konversi object { id: true/false } menjadi array of allowed IDs
const allowedModules = Object.keys(selectedModules).filter(id => selectedModules[id]);
// Check if the allowedModules matches any Tier exactly
let matchedTierId = null;
const sortedAllowed = [...allowedModules].sort().join(',');
for (const tier of tiers) {
if ([...tier.modules].sort().join(',') === sortedAllowed) {
matchedTierId = tier.id;
break;
}
}
try {
if (editingId) {
// UPDATE via API
await fetch(`${API_BASE}/xcu-api/v1/tenants/${encodeURIComponent(editingId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenant_name: newTenantName,
base_tier_id: matchedTierId,
custom_modules: matchedTierId ? [] : allowedModules,
custom_tier_name: matchedTierId ? '' : customTierName
})
});
} else {
// CREATE via API
const tenantKey = 'AEGIS-TENANT-' + newTenantName.replace(/[^A-Z0-9]/gi, '').toUpperCase().slice(0, 10) + '-' + Math.random().toString(16).substr(2, 4).toUpperCase();
await fetch(`${API_BASE}/xcu-api/v1/tenants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenant_key: tenantKey,
tenant_name: newTenantName,
base_tier_id: matchedTierId,
custom_modules: matchedTierId ? [] : allowedModules,
custom_tier_name: matchedTierId ? '' : customTierName
})
});
}
// Refresh from API
await fetchTenants();
} catch (err) {
console.error('[XCU TENANT] Save error:', err.message);
setApiError(err.message);
}
cancelEdit();
setIsProvisioning(false);
};
const isEditMode = editingId !== null;
const borderColor = isEditMode ? 'var(--accent-yellow)' : 'rgba(0, 243, 255, 0.2)';
const themeColor = isEditMode ? 'var(--accent-yellow)' : 'var(--accent-cyan)';
const filteredTenants = tenants.filter(t => {
if (!debouncedTenantQuery) return true;
const q = debouncedTenantQuery.toLowerCase();
if (t.name.toLowerCase().includes(q) || t.id.toLowerCase().includes(q)) return true;
// Omni-Search: Cek nama modul aslinya dari Registry
const activeModules = resolveTenantModules(t);
return activeModules.some(modId => {
for (let coreKey in registry) {
const found = registry[coreKey].modules.find(m => m.id === modId);
if (found && found.name.toLowerCase().includes(q)) return true;
}
return false;
});
});
const handleSelectAll = () => {
if (selectedTenants.length === filteredTenants.length && filteredTenants.length > 0) {
setSelectedTenants([]);
} else {
setSelectedTenants(filteredTenants.map(t => t.id));
}
};
const handleGenerateTether = (tenant) => {
// Generate Quantum Tether (.xcu) file
const tetherData = {
tenantId: tenant.id,
tenantName: tenant.name,
baseTierId: tenant.baseTierId,
customModules: tenant.customModules,
endpoint: "wss://xcu ULTRA.ultramodul.xyz/quantum-relay",
signature: btoa(tenant.id + "-" + Date.now())
};
const blob = new Blob([JSON.stringify(tetherData, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${tenant.id.toLowerCase()}.xcu`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return (
<div className="glass-panel" style={{padding: '30px', borderColor: 'var(--accent-cyan)'}}>
<style>{`
@keyframes laserScan {
0% { transform: scaleX(0); transform-origin: left; opacity: 0.8;}
50% { transform: scaleX(1); transform-origin: left; opacity: 1;}
50.1% { transform: scaleX(1); transform-origin: right; opacity: 1;}
100% { transform: scaleX(0); transform-origin: right; opacity: 0.8;}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0,0,0,0.2);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0, 243, 255, 0.3);
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(0, 243, 255, 0.6);
}
`}</style>
<DangerModal
isOpen={deleteModal.isOpen}
title={deleteModal.tenantIds.length > 1 ? "MASS ANNIHILATION PROTOCOL" : "REVOKE COMMERCIAL GATEWAY"}
message={`PERINGATAN ABSOLUT: Anda akan memusnahkan ${deleteModal.tenantIds.length} Entitas Klien secara permanen dari Matriks. Semua layanan WebRTC dan Sinkronisasi Data mereka akan MATI TOTAL tanpa bisa dipulihkan. Lanjutkan eksekusi?`}
onConfirm={confirmDelete}
onCancel={cancelDelete}
confirmText={deleteModal.tenantIds.length > 1 ? "EXECUTE MASS ANNIHILATION" : "ANNIHILATE TENANT"}
/>
<div style={{marginBottom: '30px'}}>
<h2 style={{color: 'var(--text-main)', marginBottom: '8px', fontFamily: 'monospace', letterSpacing: '2px'}}>THE TENANT ARCHITECT</h2>
<p style={{color: 'var(--text-muted)'}}>Source: <span style={{color: apiError ? 'var(--accent-yellow)' : 'var(--accent-green)', fontWeight: 'bold'}}>{apiError ? 'Static Fallback' : 'xcu_iam PostgreSQL'}</span> | <span style={{color: 'var(--accent-cyan)'}}>{tenants.length}</span> Tenants <button onClick={fetchTenants} style={{marginLeft:'10px',padding:'4px 12px',background:'transparent',border:'1px solid var(--accent-green)',color:'var(--accent-green)',borderRadius:'4px',cursor:'pointer',fontFamily:'monospace',fontSize:'0.75rem'}}> REFRESH</button></p>
</div>
{!showProvisionForm && !isEditMode && (
<div style={{marginBottom: '40px'}}>
<button
onClick={() => setShowProvisionForm(true)}
style={{
width: '100%', padding: '20px', background: 'rgba(168, 85, 247, 0.1)',
border: '1px dashed var(--accent-purple)', color: 'var(--accent-purple)', borderRadius: '12px',
cursor: 'pointer', fontFamily: 'monospace', fontSize: '1.2rem',
letterSpacing: '3px', fontWeight: 'bold', transition: 'all 0.3s ease',
boxShadow: '0 0 20px rgba(168, 85, 247, 0.2)'
}}
onMouseOver={(e) => {
e.currentTarget.style.background = 'rgba(168, 85, 247, 0.3)';
e.currentTarget.style.boxShadow = '0 0 40px rgba(168, 85, 247, 0.6)';
e.currentTarget.style.color = 'var(--text-main)';
}}
onMouseOut={(e) => {
e.currentTarget.style.background = 'rgba(168, 85, 247, 0.1)';
e.currentTarget.style.boxShadow = '0 0 20px rgba(168, 85, 247, 0.2)';
e.currentTarget.style.color = 'var(--accent-purple)';
}}
>
+ SPAWN NEW TENANT ENTITY
</button>
</div>
)}
{(showProvisionForm || isEditMode) && (
<div style={{background: 'var(--panel-bg)', padding: '25px', borderRadius: '12px', border: `1px solid ${borderColor}`, marginBottom: '40px', transition: 'border 0.3s ease', animation: 'fadeIn 0.3s ease-out'}}>
<h3 style={{color: themeColor, marginBottom: '20px', fontSize: '1rem', display: 'flex', justifyContent: 'space-between'}}>
{isEditMode ? `[ EDITING TENANT: ${editingId} ]` : 'PROVISION NEW TENANT'}
<span style={{color: 'var(--text-muted)', cursor: 'pointer', fontSize: '0.8rem'}} onClick={cancelEdit}>[ Batal / Tutup ]</span>
</h3>
<div style={{marginBottom: '20px'}}>
<label style={{display: 'block', color: 'var(--text-muted)', fontSize: '0.8rem', marginBottom: '8px', letterSpacing: '1px'}}>TENANT ENTITY NAME</label>
<input
type="text"
value={newTenantName}
onChange={(e) => setNewTenantName(e.target.value)}
placeholder="e.g., JUMPA.ID ENTERPRISE"
style={{
width: '100%', padding: '12px', background: 'var(--hover-bg)',
border: `1px solid ${isEditMode ? 'rgba(255, 234, 0, 0.3)' : 'rgba(255,255,255,0.1)'}`, color: 'var(--text-main)',
borderRadius: '6px', fontFamily: 'monospace', outline: 'none'
}}
/>
</div>
<div style={{display: 'flex', gap: '20px', marginBottom: '25px'}}>
<div style={{flex: 1}}>
<label style={{display: 'block', color: 'var(--text-muted)', fontSize: '0.8rem', marginBottom: '8px', letterSpacing: '1px'}}>AUTO-FILL DARI PAKET KOMERSIAL</label>
<div style={{display: 'flex', gap: '10px'}}>
<select
onChange={(e) => handleAutoFill(e.target.value)}
style={{
flex: 1, padding: '12px', background: 'var(--hover-bg)',
border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-main)',
borderRadius: '6px', fontFamily: 'monospace', outline: 'none',
cursor: 'pointer'
}}
defaultValue=""
>
<option value="" disabled style={{color: '#000'}}>-- Pilih Preset Paket (Opsional) --</option>
{tiers.map(t => (
<option key={t.id} value={t.id} style={{color: '#000'}}>{t.name}</option>
))}
</select>
<button
onClick={() => setSelectedModules({})}
style={{
padding: '0 20px', background: 'rgba(255,0,60,0.1)', border: '1px solid var(--accent-red)',
color: 'var(--accent-red)', borderRadius: '6px', cursor: 'pointer', fontFamily: 'monospace'
}}
>
RESET
</button>
</div>
</div>
<div style={{flex: 1}}>
<label style={{display: 'block', color: 'var(--accent-purple)', fontSize: '0.8rem', marginBottom: '8px', letterSpacing: '1px'}}>NAMA PAKET KUSTOM (Jika Custom Forge)</label>
<input
type="text"
value={customTierName}
onChange={(e) => setCustomTierName(e.target.value)}
placeholder="e.g., SS9 GOV MATRIX"
style={{
width: '100%', padding: '12px', background: 'rgba(168,85,247,0.05)',
border: '1px solid rgba(168,85,247,0.3)', color: 'var(--text-main)',
borderRadius: '6px', fontFamily: 'monospace', outline: 'none'
}}
/>
</div>
</div>
<div style={{marginBottom: '25px'}}>
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: '15px'}}>
<label style={{display: 'block', color: 'var(--text-muted)', fontSize: '0.8rem', letterSpacing: '1px'}}>MODUL 1-75 ALLOCATION MATRIX</label>
<div style={{position: 'relative', width: '300px'}}>
<input
type="text"
placeholder="Decryption Search (e.g., 'Aegis')..."
value={moduleSearchQuery}
onChange={(e) => setModuleSearchQuery(e.target.value)}
style={{
width: '100%', padding: '8px 12px', background: 'rgba(0, 243, 255, 0.05)',
border: '1px solid rgba(0, 243, 255, 0.3)', color: 'var(--accent-cyan)',
borderRadius: '6px', fontFamily: 'monospace', outline: 'none', fontSize: '0.8rem',
boxShadow: moduleSearchQuery ? '0 0 10px rgba(0,243,255,0.2)' : 'none',
transition: 'all 0.3s'
}}
/>
{moduleSearchQuery && (
<div style={{
position: 'absolute', bottom: 0, left: 0, height: '2px', background: 'var(--accent-cyan)',
animation: 'laserScan 1.5s infinite', width: '100%'
}}></div>
)}
</div>
</div>
<div style={{display: 'flex', flexDirection: 'column', gap: '20px', maxHeight: '600px', overflowY: 'auto', paddingRight: '10px'}} className="custom-scrollbar">
{Object.keys(registry).map(coreKey => {
const core = registry[coreKey];
// Omni-Index Filtering
const filteredModules = core.modules.filter(mod =>
!debouncedModuleQuery ||
mod.name.toLowerCase().includes(debouncedModuleQuery.toLowerCase()) ||
mod.id.toLowerCase().includes(debouncedModuleQuery.toLowerCase())
);
if (filteredModules.length === 0) return null;
return (
<div key={coreKey} style={{background: 'var(--hover-bg)', padding: '15px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.05)', animation: 'fadeIn 0.4s ease-out'}}>
<h4 style={{color: 'var(--text-muted)', fontSize: '0.75rem', marginBottom: '12px', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '5px'}}>
{core.name} <span style={{color: themeColor, float: 'right'}}>[ {filteredModules.length} ]</span>
</h4>
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px'}}>
{filteredModules.map(mod => (
<div
key={mod.id}
onClick={() => toggleModule(mod.id)}
style={{
padding: '10px',
background: selectedModules[mod.id] ? (isEditMode ? 'rgba(255, 234, 0, 0.15)' : 'rgba(0, 243, 255, 0.15)') : 'rgba(255,255,255,0.02)',
border: `1px solid ${selectedModules[mod.id] ? themeColor : 'rgba(255,255,255,0.1)'}`,
borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', transition: 'all 0.2s',
boxShadow: debouncedModuleQuery && selectedModules[mod.id] ? `0 0 15px ${themeColor}40` : 'none'
}}
>
<div style={{
width: '16px', height: '16px', borderRadius: '4px',
border: `1px solid ${selectedModules[mod.id] ? themeColor : '#555'}`,
background: selectedModules[mod.id] ? themeColor : 'transparent',
marginRight: '10px'
}}></div>
<span style={{color: selectedModules[mod.id] ? 'var(--text-main)' : 'var(--text-muted)', fontSize: '0.8rem'}}>{mod.name}</span>
</div>
))}
</div>
</div>
);
})}
</div>
</div>
<button
className={`btn-primary ${isProvisioning ? 'processing' : ''}`}
onClick={saveTenant}
disabled={isProvisioning || !newTenantName}
style={{
width: '100%', padding: '15px', fontSize: '1rem',
background: isEditMode ? 'rgba(255, 234, 0, 0.1)' : 'transparent',
borderColor: themeColor, color: themeColor
}}
>
{isProvisioning ? '[ SYNCHRONIZING MATRIX... ]' : (isEditMode ? 'UPDATE TENANT PROTOCOL' : 'ENGAGE PROVISIONING PROTOCOL')}
</button>
</div>
)}
<div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: '20px', marginTop: '40px'}}>
<h3 style={{color: 'var(--text-main)', fontSize: '1rem', margin: 0}}>GLOBAL ACTIVE TENANTS</h3>
<div style={{position: 'relative', width: '300px'}}>
<input
type="text"
placeholder="Search Klien / ID / Modul..."
value={tenantSearchQuery}
onChange={(e) => setTenantSearchQuery(e.target.value)}
style={{
width: '100%', padding: '8px 12px', background: 'rgba(255, 0, 60, 0.05)',
border: '1px solid rgba(255, 0, 60, 0.3)', color: 'var(--accent-red)',
borderRadius: '6px', fontFamily: 'monospace', outline: 'none', fontSize: '0.8rem',
boxShadow: tenantSearchQuery ? '0 0 10px rgba(255,0,60,0.2)' : 'none',
transition: 'all 0.3s'
}}
/>
{tenantSearchQuery && (
<div style={{
position: 'absolute', bottom: 0, left: 0, height: '2px', background: 'var(--accent-red)',
animation: 'laserScan 1.5s infinite reverse', width: '100%'
}}></div>
)}
</div>
</div>
{isLoading ? (
<div style={{textAlign: 'center', padding: '30px', color: 'var(--accent-cyan)'}}>Menarik Struktur Omni-Tenant...</div>
) : (
<table className="glass-table">
<thead>
<tr>
<th style={{width: '40px', textAlign: 'center'}}>
<input
type="checkbox"
onChange={handleSelectAll}
checked={selectedTenants.length === filteredTenants.length && filteredTenants.length > 0}
style={{cursor: 'pointer', accentColor: 'var(--accent-red)'}}
/>
</th>
<th>Cryptographic Key ID</th>
<th>Tenant Name</th>
<th>Active Modules</th>
<th>Override</th>
</tr>
</thead>
<tbody>
{filteredTenants.length === 0 ? (
<tr>
<td colSpan="5" style={{textAlign: 'center', color: 'var(--text-muted)', padding: '30px'}}>Tidak ada Entitas Klien yang cocok dengan pencarian.</td>
</tr>
) : (
filteredTenants.map((t, idx) => {
const badge = resolveTierBadge(t);
const activeModules = resolveTenantModules(t);
return (
<tr key={t.id} style={{
background: editingId === t.id ? 'rgba(255, 234, 0, 0.05)' : (selectedTenants.includes(t.id) ? 'rgba(255, 0, 60, 0.1)' : 'transparent'),
animation: 'fadeIn 0.5s ease-out',
transition: 'background 0.3s'
}}>
<td style={{textAlign: 'center'}}>
<input
type="checkbox"
checked={selectedTenants.includes(t.id)}
onChange={() => toggleSelectTenant(t.id)}
style={{cursor: 'pointer', accentColor: 'var(--accent-red)'}}
/>
</td>
<td style={{fontFamily: 'monospace', color: editingId === t.id ? 'var(--accent-yellow)' : 'var(--accent-cyan)'}}>{t.id}</td>
<td style={{fontWeight: 'bold', color: 'var(--text-main)'}}>
<div style={{marginBottom: '5px'}}>{t.name}</div>
<div style={{
display: 'inline-block',
background: `${badge.color}20`,
border: `1px solid ${badge.color}`,
color: badge.color,
padding: '2px 8px',
borderRadius: '4px',
fontSize: '0.7rem',
fontFamily: 'monospace',
boxShadow: `0 0 10px ${badge.color}40`,
textTransform: 'uppercase',
letterSpacing: '1px'
}}>
{badge.name}
</div>
</td>
<td style={{color: 'var(--text-muted)', fontSize: '0.8rem'}}>
{activeModules && activeModules.length > 0 ? (
<div style={{display: 'flex', flexWrap: 'wrap', gap: '4px'}}>
{activeModules.map(m => {
let modName = m;
for (let coreKey in registry) {
const found = registry[coreKey].modules.find(x => x.id === m);
if (found) modName = found.name;
}
const isMatch = debouncedTenantQuery && modName.toLowerCase().includes(debouncedTenantQuery.toLowerCase());
return (
<span key={m} style={{
background: isMatch ? 'rgba(255,0,60,0.2)' : 'var(--bg-panel)',
border: isMatch ? '1px solid var(--accent-red)' : '1px solid var(--table-border)',
color: isMatch ? 'var(--text-main)' : 'var(--text-main)',
padding: '2px 6px', borderRadius: '4px'
}}>
{modName}
</span>
);
})}
</div>
) : <span style={{color: 'var(--accent-red)'}}>BLIND (0 MODUL)</span>}
</td>
<td>
<div style={{display: 'flex', gap: '8px', flexWrap: 'wrap'}}>
<button onClick={() => handleGenerateTether(t)} style={{
background: 'rgba(0, 243, 255, 0.1)', border: '1px solid var(--accent-cyan)',
color: 'var(--accent-cyan)', padding: '6px 12px', borderRadius: '4px', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem', boxShadow: '0 0 10px rgba(0, 243, 255, 0.2)'
}}>GENERATE TETHER (.xcu)</button>
<button onClick={() => handleEditClick(t)} style={{
background: 'rgba(255,234,0,0.1)', border: '1px solid var(--accent-yellow)',
color: 'var(--accent-yellow)', padding: '6px 12px', borderRadius: '4px', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem'
}}>EDIT / REFORGE</button>
<button onClick={() => handleDeleteClick(t.id)} style={{
background: 'rgba(255,0,60,0.1)', border: '1px solid var(--accent-red)',
color: 'var(--accent-red)', padding: '6px 12px', borderRadius: '4px', cursor: 'pointer',
fontFamily: 'monospace', fontSize: '0.75rem'
}}>REVOKE</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
)}
{selectedTenants.length > 0 && (
<div style={{
position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)',
background: 'var(--panel-bg)', border: '1px solid var(--accent-red)', borderRadius: '12px',
padding: '20px 40px', display: 'flex', alignItems: 'center', gap: '30px',
boxShadow: '0 0 30px rgba(255,0,60,0.5)', zIndex: 1000, animation: 'fadeIn 0.3s ease-out'
}}>
<div style={{color: 'var(--accent-red)', fontFamily: 'monospace', fontSize: '1.2rem', fontWeight: 'bold'}}>
{selectedTenants.length} ENTITAS KLIEN SIAP DIMUSNAHKAN
</div>
<button
onClick={handleBulkDeleteClick}
style={{
background: 'var(--accent-red)', color: '#000', border: 'none', padding: '10px 20px',
borderRadius: '6px', fontWeight: 'bold', fontFamily: 'monospace', cursor: 'pointer',
textTransform: 'uppercase', letterSpacing: '1px', boxShadow: '0 0 15px rgba(255,0,60,0.8)'
}}
>
MASS ANNIHILATION PROTOCOL
</button>
</div>
)}
</div>
);
}