Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions client/src/pages/app/AseanSingleWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default function AseanSingleWindow() {
const inboundQ = trpc.aseanSw.listInboundMessages.useQuery();
const statsQ = trpc.aseanSw.getStats.useQuery();
const connectivityQ = trpc.aseanSw.getConnectivityStatus.useQuery();
const aseanUnavailable = connectionsQ.isError || messagesQ.isError || inboundQ.isError || statsQ.isError || connectivityQ.isError;

const testMut = trpc.aseanSw.testConnection.useMutation({
onSuccess: (data) => {
Expand Down Expand Up @@ -159,6 +160,9 @@ export default function AseanSingleWindow() {
return (
<DashboardLayout>
<div className="p-6 space-y-6">
{aseanUnavailable && (
<p className="text-sm text-amber-700">ASEAN Single Window integration unavailable — empty results are not being treated as healthy.</p>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
Expand All @@ -183,10 +187,10 @@ export default function AseanSingleWindow() {
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ label: "Active Connections", value: `${connections.filter(c => c.status === "active").length}/10`, icon: <Wifi className="h-5 w-5 text-emerald-400" /> },
{ label: "Total Messages", value: stats?.total ?? 0, icon: <FileText className="h-5 w-5 text-blue-400" /> },
{ label: "Pending Inbound Acks", value: inbound.filter(m => m.status === "pending_ack").length, icon: <ArrowDownToLine className="h-5 w-5 text-yellow-400" /> },
{ label: "Failed/Rejected", value: (stats?.by_status?.failed ?? 0) + (stats?.by_status?.rejected ?? 0), icon: <XCircle className="h-5 w-5 text-red-400" /> },
{ label: "Active Connections", value: aseanUnavailable ? "—" : `${connections.filter(c => c.status === "active").length}/10`, icon: <Wifi className="h-5 w-5 text-emerald-400" /> },
{ label: "Total Messages", value: aseanUnavailable ? "—" : stats?.total ?? "—", icon: <FileText className="h-5 w-5 text-blue-400" /> },
{ label: "Pending Inbound Acks", value: aseanUnavailable ? "—" : inbound.filter(m => m.status === "pending_ack").length, icon: <ArrowDownToLine className="h-5 w-5 text-yellow-400" /> },
{ label: "Failed/Rejected", value: aseanUnavailable ? "—" : (stats?.by_status?.failed ?? 0) + (stats?.by_status?.rejected ?? 0), icon: <XCircle className="h-5 w-5 text-red-400" /> },
].map((s) => (
<Card key={s.label} className="bg-card border-border">
<CardContent className="pt-4">
Expand Down Expand Up @@ -217,7 +221,9 @@ export default function AseanSingleWindow() {
<Table>
<TableHeader><TableRow><TableHead>Ref</TableHead><TableHead>Destination</TableHead><TableHead>Doc Type</TableHead><TableHead>UCR</TableHead><TableHead>Status</TableHead><TableHead>Sent At</TableHead><TableHead>Ack Ref</TableHead><TableHead>Actions</TableHead></TableRow></TableHeader>
<TableBody>
{messages.length === 0 ? (
{aseanUnavailable ? (
<TableRow><TableCell colSpan={8} className="text-center text-amber-700 py-8">Outbound message data unavailable.</TableCell></TableRow>
) : messages.length === 0 ? (
<TableRow><TableCell colSpan={8} className="text-center text-muted-foreground py-8">No outbound messages yet</TableCell></TableRow>
) : messages.map((m) => (
<TableRow key={m.id}>
Expand All @@ -244,7 +250,9 @@ export default function AseanSingleWindow() {
<Table>
<TableHeader><TableRow><TableHead>Ref</TableHead><TableHead>Source</TableHead><TableHead>Doc Type</TableHead><TableHead>UCR</TableHead><TableHead>Status</TableHead><TableHead>Received At</TableHead><TableHead>Ack Ref</TableHead><TableHead>Actions</TableHead></TableRow></TableHeader>
<TableBody>
{inbound.length === 0 ? (
{aseanUnavailable ? (
<TableRow><TableCell colSpan={8} className="text-center text-amber-700 py-8">Inbound message data unavailable.</TableCell></TableRow>
) : inbound.length === 0 ? (
<TableRow><TableCell colSpan={8} className="text-center text-muted-foreground py-8">No inbound messages</TableCell></TableRow>
) : inbound.map((m) => (
<TableRow key={m.id}>
Expand All @@ -266,8 +274,10 @@ export default function AseanSingleWindow() {

<TabsContent value="connectivity">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{connectivity.length === 0 ? (
<div className="col-span-2 text-center text-muted-foreground py-12">Loading connectivity data…</div>
{aseanUnavailable ? (
<div className="col-span-2 text-center text-amber-700 py-12">Connectivity data unavailable.</div>
) : connectivity.length === 0 ? (
<div className="col-span-2 text-center text-muted-foreground py-12">No connectivity data.</div>
) : connectivity.map((m: any) => (
<Card key={m.code} className="bg-card border-border">
<CardContent className="p-4 flex items-center justify-between">
Expand Down
3 changes: 3 additions & 0 deletions client/src/pages/app/CustomsDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ function LiveCargoStream() {
{ limit: 10 },
{ refetchInterval: wsInfo?.pollingIntervalMs ?? false, enabled: !wsInfo?.wsUrl }
);
const streamUnavailable = initialEvents?.unavailable === true || polled?.unavailable === true;
useEffect(() => {
if (!wsInfo?.wsUrl && polled?.events) {
setEvents(polled.events.slice(0, 20));
Expand Down Expand Up @@ -660,6 +661,8 @@ function LiveCargoStream() {
<div ref={listRef} className="max-h-72 overflow-y-auto divide-y">
{isLoading ? (
Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-10 mx-4 my-1" />)
) : streamUnavailable ? (
<p className="p-4 text-sm text-amber-700 text-center">Cargo event stream unavailable — live events cannot be displayed.</p>
) : events.length === 0 ? (
<p className="p-4 text-sm text-muted-foreground text-center">No events yet — waiting for stream…</p>
) : (
Expand Down
26 changes: 18 additions & 8 deletions client/src/pages/app/SecurityAlerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ export default function SecurityAlerts() {
const [activeTab, setActiveTab] = useState<"alerts" | "agents" | "playbooks" | "score">("alerts");

// ── Queries ──────────────────────────────────────────────────────────────────
const { data: alerts, isLoading: alertsLoading, refetch: refetchAlerts } = trpc.wazuh.getAlerts.useQuery(undefined, { enabled: isAdmin });
const { data: agents, isLoading: agentsLoading } = trpc.wazuh.getAgents.useQuery(undefined, { enabled: isAdmin && activeTab === "agents" });
const { data: playbooks, isLoading: playbooksLoading } = trpc.wazuh.listPlaybooks.useQuery(undefined, { enabled: isAdmin && activeTab === "playbooks" });
const { data: securityScore } = trpc.wazuh.getSecurityScore.useQuery();
const { data: alerts, isLoading: alertsLoading, isError: alertsError, refetch: refetchAlerts } = trpc.wazuh.getAlerts.useQuery(undefined, { enabled: isAdmin });
const { data: agents, isLoading: agentsLoading, isError: agentsError } = trpc.wazuh.getAgents.useQuery(undefined, { enabled: isAdmin && activeTab === "agents" });
const { data: playbooks, isLoading: playbooksLoading, isError: playbooksError } = trpc.wazuh.listPlaybooks.useQuery(undefined, { enabled: isAdmin && activeTab === "playbooks" });
const { data: securityScore, isError: securityScoreError } = trpc.wazuh.getSecurityScore.useQuery();

// ── Mutations ─────────────────────────────────────────────────────────────────
const triggerPlaybookMutation = trpc.wazuh.triggerPlaybook.useMutation({
Expand Down Expand Up @@ -87,7 +87,7 @@ export default function SecurityAlerts() {
<p className="text-muted-foreground text-sm">Your platform security score</p>
</div>
</div>
{securityScore && (
{securityScore ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-card border rounded-lg p-4 text-center">
<div className="text-4xl font-bold text-green-600">{(securityScore as any).score ?? "—"}</div>
Expand All @@ -106,7 +106,9 @@ export default function SecurityAlerts() {
<div className="text-sm text-muted-foreground mt-1">Failed Checks</div>
</div>
</div>
)}
) : securityScoreError ? (
<p className="text-sm text-amber-700">Wazuh security score unavailable — no score is being reported.</p>
) : null}
</div>
);
}
Expand Down Expand Up @@ -176,6 +178,10 @@ export default function SecurityAlerts() {
<div className="flex items-center justify-center py-12 text-muted-foreground">
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Loading alerts...
</div>
) : alertsError ? (
<div className="text-center py-12 text-amber-700">
Wazuh alerts unavailable — no clean result can be reported.
</div>
) : alertList.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<CheckCircle className="w-12 h-12 mx-auto mb-3 text-green-500 opacity-60" />
Expand Down Expand Up @@ -234,6 +240,8 @@ export default function SecurityAlerts() {
<div className="flex items-center justify-center py-12 text-muted-foreground">
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Loading agents...
</div>
) : agentsError ? (
<div className="text-center py-8 text-amber-700">Wazuh agents unavailable — no agent state can be reported.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{agentList.map((agent: any) => (
Expand All @@ -255,7 +263,7 @@ export default function SecurityAlerts() {
</div>
</div>
))}
{agentList.length === 0 && (
{agentList.length === 0 && !agentsError && (
<div className="col-span-3 text-center py-8 text-muted-foreground">
<Server className="w-8 h-8 mx-auto mb-2 opacity-30" />
<p>No agents registered</p>
Expand All @@ -271,6 +279,8 @@ export default function SecurityAlerts() {
<div className="flex items-center justify-center py-12 text-muted-foreground">
<RefreshCw className="w-5 h-5 animate-spin mr-2" /> Loading playbooks...
</div>
) : playbooksError ? (
<div className="text-center py-8 text-amber-700">Wazuh playbooks unavailable.</div>
) : (
<div className="space-y-3">
{playbookList.map((pb: any) => (
Expand All @@ -294,7 +304,7 @@ export default function SecurityAlerts() {
</Button>
</div>
))}
{playbookList.length === 0 && (
{playbookList.length === 0 && !playbooksError && (
<div className="text-center py-8 text-muted-foreground">No playbooks configured</div>
)}
</div>
Expand Down
46 changes: 0 additions & 46 deletions client/src/pages/app/TraderOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -621,46 +621,6 @@ const ROLE_OPTIONS = [
border: "border-blue-400/30",
portal: "/app/trader",
},
{
value: "customs_officer" as const,
label: "Customs Officer",
description: "Review declarations, assign risk lanes, issue clearances",
icon: Shield,
color: "text-amber-400",
bg: "bg-amber-400/10",
border: "border-amber-400/30",
portal: "/app/customs",
},
{
value: "oga_officer" as const,
label: "Other Government Agency (OGA) Officer",
description: "Review and approve permits, licences, and certificates",
icon: ClipboardList,
color: "text-purple-400",
bg: "bg-purple-400/10",
border: "border-purple-400/30",
portal: "/app/oga",
},
{
value: "inspector" as const,
label: "Port Inspector",
description: "Conduct physical inspections, manage cargo tracking",
icon: FileCheck,
color: "text-green-400",
bg: "bg-green-400/10",
border: "border-green-400/30",
portal: "/app/geo/heatmap",
},
{
value: "finance" as const,
label: "Finance / Revenue Officer",
description: "Monitor duty revenue, manage payments and drawbacks",
icon: Landmark,
color: "text-emerald-400",
bg: "bg-emerald-400/10",
border: "border-emerald-400/30",
portal: "/app/finance",
},
] as const;

type SelfAssignableRole = typeof ROLE_OPTIONS[number]["value"];
Expand Down Expand Up @@ -815,12 +775,6 @@ export default function TraderOnboarding() {
<RoleSelectionStep
onSelect={(role) => {
setRoleSelected(role);
// Non-trader roles skip the 5-step wizard and go straight to their portal
if (role !== "user") {
const portal = ROLE_OPTIONS.find(r => r.value === role)?.portal ?? "/app/trader";
toast.success("Role confirmed!", { description: `Redirecting you to the ${role.replace("_", " ")} portal.` });
setTimeout(() => navigate(portal), 1200);
}
}}
/>
</CardContent>
Expand Down
28 changes: 20 additions & 8 deletions client/src/pages/app/WazuhSecurityEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ interface Playbook {
export default function WazuhSecurityEvents() {
const [triggeringPlaybook, setTriggeringPlaybook] = useState<string | null>(null);

const { data: alertsData, isLoading: loadingAlerts, refetch: refetchAlerts } = trpc.wazuh.getAlerts.useQuery();
const { data: agentsData, isLoading: loadingAgents } = trpc.wazuh.getAgents.useQuery();
const { data: playbooksData, isLoading: loadingPlaybooks } = trpc.wazuh.listPlaybooks.useQuery();
const { data: secScore } = trpc.wazuh.getSecurityScore.useQuery();
const { data: alertsData, isLoading: loadingAlerts, isError: alertsError, refetch: refetchAlerts } = trpc.wazuh.getAlerts.useQuery();
const { data: agentsData, isLoading: loadingAgents, isError: agentsError } = trpc.wazuh.getAgents.useQuery();
const { data: playbooksData, isLoading: loadingPlaybooks, isError: playbooksError } = trpc.wazuh.listPlaybooks.useQuery();
const { data: secScore, isError: scoreError } = trpc.wazuh.getSecurityScore.useQuery();

const triggerPlaybook = trpc.wazuh.triggerPlaybook.useMutation({
onSuccess: (data) => {
Expand All @@ -86,10 +86,14 @@ export default function WazuhSecurityEvents() {
const playbooks: Playbook[] = (playbooksData as { playbooks?: Playbook[] } | undefined)?.playbooks ?? [];

const score = secScore as { score?: number; grade?: string; unresolved_alerts?: number } | undefined;
const wazuhUnavailable = alertsError || agentsError || playbooksError || scoreError;

return (
<DashboardLayout>
<div className="p-6 space-y-6">
{wazuhUnavailable && (
<p className="text-sm text-amber-700">Wazuh data unavailable — empty panels do not indicate a clean security state.</p>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
Expand All @@ -107,13 +111,13 @@ export default function WazuhSecurityEvents() {
</div>

{/* Security Score */}
{score && (
{score ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="bg-card border-border">
<CardContent className="p-4 flex items-center gap-3">
<Activity className="h-5 w-5 text-blue-400" />
<div>
<p className="text-2xl font-bold text-foreground">{score.score ?? 0}</p>
<p className="text-2xl font-bold text-foreground">{score.score ?? "—"}</p>
<p className="text-xs text-muted-foreground">Security Score</p>
</div>
</CardContent>
Expand All @@ -131,7 +135,7 @@ export default function WazuhSecurityEvents() {
<CardContent className="p-4 flex items-center gap-3">
<AlertTriangle className="h-5 w-5 text-red-400" />
<div>
<p className="text-2xl font-bold text-foreground">{score.unresolved_alerts ?? 0}</p>
<p className="text-2xl font-bold text-foreground">{score.unresolved_alerts ?? "—"}</p>
<p className="text-xs text-muted-foreground">Unresolved Alerts</p>
</div>
</CardContent>
Expand All @@ -146,7 +150,9 @@ export default function WazuhSecurityEvents() {
</CardContent>
</Card>
</div>
)}
) : scoreError ? (
<p className="text-sm text-amber-700">Security score unavailable.</p>
) : null}

<Tabs defaultValue="alerts">
<TabsList>
Expand All @@ -159,6 +165,8 @@ export default function WazuhSecurityEvents() {
<TabsContent value="alerts" className="mt-4 space-y-3">
{loadingAlerts ? (
<p className="text-muted-foreground text-sm">Loading alerts…</p>
) : alertsError ? (
<p className="text-sm text-amber-700">Security alerts unavailable.</p>
) : alerts.length === 0 ? (
<p className="text-muted-foreground text-sm">No security alerts.</p>
) : (
Expand Down Expand Up @@ -196,6 +204,8 @@ export default function WazuhSecurityEvents() {
<TabsContent value="agents" className="mt-4 space-y-3">
{loadingAgents ? (
<p className="text-muted-foreground text-sm">Loading agents…</p>
) : agentsError ? (
<p className="text-sm text-amber-700">Wazuh agents unavailable.</p>
) : agents.length === 0 ? (
<p className="text-muted-foreground text-sm">No agents registered.</p>
) : (
Expand Down Expand Up @@ -223,6 +233,8 @@ export default function WazuhSecurityEvents() {
<TabsContent value="playbooks" className="mt-4 space-y-3">
{loadingPlaybooks ? (
<p className="text-muted-foreground text-sm">Loading playbooks…</p>
) : playbooksError ? (
<p className="text-sm text-amber-700">Wazuh playbooks unavailable.</p>
) : playbooks.length === 0 ? (
<p className="text-muted-foreground text-sm">No playbooks available.</p>
) : (
Expand Down
Loading