feat(source2): push all latest files - device labeling, help system, proxy docs, database isolation fix

- Added DOKUMENTASI-FILTER-PER-SITE.md (site isolation docs)
- Fixed start-with-env.js to force-load .env.production
- Fixed MONGODB_URI hostname from mongodb-netify to mongodb.prod.proit.id
- Updated .gitignore to exclude sensitive scripts and credential files
- Minor UI and labeling improvements
This commit is contained in:
rafif committed 2026-07-29 14:14:29 +07:00
1 parent a403f752f3
commit dd4c8f6876
385 files changed
+31016 -8268

No files matched your search

@@ -3,7 +3,7 @@
import { AgentModals } from "@/components/admin/AgentModals";
import ExternalAccountModal from "@/components/admin/ExternalAccountModal";
import AgentLocationModal from "@/components/admin/AgentLocationModal";
import { Agent } from "@/lib/actions/agents";
import { Agent } from "@/lib/actions/agentsCore";
import { type ManagedUser } from "@/lib/admin-api";
interface AgentsPageModalsProps {
@@ -101,6 +101,7 @@ export default function AgentsPageModals({
user={externalModalUser}
targetSiteUuid={targetSiteUuid}
targetCreatedBy={targetCreatedBy}
modalContext="agents"
/>
<AgentLocationModal
@@ -2,24 +2,37 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { DataTable, Column } from "@/components/ui/DataTable";
import { Loader2 } from "lucide-react";
import { Agent } from "@/lib/actions/agents";
import { Loader2, Plus } from "lucide-react";
import { Agent } from "@/lib/actions/agentsCore";
interface AgentsTableSectionProps {
agents: Agent[];
isLoading: boolean;
columns: Column<Agent>[];
role: string | null;
onProvisionClick?: () => void;
}
export default function AgentsTableSection({
agents,
isLoading,
columns,
role,
onProvisionClick,
}: AgentsTableSectionProps) {
return (
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-white">Provisioned Network Agents ({agents.length})</CardTitle>
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && onProvisionClick && (
<button
onClick={onProvisionClick}
className="flex items-center gap-2 bg-primary hover:bg-primary/95 text-primary-foreground px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-lg shadow-blue-500/10"
>
<Plus className="w-4 h-4" />
Provision Agent
</button>
)}
</CardHeader>
<CardContent>
{isLoading ? (
@@ -51,7 +51,7 @@ export default function ExternalAccountsSection({
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
Add Technical Account
</button>
</CardHeader>
<CardContent>
@@ -91,7 +91,7 @@ export default function ExternalAccountsSection({
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
Add Technical Account
</button>
</CardHeader>
<CardContent>
@@ -117,21 +117,21 @@ export default function ExternalAccountsSection({
</CardContent>
</Card>
{/* Table 3: Nexus Accounts */}
{/* Table 3: Office Accounts */}
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white">Nexus Tenant External Accounts ({nexusUsers.length})</CardTitle>
<CardTitle className="text-white">Office Tenant External Accounts ({nexusUsers.length})</CardTitle>
<button
onClick={() => {
setExternalModalUser(null);
setTargetSiteUuid('d7902405_0dc2_458b_8584_ed4d24b64f24');
setTargetCreatedBy('nexus');
setTargetSiteUuid('1959bb55_045b_47c7_bbdd_f33b7db197b9');
setTargetCreatedBy('office');
setExternalModalOpen(true);
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
Add Technical Account
</button>
</CardHeader>
<CardContent>
@@ -175,7 +175,7 @@ export default function ExternalAccountsSection({
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30 rounded-lg transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Add External Account
Add Technical Account
</button>
</CardHeader>
<CardContent>
@@ -203,5 +203,40 @@ export default function ExternalAccountsSection({
);
}
// EXECUTIVE: read-only view of all accounts, no create/edit buttons
if (role === 'EXECUTIVE') {
return (
<Card className="bg-card/50 backdrop-blur-sm border-border/50">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-white flex items-center gap-2">
<span>External Accounts Directory ({externalUsers.length})</span>
</CardTitle>
<span className="text-xs text-slate-500 italic px-3 py-1.5 border border-slate-700/50 rounded-lg">Read-Only View</span>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
) : (
<DataTable
data={externalUsers}
columns={externalColumns}
searchPlaceholder="Search accounts..."
searchFilter={(row, query) => {
const q = query.toLowerCase();
return (
(row.username || "").toLowerCase().includes(q) ||
(row.account_name || "").toLowerCase().includes(q) ||
(row.role || "").toLowerCase().includes(q)
);
}}
/>
)}
</CardContent>
</Card>
);
}
return null;
}
@@ -21,29 +21,29 @@ export default function UptimeStatsCards({
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<Server className="w-12 h-12 text-blue-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Total Agents</span>
<span className="text-2xl font-black text-white tracking-tight">{totalAgents}</span>
<span className="text-slate-400 uppercase tracking-wider mb-1 text-[10px] font-medium">Total Agents</span>
<span className="text-2xl font-bold text-white tracking-tight">{totalAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<CheckCircle2 className="w-12 h-12 text-emerald-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Online Agents</span>
<span className="text-2xl font-black text-emerald-400 tracking-tight">{onlineAgents}</span>
<span className="text-slate-400 uppercase tracking-wider mb-1 text-[10px] font-medium">Online Agents</span>
<span className="text-2xl font-bold text-emerald-400 tracking-tight">{onlineAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<XCircle className="w-12 h-12 text-red-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Offline Agents</span>
<span className="text-2xl font-black text-red-400 tracking-tight">{offlineAgents}</span>
<span className="text-slate-400 uppercase tracking-wider mb-1 text-[10px] font-medium">Offline Agents</span>
<span className="text-2xl font-bold text-red-400 tracking-tight">{offlineAgents}</span>
</div>
<div className="bg-slate-900/50 backdrop-blur-sm border border-slate-800/80 p-5 rounded-2xl flex flex-col relative overflow-hidden group">
<div className="absolute top-0 right-0 p-3 opacity-10 group-hover:opacity-20 transition-opacity">
<Activity className="w-12 h-12 text-cyan-400" />
</div>
<span className="text-slate-400 uppercase tracking-wider mb-1">Avg. Historical Uptime</span>
<span className="text-2xl font-black text-cyan-400 tracking-tight">{avgUptime.toFixed(2)}%</span>
<span className="text-slate-400 uppercase tracking-wider mb-1 text-[10px] font-medium">Avg. Historical Uptime</span>
<span className="text-2xl font-bold text-cyan-400 tracking-tight">{avgUptime.toFixed(2)}%</span>
</div>
</div>
);
+76 -58
View File
@@ -1,7 +1,7 @@
"use client";
import { Column } from "@/components/ui/DataTable";
import { Agent } from "@/lib/actions/agents";
import { Agent } from "@/lib/actions/agentsCore";
import { Badge } from "@/components/ui/Badge";
import { resolveAgentLabel, type ManagedUser } from "@/lib/admin-api";
import { Loader2, Trash2, CheckCircle2, XCircle, ChevronRight, UserCog, Eye, MapPin } from "lucide-react";
@@ -17,8 +17,9 @@ interface GetColumnsProps {
setAccountModalAgent: (agent: { uuid: string; label: string } | null) => void;
handleViewAs: (uuid: string, label: string) => void;
openDelete: (agent: Agent) => void;
onEditLocation?: (agentUuid: string, agentLabel: string) => void;
locations?: { agent_uuid: string; latitude: number; longitude: number; label?: string }[];
handleEditLocation?: (agentUuid: string, agentLabel: string) => void;
/** Set of agent UUIDs that already have a location saved */
locationUuids?: Set<string>;
}
export function getAgentColumns({
@@ -32,14 +33,13 @@ export function getAgentColumns({
setAccountModalAgent,
handleViewAs,
openDelete,
onEditLocation,
locations = [],
handleEditLocation,
locationUuids = new Set(),
}: GetColumnsProps): Column<Agent>[] {
const isSuperAdmin = role === 'SUPER_ADMIN';
const cols: Column<Agent>[] = [
{
header: "UUID / Serial",
className: isSuperAdmin ? "w-[12%] text-center" : "w-[14%] text-center",
className: "w-[12%] text-center",
accessor: (row) => (
<button onClick={() => setDetailAgent(row)} className="flex items-center justify-center gap-2 group text-center mx-auto" title="Click to view agent details">
<span className="font-mono text-xs text-blue-400 group-hover:text-blue-300 transition-colors">{row.uuid || row.serial}</span>
@@ -49,14 +49,19 @@ export function getAgentColumns({
},
{
header: "Label",
className: isSuperAdmin ? "w-[18%] text-center" : "w-[24%] text-center",
className: "w-[15%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
const hasAccount = managedUsers.some(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER');
const displayLabel = dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid);
return (
<div className="flex items-center justify-center gap-2 mx-auto max-w-full">
<button onClick={() => setDetailAgent(row)} className="text-center hover:text-white transition-colors truncate">
<button
onClick={() => setDetailAgent(row)}
className="text-center hover:text-white transition-colors truncate"
title={displayLabel}
>
{dynamicLabel !== uuid ? dynamicLabel : (row.label || <span className="text-muted-foreground italic font-mono text-xs">{uuid}</span>)}
</button>
{!hasAccount && (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
@@ -68,30 +73,33 @@ export function getAgentColumns({
},
{
header: "Provisioned",
className: isSuperAdmin ? "w-[10%] text-center" : "w-[11%] text-center",
className: "w-[11%] text-center",
accessor: (row) => row.provisioned ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge>
},
{
header: "Activated",
className: isSuperAdmin ? "w-[10%] text-center" : "w-[11%] text-center",
className: "w-[11%] text-center",
accessor: (row) => row.activated ? <Badge variant="green"><CheckCircle2 className="w-3 h-3 mr-1" /> Yes</Badge> : <Badge variant="red"><XCircle className="w-3 h-3 mr-1" /> No</Badge>
},
{
header: "Status",
className: "w-[12%] text-center",
header: "Traffic Status",
className: "w-[15%] text-center",
accessor: (row) => {
const statusText = row.last_seen_at?.human || '-';
if (statusText === 'Online') {
return <span className="text-emerald-400 font-medium flex items-center justify-center gap-1.5"><div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div> Online</span>;
} else if (statusText === 'Offline') {
return <span className="text-red-400 font-medium">Offline</span>;
if (statusText.startsWith('Last seen:')) {
return (
<span className="text-emerald-400 font-medium flex items-center justify-center gap-1.5 mx-auto">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div>
{statusText}
</span>
);
} else {
return <span className="text-slate-400 text-xs">{statusText}</span>;
return <span className="text-slate-400 text-xs font-normal">{statusText}</span>;
}
}
},
{
header: "Historical Uptime",
header: "Avg Uptime",
className: "w-[10%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
@@ -99,60 +107,75 @@ export function getAgentColumns({
let color = "text-emerald-400";
if (uptime < 90) color = "text-red-400";
else if (uptime < 98) color = "text-amber-400";
return <span className={`font-mono font-bold ${color}`}>{uptime.toFixed(1)}%</span>;
return <span className={`font-mono font-medium text-xs tracking-wide ${color}`}>{uptime.toFixed(1)}%</span>;
}
},
{
header: "Actions",
className: "w-[18%] text-center",
className: "w-[16%] text-center",
accessor: (row) => {
const uuid = row.uuid || row.serial;
const dynamicLabel = resolveAgentLabel(uuid, managedUsers);
const existingUser = managedUsers.find(u => u.agent_uuid === uuid && u.role === 'AGENT_VIEWER') || null;
const isViewAsLoading = viewAsLoading === uuid;
const agentLabel = dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid);
return (
<div className="flex items-center justify-center gap-1.5">
<button onClick={() => setDetailAgent(row)} className="p-1.5 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors" title="Detail Agent">
<div className="flex items-center justify-center gap-1 mx-auto">
<button onClick={() => setDetailAgent(row)} className="p-1 text-blue-400 hover:text-blue-300 hover:bg-blue-950/30 rounded transition-colors" title="Detail Agent">
<ChevronRight className="w-4 h-4" />
</button>
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<>
{onEditLocation && (() => {
const hasCoords = locations.some(l => l.agent_uuid === uuid);
return (
<button
onClick={(e) => { e.stopPropagation(); onEditLocation(uuid, dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid)); }}
className={`p-1.5 rounded transition-colors ${
hasCoords
? "text-blue-400 hover:text-blue-300 hover:bg-blue-950/30"
: "text-red-400/60 hover:text-red-400 hover:bg-red-950/30"
}`}
title={hasCoords ? "Edit Geolocation (Dikonfigurasi)" : "Configure Geolocation (Belum Dikonfigurasi)"}
>
<MapPin className="w-4 h-4" />
</button>
);
})()}
<button
onClick={(e) => { e.stopPropagation(); setAccountModalAgent({ uuid, label: dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid) }); setAccountModalOpen(true); }}
className="p-1.5 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-950/30 rounded transition-colors"
onClick={(e) => { e.stopPropagation(); setAccountModalAgent({ uuid, label: agentLabel }); setAccountModalOpen(true); }}
className="p-1 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-950/30 rounded transition-colors"
title={existingUser ? 'Edit Agent Account' : 'Create Agent Account'}
>
<UserCog className="w-4 h-4" />
</button>
{handleEditLocation && (() => {
const hasLocation = locationUuids.has(uuid);
return (
<button
onClick={(e) => { e.stopPropagation(); handleEditLocation(uuid, agentLabel); }}
className={`relative p-1 rounded transition-colors ${
hasLocation
? "text-teal-400 hover:text-teal-300 hover:bg-teal-950/30"
: "text-slate-500 hover:text-indigo-400 hover:bg-indigo-950/30"
}`}
title={hasLocation ? "✅ Location set — click to update" : "⚠️ No location — click to set coordinates"}
>
<MapPin className="w-4 h-4" />
{hasLocation && (
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full bg-teal-400 border border-slate-900" />
)}
</button>
);
})()}
<button
onClick={(e) => { e.stopPropagation(); handleViewAs(uuid, dynamicLabel !== uuid ? dynamicLabel : (row.label || uuid)); }}
onClick={(e) => { e.stopPropagation(); handleViewAs(uuid, agentLabel); }}
disabled={isViewAsLoading}
className="p-1.5 text-amber-400 hover:text-amber-300 hover:bg-amber-950/30 rounded transition-colors disabled:opacity-50"
className="p-1 text-amber-400 hover:text-amber-300 hover:bg-amber-950/30 rounded transition-colors disabled:opacity-50"
title="View As Agent"
>
{isViewAsLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
</button>
<button onClick={(e) => { e.stopPropagation(); openDelete(row); }} className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors" title="Delete">
<button onClick={(e) => { e.stopPropagation(); openDelete(row); }} className="p-1 text-red-400 hover:text-red-300 hover:bg-red-950/30 rounded transition-colors" title="Delete">
<Trash2 className="w-4 h-4" />
</button>
</>
)}
{/* COMPANY_ADMIN dan COMPANY_OPERATOR: hanya View-As */}
{(role === 'COMPANY_ADMIN' || role === 'COMPANY_OPERATOR') && (
<button
onClick={(e) => { e.stopPropagation(); handleViewAs(uuid, agentLabel); }}
disabled={isViewAsLoading}
className="p-1 text-amber-400 hover:text-amber-300 hover:bg-amber-950/30 rounded transition-colors disabled:opacity-50"
title="View As Agent — see dashboard from this agent's perspective"
>
{isViewAsLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
</button>
)}
</div>
);
}
@@ -166,7 +189,15 @@ export function getAgentColumns({
accessor: (row) => {
const uuid = row.uuid || row.serial;
const sizeMB = storageMap[uuid] ?? 0;
return <span className="font-mono text-slate-300 font-semibold">{formatStorageSize(sizeMB)}</span>;
let displaySize = "";
if (sizeMB >= 1024 * 1024) {
displaySize = `${(sizeMB / (1024 * 1024)).toFixed(2)} TB`;
} else if (sizeMB >= 1024) {
displaySize = `${(sizeMB / 1024).toFixed(2)} GB`;
} else {
displaySize = `${sizeMB.toFixed(2)} MB`;
}
return <span className="font-mono text-xs font-normal text-slate-300 tracking-wide">{displaySize}</span>;
}
});
}
@@ -174,17 +205,4 @@ export function getAgentColumns({
return cols;
}
function formatStorageSize(sizeMB: number): string {
if (sizeMB === 0) return "0.00 MB";
if (sizeMB >= 1024 * 1024) {
return `${(sizeMB / (1024 * 1024)).toFixed(2)} TB`;
}
if (sizeMB >= 1024) {
return `${(sizeMB / 1024).toFixed(2)} GB`;
}
if (sizeMB < 1) {
return `${(sizeMB * 1024).toFixed(2)} KB`;
}
return `${sizeMB.toFixed(2)} MB`;
}
@@ -2,6 +2,7 @@
import { Column } from "@/components/ui/DataTable";
import { Badge } from "@/components/ui/Badge";
import { SafeImage } from "@/components/ui/SafeImage";
import { type ManagedUser } from "@/lib/admin-api";
import { UserCog, CheckCircle2, XCircle } from "lucide-react";
@@ -16,7 +17,12 @@ export function getExternalAccountColumns(
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center border border-slate-700 shrink-0 overflow-hidden">
{row.profile_picture ? (
<img src={`/api/uploads/${row.profile_picture}`} alt={row.account_name || row.username} className="w-full h-full object-cover" />
<SafeImage
src={`/api/uploads/${row.profile_picture}`}
alt={row.account_name || row.username}
fallbackName={row.account_name || row.username}
className="w-full h-full object-cover"
/>
) : (
<span className="text-xs font-bold text-slate-400">{(row.account_name || row.username).substring(0, 2).toUpperCase()}</span>
)}
@@ -36,6 +42,7 @@ export function getExternalAccountColumns(
accessor: (row) => {
let badgeColor = "bg-slate-500/20 text-slate-400 border-slate-500/30";
if (row.role === 'SUPER_ADMIN') badgeColor = "bg-purple-500/20 text-purple-400 border-purple-500/30";
if (row.role === 'EXECUTIVE') badgeColor = "bg-yellow-500/20 text-yellow-400 border-yellow-500/30";
if (row.role === 'SOC_ANALYST') badgeColor = "bg-amber-500/20 text-amber-400 border-amber-500/30";
if (row.role === 'ENGINEER') badgeColor = "bg-blue-500/20 text-blue-400 border-blue-500/30";
if (row.role === 'TENANT_ADMIN') badgeColor = "bg-emerald-500/20 text-emerald-400 border-emerald-500/30";
+104 -71
View File
@@ -1,32 +1,22 @@
"use client";
import { Loader2, Plus, Server } from "lucide-react";
import { useEffect } from "react";
import { Plus, Loader2, MapPin } from "lucide-react";
import { HelpTrigger } from "@/components/help/HelpTrigger";
import { useAgentsData } from "./useAgentsData";
import { getAgentColumns } from "./columns";
import { getExternalAccountColumns } from "./externalColumns";
import ExternalAccountsSection from "./ExternalAccountsSection";
import UptimeStatsCards from "./UptimeStatsCards";
import AgentsTableSection from "./AgentsTableSection";
import ExternalAccountsSection from "./ExternalAccountsSection";
import AgentsPageModals from "./AgentsPageModals";
import { useAgentsData } from "./useAgentsData";
import { HelpTrigger } from "@/components/help/HelpTrigger";
import dynamic from "next/dynamic";
const IndonesiaAgentMap = dynamic(
() => import("@/components/dashboard/IndonesiaAgentMap").then(m => ({ default: m.IndonesiaAgentMap })),
{
ssr: false,
loading: () => (
<div className="h-[320px] w-full rounded-2xl border border-white/5 bg-slate-900/40 flex items-center justify-center">
<div className="flex flex-col items-center gap-2">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<span className="text-xs text-slate-400">Loading Agent Location Map...</span>
</div>
</div>
),
}
);
import AgentLocationMap from "@/components/admin/AgentLocationMap";
import { resolveAgentLabel } from "@/lib/admin-api";
export default function AgentsPage() {
useEffect(() => {
document.title = "Agents | BackOne - Deep Package Inspection";
}, []);
const {
role,
agents,
@@ -82,46 +72,45 @@ export default function AgentsPage() {
setAccountModalAgent,
handleViewAs,
openDelete,
onEditLocation: handleEditLocation,
locations,
handleEditLocation,
locationUuids: new Set(locations.map((l: any) => l.agent_uuid)),
});
const totalAgents = agents.length;
const onlineAgents = agents.filter(a => a.last_seen_at?.human === 'Online').length;
const offlineAgents = totalAgents - onlineAgents;
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100);
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100;
const externalUsers = managedUsers.filter(u => u.role !== 'AGENT_VIEWER');
const superadminUsers = externalUsers.filter(u => u.role === 'SUPER_ADMIN' || !u.site_uuid || u.site_uuid === 'default' || (u.site_uuid !== '6681452d_9cae_4ff4_8ae8_0d504774265e' && u.site_uuid !== 'd7902405_0dc2_458b_8584_ed4d24b64f24'));
const siabUsers = externalUsers.filter(u => u.role !== 'SUPER_ADMIN' && u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e');
const nexusUsers = externalUsers.filter(u => u.role !== 'SUPER_ADMIN' && u.site_uuid === 'd7902405_0dc2_458b_8584_ed4d24b64f24');
const externalColumns = getExternalAccountColumns((user) => {
setExternalModalUser(user);
setExternalModalOpen(true);
});
const totalAgents = agents.length;
const onlineAgents = agents.filter(a => {
const hum = a.last_seen_at?.human;
return hum === 'Online' || hum === 'Flows Detected' || (hum && hum.startsWith('Last seen:'));
}).length;
const offlineAgents = totalAgents - onlineAgents;
const uptimeValues = agents.map(a => uptimeMap[a.uuid || a.serial] ?? 100);
const avgUptime = uptimeValues.length > 0 ? uptimeValues.reduce((s, v) => s + v, 0) / uptimeValues.length : 100;
// External Accounts di halaman Agents = akun eksternal teknikal/operasional saja
// AGENT_VIEWER = akun network agent (sudah ada di tabel agents) → BUKAN akun eksternal
// COMPANY_* = akun customer/client → sudah ada di halaman User Accounts
const EXCLUDED_ROLES = ['COMPANY_ADMIN', 'COMPANY_OPERATOR', 'COMPANY_VIEWER', 'AGENT_VIEWER'];
const externalUsers = managedUsers.filter(u => !EXCLUDED_ROLES.includes(u.role || ''));
// Grouping by site — semua sudah dipastikan hanya SUPER_ADMIN, EXECUTIVE, TENANT_ADMIN, SOC_ANALYST, ENGINEER
const superadminUsers = externalUsers.filter(u => u.role === 'SUPER_ADMIN' || u.role === 'EXECUTIVE' || !u.site_uuid || u.site_uuid === 'default');
const siabUsers = externalUsers.filter(u => u.site_uuid === '6681452d_9cae_4ff4_8ae8_0d504774265e' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE');
const nexusUsers = externalUsers.filter(u => u.site_uuid === '1959bb55_045b_47c7_bbdd_f33b7db197b9' && u.role !== 'SUPER_ADMIN' && u.role !== 'EXECUTIVE');
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<h2 className="text-2xl font-bold tracking-tight text-white flex items-center gap-3">
<Server className="w-6 h-6 text-primary" />
Agents Inventory
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
<div className="flex items-center gap-3">
<HelpTrigger pageId="agents" />
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<button
onClick={() => setIsCreateOpen(true)}
className="flex items-center gap-2 bg-primary hover:bg-primary/95 text-primary-foreground px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-lg shadow-blue-500/10"
>
<Plus className="w-4 h-4" />
Provision Agent
</button>
)}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold tracking-tight text-white flex items-center gap-3">
Agents Inventory
{isLoading && <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />}
</h2>
<p className="text-sm text-muted-foreground mt-1">Managed DPI probes, remote collectors, and probe infrastructure.</p>
</div>
<HelpTrigger pageId="agents" />
</div>
{error && (
@@ -137,35 +126,79 @@ export default function AgentsPage() {
avgUptime={avgUptime}
/>
{/* Indonesia Agent Geolocation & Flow Map */}
{!isLoading && (
<IndonesiaAgentMap
agents={agents}
locations={locations}
onEditLocation={handleEditLocation}
role={role}
/>
{/* Agent Location Map — tampil untuk SUPER_ADMIN, TENANT_ADMIN, dan COMPANY_ADMIN */}
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN' || role === 'COMPANY_ADMIN') && (
<div className="rounded-2xl border border-white/8 bg-slate-900/40 overflow-hidden relative z-0">
<div className="flex items-center justify-between px-5 py-3.5 border-b border-white/8">
<div className="flex items-center gap-2.5">
<MapPin className="w-4 h-4 text-indigo-400" />
<span className="text-sm font-semibold text-white">Agent Network Map</span>
<span className="text-xs text-slate-500">
{locations.length > 0
? `${locations.length} of ${agents.length} agent${agents.length !== 1 ? 's' : ''} positioned`
: 'No positions configured yet'}
</span>
</div>
{agents.length > locations.length && (
<span className="text-[11px] text-amber-400 bg-amber-400/10 border border-amber-400/20 px-2.5 py-1 rounded-full">
{agents.length - locations.length} agent{agents.length - locations.length !== 1 ? 's' : ''} without location — click 📍 in the table
</span>
)}
</div>
<div className="p-3">
<AgentLocationMap
pins={locations
.filter(loc => agents.some(a => (a.uuid || a.serial) === loc.agent_uuid))
.map(loc => {
const agent = agents.find(a => (a.uuid || a.serial) === loc.agent_uuid);
const agentLabel = agent
? (resolveAgentLabel(loc.agent_uuid, managedUsers) !== loc.agent_uuid
? resolveAgentLabel(loc.agent_uuid, managedUsers)
: (agent.label || loc.label || loc.agent_uuid))
: (loc.label || loc.agent_uuid);
const isOnline = !!(agent?.last_seen_at?.human === 'Online' ||
agent?.last_seen_at?.human === 'Flows Detected' ||
(agent?.last_seen_at?.human && agent.last_seen_at.human.startsWith('Last seen:')));
const uptime = uptimeMap[loc.agent_uuid];
return {
agent_uuid: loc.agent_uuid,
label: agentLabel,
latitude: loc.latitude,
longitude: loc.longitude,
isOnline,
uptimePercent: uptime,
};
})}
onEditLocation={handleEditLocation}
canEdit={role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN'}
/>
</div>
</div>
)}
<AgentsTableSection
agents={agents}
isLoading={isLoading}
columns={columns}
role={role}
onProvisionClick={() => setIsCreateOpen(true)}
/>
<ExternalAccountsSection
role={role}
isLoading={isLoading}
externalUsers={externalUsers}
superadminUsers={superadminUsers}
siabUsers={siabUsers}
nexusUsers={nexusUsers}
externalColumns={externalColumns}
setExternalModalUser={setExternalModalUser}
setTargetSiteUuid={setTargetSiteUuid}
setTargetCreatedBy={setTargetCreatedBy}
setExternalModalOpen={setExternalModalOpen}
/>
{(role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') && (
<ExternalAccountsSection
role={role}
isLoading={isLoading}
externalUsers={externalUsers}
superadminUsers={superadminUsers}
siabUsers={siabUsers}
nexusUsers={nexusUsers}
externalColumns={externalColumns}
setExternalModalUser={setExternalModalUser}
setTargetSiteUuid={setTargetSiteUuid}
setTargetCreatedBy={setTargetCreatedBy}
setExternalModalOpen={setExternalModalOpen}
/>
)}
<AgentsPageModals
isCreateOpen={isCreateOpen}
+24 -14
View File
@@ -2,7 +2,8 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { getAgents, Agent } from "@/lib/actions/agents";
import { getAgents } from "@/lib/actions/agents";
import { Agent } from "@/lib/actions/agentsCore";
import { getAdminUsers, startViewAs, getViewAsHeaders, type ManagedUser } from "@/lib/admin-api";
import { useTimeFilter } from "@/contexts/TimeFilterContext";
import { usePollingKey } from "@/lib/usePolling";
@@ -10,6 +11,7 @@ import { usePollingKey } from "@/lib/usePolling";
export function useAgentsData() {
const router = useRouter();
const [role, setRole] = useState<string | null>(null);
const [myAgentUuids, setMyAgentUuids] = useState<string[]>([]);
const [agents, setAgents] = useState<Agent[]>([]);
const [uptimeMap, setUptimeMap] = useState<Record<string, number>>({});
const [storageMap, setStorageMap] = useState<Record<string, number>>({});
@@ -89,7 +91,7 @@ export function useAgentsData() {
}
};
const loadAgents = async () => {
const loadAgents = async (allowedUuids?: string[], currentRole?: string) => {
setIsLoading(true);
try {
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
@@ -97,8 +99,18 @@ export function useAgentsData() {
getAgents(siteUuid),
getAdminUsers(),
]);
if (agentData.status === 'fulfilled') setAgents(agentData.value);
else setError((agentData.reason as Error).message);
if (agentData.status === 'fulfilled') {
let allAgents = agentData.value;
// Filter agents untuk COMPANY_ADMIN dan COMPANY_OPERATOR — hanya tampilkan yang di-assign
const uuids = allowedUuids ?? myAgentUuids;
const r = currentRole ?? role;
if ((r === 'COMPANY_ADMIN' || r === 'COMPANY_OPERATOR') && uuids.length > 0) {
allAgents = allAgents.filter(a => uuids.includes(a.uuid || a.serial));
}
setAgents(allAgents);
} else {
setError((agentData.reason as Error).message);
}
if (userData.status === 'fulfilled') setManagedUsers(userData.value);
await loadUptime();
await loadLocations();
@@ -127,11 +139,14 @@ export function useAgentsData() {
.then(data => {
if (data.user) {
const userRole = data.user.role || null;
const uuids: string[] = data.user.agent_uuids || [];
setRole(userRole);
if (userRole !== 'SUPER_ADMIN' && userRole !== 'TENANT_ADMIN') {
setMyAgentUuids(uuids);
const allowed = ['SUPER_ADMIN', 'TENANT_ADMIN', 'EXECUTIVE', 'COMPANY_ADMIN', 'COMPANY_OPERATOR'];
if (!allowed.includes(userRole || '')) {
router.push('/');
} else {
loadAgents();
loadAgents(uuids, userRole);
}
} else {
setIsLoading(false);
@@ -148,7 +163,7 @@ export function useAgentsData() {
}, [timeRange]);
useEffect(() => {
if (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') {
if (role === 'SUPER_ADMIN') {
loadStorage();
}
if (role && role !== 'AGENT_VIEWER') {
@@ -158,13 +173,8 @@ export function useAgentsData() {
useEffect(() => {
if (role && role !== 'AGENT_VIEWER' && refreshKey > 0) {
const siteUuid = localStorage.getItem("backone_site_uuid") || undefined;
getAgents(siteUuid)
.then(data => setAgents(data))
.catch(err => console.warn("Failed to auto-refresh agents:", err));
loadUptime();
loadLocations();
if (role === 'SUPER_ADMIN' || role === 'TENANT_ADMIN') {
loadAgents();
if (role === 'SUPER_ADMIN') {
loadStorage();
}
}