-
ATTACKERS
+
+
+
ATTACKERS
+
{total.toLocaleString()} UNIQUE SOURCES · {activityCounts.active} ACTIVE · {activityCounts.passive} PASSIVE · {activityCounts.inactive} INACTIVE
@@ -160,39 +163,35 @@ const Attackers: React.FC = () => {
- {(['active', 'passive', 'inactive'] as ActivityTier[]).map(tier => (
-
setActivity(tier)}
- >
-
- {tier.toUpperCase()} {activityCounts[tier] > 0 ? activityCounts[tier] : ''}
-
- ))}
- {countries.length > 0 &&
| }
- {countries.map(cc => (
-
setCountry(cc)}
- >
- {cc}
-
- ))}
- {(activityFilter || countryFilter || serviceFilter) && (
-
setSearchParams(_params({ activity: '', country: '', service: '' }))}
- >
- CLEAR ×
+
+ setActivity('')}>
+ ALL
+ {(['active', 'passive', 'inactive'] as ActivityTier[]).map(tier => (
+ setActivity(tier)}
+ >
+
+ {tier.toUpperCase()}{activityCounts[tier] > 0 ? ` ${activityCounts[tier]}` : ''}
+
+ ))}
+
+ {countries.length > 0 && (
+
+ {countries.map(cc => (
+ setCountry(cc)}
+ >
+ {cc}
+
+ ))}
+
)}
diff --git a/decnet_web/src/components/Bounty.tsx b/decnet_web/src/components/Bounty.tsx
index 3877ca6c..d3fa1cb0 100644
--- a/decnet_web/src/components/Bounty.tsx
+++ b/decnet_web/src/components/Bounty.tsx
@@ -72,7 +72,8 @@ const Bounty: React.FC = () => {
const offset = (page - 1) * limit;
let url = `/bounty?limit=${limit}&offset=${offset}`;
if (query) url += `&search=${encodeURIComponent(query)}`;
- if (typeFilter) url += `&bounty_type=${typeFilter}`;
+ const apiType = typeFilter === 'mail' ? 'artifact' : typeFilter;
+ if (apiType) url += `&bounty_type=${apiType}`;
const res = await api.get(url);
setBounties(res.data.data);
setTotal(res.data.total);
@@ -94,10 +95,9 @@ const Bounty: React.FC = () => {
const totalPages = Math.max(1, Math.ceil(total / limit));
- const credCount = bounties.filter(b => b.bounty_type === 'credential').length;
- const payCount = bounties.filter(b => b.bounty_type === 'payload').length;
const fpCount = bounties.filter(b => b.bounty_type === 'fingerprint').length;
- const artCount = bounties.filter(b => b.bounty_type === 'artifact').length;
+ const artCount = bounties.filter(b => b.bounty_type === 'artifact' && b.payload?.kind !== 'mail').length;
+ const mailCount = bounties.filter(b => b.bounty_type === 'artifact' && b.payload?.kind === 'mail').length;
const handleSortCol = (col: string) => {
if (sortCol === col) {
@@ -109,9 +109,14 @@ const Bounty: React.FC = () => {
}
};
+ const filteredBounties = useMemo(() => {
+ if (typeFilter !== 'mail') return bounties;
+ return bounties.filter(b => b.bounty_type === 'artifact' && b.payload?.kind === 'mail');
+ }, [bounties, typeFilter]);
+
const sortedBounties = useMemo(() => {
- if (!sortCol) return bounties;
- return [...bounties].sort((a, b) => {
+ if (!sortCol) return filteredBounties;
+ return [...filteredBounties].sort((a, b) => {
let av: string | number = '';
let bv: string | number = '';
if (sortCol === 'time') { av = a.timestamp; bv = b.timestamp; }
@@ -122,7 +127,7 @@ const Bounty: React.FC = () => {
const cmp = String(av).localeCompare(String(bv));
return sortDir === 'asc' ? cmp : -cmp;
});
- }, [bounties, sortCol, sortDir]);
+ }, [filteredBounties, sortCol, sortDir]);
const SortTh: React.FC<{ col: string; children: React.ReactNode }> = ({ col, children }) => (
{
const SEGMENTS: [string, string][] = [
['', 'ALL'],
- ['credential', 'CREDENTIALS'],
- ['payload', 'PAYLOADS'],
['artifact', 'ARTIFACTS'],
['fingerprint', 'FINGERPRINTS'],
+ ['mail', 'EMAIL'],
];
const formatBytes = (n: any): string => {
@@ -159,7 +163,7 @@ const Bounty: React.FC = () => {
BOUNTY VAULT
- {total.toLocaleString()} BOUNTIES · {credCount} CREDENTIALS · {payCount} PAYLOADS · {artCount} ARTIFACTS · {fpCount} FINGERPRINTS
+ {total.toLocaleString()} BOUNTIES · {artCount} ARTIFACTS · {fpCount} FINGERPRINTS · {mailCount} EMAIL
diff --git a/decnet_web/src/components/CanaryTokens.tsx b/decnet_web/src/components/CanaryTokens.tsx
index 28233d82..93528a3f 100644
--- a/decnet_web/src/components/CanaryTokens.tsx
+++ b/decnet_web/src/components/CanaryTokens.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
- Plus, Upload, X, AlertTriangle, Search,
+ Plus, Upload, X, AlertTriangle, Search, Target,
} from '../icons';
import api from '../utils/api';
import { useEscapeKey } from '../hooks/useEscapeKey';
@@ -948,7 +948,10 @@ const CanaryTokens: React.FC = () => {
-
CANARY TOKENS
+
+
+
CANARY TOKENS
+
{tokens.length} TOKEN{tokens.length === 1 ? '' : 'S'} · {counts.planted} PLANTED · {counts.hits} TOTAL HIT{counts.hits === 1 ? '' : 'S'} · {blobs.length} UPLOADED BLOB{blobs.length === 1 ? '' : 'S'}
diff --git a/decnet_web/src/components/Credentials.tsx b/decnet_web/src/components/Credentials.tsx
index ea451ee6..a1286a9b 100644
--- a/decnet_web/src/components/Credentials.tsx
+++ b/decnet_web/src/components/Credentials.tsx
@@ -170,6 +170,23 @@ const Credentials: React.FC = () => {
});
}, [creds, sortCol, sortDir]);
+ const sortedReuseRows = useMemo(() => {
+ if (!sortCol) return reuseRows;
+ return [...reuseRows].sort((a, b) => {
+ let av: string | number = '';
+ let bv: string | number = '';
+ if (sortCol === 'seen') { av = a.last_seen; bv = b.last_seen; }
+ else if (sortCol === 'principal') { av = a.principal ?? ''; bv = b.principal ?? ''; }
+ else if (sortCol === 'kind') { av = a.secret_kind; bv = b.secret_kind; }
+ else if (sortCol === 'targets') { av = a.target_count; bv = b.target_count; }
+ else if (sortCol === 'attempts') { av = a.attempt_count; bv = b.attempt_count; }
+ const cmp = typeof av === 'number' && typeof bv === 'number'
+ ? av - bv
+ : String(av).localeCompare(String(bv));
+ return sortDir === 'asc' ? cmp : -cmp;
+ });
+ }, [reuseRows, sortCol, sortDir]);
+
const SortTh: React.FC<{ col: string; children: React.ReactNode }> = ({ col, children }) => (
{
- LAST SEEN
- PRINCIPAL
- KIND
- TARGETS
- ATTEMPTS
+ LAST SEEN
+ PRINCIPAL
+ KIND
+ TARGETS
+ ATTEMPTS
DECKIES
SERVICES
- {reuseRows.length > 0 ? reuseRows.map(r => {
+ {sortedReuseRows.length > 0 ? sortedReuseRows.map(r => {
const isPlain = r.secret_kind === 'plaintext';
const moreDeckies = Math.max(0, r.deckies.length - 3);
const moreServices = Math.max(0, r.services.length - 3);
diff --git a/decnet_web/src/components/Dashboard.css b/decnet_web/src/components/Dashboard.css
index fdb5f015..12896b6a 100644
--- a/decnet_web/src/components/Dashboard.css
+++ b/decnet_web/src/components/Dashboard.css
@@ -264,10 +264,15 @@
min-height: 0;
}
+.dash-grid > .logs-section {
+ max-height: 480px;
+ overflow: hidden;
+}
+
.dash-grid > .logs-section .logs-table-container {
flex: 1;
- max-height: 420px;
overflow-y: auto;
+ min-height: 0;
}
.dash-side {
@@ -281,11 +286,13 @@
flex: 1;
min-height: 0;
overflow: hidden;
+ max-height: 300px;
}
.dash-side > .logs-section .panel-body {
- max-height: 260px;
overflow-y: auto;
+ flex: 1;
+ min-height: 0;
}
/* Attacker/siege rows */
diff --git a/decnet_web/src/components/DeckyFleet.tsx b/decnet_web/src/components/DeckyFleet.tsx
index f9d168d0..081e8b2b 100644
--- a/decnet_web/src/components/DeckyFleet.tsx
+++ b/decnet_web/src/components/DeckyFleet.tsx
@@ -1553,7 +1553,10 @@ const DeckyFleet: React.FC = ({ searchQuery = '' }) => {
-
DECOY FLEET
+
+
+
DECOY FLEET
+
{deckies.length} DECKIES DEPLOYED · {counts.active + counts.hot} ACTIVE · {counts.hot} UNDER SIEGE
{deployMode && (
diff --git a/decnet_web/src/components/LiveLogs.tsx b/decnet_web/src/components/LiveLogs.tsx
index 57f10544..18614d26 100644
--- a/decnet_web/src/components/LiveLogs.tsx
+++ b/decnet_web/src/components/LiveLogs.tsx
@@ -182,7 +182,10 @@ const LiveLogs: React.FC = () => {
-
LOGS
+
+
+
LOGS
+
{filteredLogs.length.toLocaleString()} SHOWN · {totalLogs.toLocaleString()} MATCHES · STREAM {streaming ? 'LIVE' : 'PAUSED'}
diff --git a/decnet_web/src/components/PersonaGeneration.tsx b/decnet_web/src/components/PersonaGeneration.tsx
index cc3b6a06..59e5a371 100644
--- a/decnet_web/src/components/PersonaGeneration.tsx
+++ b/decnet_web/src/components/PersonaGeneration.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
- Mail, Plus, Pencil, Trash2, Check, AlertTriangle, Upload, Download,
+ Mail, Plus, Pencil, Trash2, Check, AlertTriangle, Upload, Download, Sparkles,
} from '../icons';
import api from '../utils/api';
import { useToast } from './Toasts/useToast';
@@ -736,7 +736,10 @@ const PersonaGeneration: React.FC
= ({ topologyId }) =>
-
{isTopology ? 'TOPOLOGY PERSONAS' : 'PERSONA GENERATION'}
+
+
+
{isTopology ? 'TOPOLOGY PERSONAS' : 'PERSONA GENERATION'}
+
{personas.length} PERSONA{personas.length === 1 ? '' : 'S'} · {llmHeavyCount} LLM-HEAVY
{isTopology
diff --git a/decnet_web/src/components/RealismConfig/RealismConfig.tsx b/decnet_web/src/components/RealismConfig/RealismConfig.tsx
index 918796b5..25a9d946 100644
--- a/decnet_web/src/components/RealismConfig/RealismConfig.tsx
+++ b/decnet_web/src/components/RealismConfig/RealismConfig.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import api from '../../utils/api';
import { useToast } from '../Toasts/useToast';
-import { Save, RotateCcw, AlertTriangle } from '../../icons';
+import { Save, RotateCcw, AlertTriangle, Sliders } from '../../icons';
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
// Reuse the DeckyFleet shell (page-header / btn / fleet-* / dim / mono) and
// the persona-page tweaks (info-banner, .input) so the realism config panel
@@ -168,7 +168,10 @@ const RealismConfig: React.FC = () => {
-
REALISM CONFIG
+
+
+
REALISM CONFIG
+
USER {totals.user} · SYSTEM {totals.system} · CANARY {totals.canary} ·
{' '}CANARY PROB {(config.canary_probability * 100).toFixed(1)}%
diff --git a/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx b/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx
index 277c0f32..16b40485 100644
--- a/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx
+++ b/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import api from '../../utils/api';
import { useEscapeKey } from '../../hooks/useEscapeKey';
import { useFocusTrap } from '../../hooks/useFocusTrap';
-import { X } from '../../icons';
+import { X, FileText } from '../../icons';
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
// Reuse the DeckyFleet shell + the persona-page tweaks so this page reads
// the same as the rest of the realism nav group.
@@ -241,7 +241,10 @@ const SyntheticFiles: React.FC = () => {
-
SYNTHETIC FILES
+
+
+
SYNTHETIC FILES
+
{total} TOTAL · PAGE {page + 1} / {totalPages}
{filtersActive ? ' · FILTERED' : ''}
diff --git a/decnet_web/src/components/TopologyList/TopologyList.tsx b/decnet_web/src/components/TopologyList/TopologyList.tsx
index 23ae45c9..e252039a 100644
--- a/decnet_web/src/components/TopologyList/TopologyList.tsx
+++ b/decnet_web/src/components/TopologyList/TopologyList.tsx
@@ -154,7 +154,10 @@ const TopologyList: React.FC = () => {
-
TOPOLOGIES
+
+
+
TOPOLOGIES
+
{loading ? 'LOADING…' : `${rows.length} ${rows.length === 1 ? 'TOPOLOGY' : 'TOPOLOGIES'}`}
{err && · {err} }
diff --git a/decnet_web/src/components/Webhooks.tsx b/decnet_web/src/components/Webhooks.tsx
index 417d59cc..2f648e7f 100644
--- a/decnet_web/src/components/Webhooks.tsx
+++ b/decnet_web/src/components/Webhooks.tsx
@@ -292,7 +292,10 @@ const Webhooks: React.FC = () => {
-
WEBHOOKS
+
+
+
WEBHOOKS
+
{webhooks.length} CONFIGURED · {enabledCount} ENABLED
{trippedCount > 0 && ` · ${trippedCount} TRIPPED`}