fix(ui): follow-up polish — icons, dashboard bar, filter redesign, bounty/creds sort

- Dashboard: fix invisible bar at bottom of LIVE FEED by constraining
  max-height on the section instead of the inner container; same fix
  for side panels
- Page icons: add violet-accent icon beside h1 on all 9 missing pages
  (CanaryTokens, RealismConfig, SyntheticFiles, PersonaGeneration,
  Attackers, Webhooks, LiveLogs, Topologies, DecoyFleet)
- Attackers filter chips: replace ad-hoc chip buttons with seg-group
  tabs (ALL / ACTIVE N / PASSIVE N / INACTIVE N) matching Credential
  Vault style; country chips use same seg-group treatment
- Credential Vault: add sortable headers to REUSE tab (LAST SEEN,
  PRINCIPAL, KIND, TARGETS, ATTEMPTS); reuses same SortTh pattern
- Bounty: remove CREDENTIALS and PAYLOADS tabs; keep ALL, ARTIFACTS,
  FINGERPRINTS; add EMAIL (artifact subtype, filtered client-side)
This commit is contained in:
2026-04-30 00:20:25 -04:00
parent 9adee07d21
commit fbc9877ef2
13 changed files with 152 additions and 69 deletions

View File

@@ -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 }) => (
<th
@@ -136,10 +141,9 @@ const Bounty: React.FC = () => {
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 = () => {
<h1>BOUNTY VAULT</h1>
</div>
<span className="page-sub">
{total.toLocaleString()} BOUNTIES · {credCount} CREDENTIALS · {payCount} PAYLOADS · {artCount} ARTIFACTS · {fpCount} FINGERPRINTS
{total.toLocaleString()} BOUNTIES · {artCount} ARTIFACTS · {fpCount} FINGERPRINTS · {mailCount} EMAIL
</span>
</div>
</div>