feat(ui): frontend polish sweep — 8 UX fixes

- DeckyFleet: card click opens inspect side-drawer instead of
  auto-filtering (localSearch filter behavior removed)
- Dashboard: LIVE FEED / DECKIES UNDER SIEGE / TOP ATTACKERS panels
  now have fixed max-height with overflow scroll instead of growing
- parseEventBody: defensive RFC 5424 header strip so raw syslog lines
  from the collector render as k=v pills instead of raw text
- Attackers: search placeholder updated; activity (Active/Passive/
  Inactive) and country chip filters added on top of existing IP search
- Credentials + Bounty: sortable column headers (click to asc/desc/clear)
- SwarmHosts + RemoteUpdates: icon extracted from <h1> into flex div
  with violet-accent class, matching site-wide Identities pattern
- Swarm.css: fix --panel-border undefined variable → --border so the
  title border-bottom line is visible on SwarmHosts and RemoteUpdates
This commit is contained in:
2026-04-29 23:56:38 -04:00
parent a322d88b3c
commit 9adee07d21
10 changed files with 318 additions and 37 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import {
Archive, Search, ChevronLeft, ChevronRight, Filter, Key, Package, ChevronRight as ChevR,
@@ -61,6 +61,8 @@ const Bounty: React.FC = () => {
const searchRef = useRef<HTMLInputElement | null>(null);
useFocusSearch(searchRef);
const [selected, setSelected] = useState<BountyEntry | null>(null);
const [sortCol, setSortCol] = useState<string>('');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const limit = 50;
@@ -97,6 +99,41 @@ const Bounty: React.FC = () => {
const fpCount = bounties.filter(b => b.bounty_type === 'fingerprint').length;
const artCount = bounties.filter(b => b.bounty_type === 'artifact').length;
const handleSortCol = (col: string) => {
if (sortCol === col) {
if (sortDir === 'asc') setSortDir('desc');
else { setSortCol(''); setSortDir('asc'); }
} else {
setSortCol(col);
setSortDir('asc');
}
};
const sortedBounties = useMemo(() => {
if (!sortCol) return bounties;
return [...bounties].sort((a, b) => {
let av: string | number = '';
let bv: string | number = '';
if (sortCol === 'time') { av = a.timestamp; bv = b.timestamp; }
else if (sortCol === 'decky') { av = a.decky; bv = b.decky; }
else if (sortCol === 'svc') { av = a.service; bv = b.service; }
else if (sortCol === 'attacker') { av = a.attacker_ip; bv = b.attacker_ip; }
else if (sortCol === 'type') { av = a.bounty_type; bv = b.bounty_type; }
const cmp = String(av).localeCompare(String(bv));
return sortDir === 'asc' ? cmp : -cmp;
});
}, [bounties, sortCol, sortDir]);
const SortTh: React.FC<{ col: string; children: React.ReactNode }> = ({ col, children }) => (
<th
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
onClick={() => handleSortCol(col)}
>
{children}
{sortCol === col ? (sortDir === 'asc' ? ' ▲' : ' ▼') : ''}
</th>
);
const SEGMENTS: [string, string][] = [
['', 'ALL'],
['credential', 'CREDENTIALS'],
@@ -175,17 +212,17 @@ const Bounty: React.FC = () => {
<table className="logs-table">
<thead>
<tr>
<th>TIME</th>
<th>DECKY</th>
<th>SVC</th>
<th>ATTACKER</th>
<th>TYPE</th>
<SortTh col="time">TIME</SortTh>
<SortTh col="decky">DECKY</SortTh>
<SortTh col="svc">SVC</SortTh>
<SortTh col="attacker">ATTACKER</SortTh>
<SortTh col="type">TYPE</SortTh>
<th>DATA</th>
<th></th>
</tr>
</thead>
<tbody>
{bounties.length > 0 ? bounties.map(b => {
{sortedBounties.length > 0 ? sortedBounties.map(b => {
const isCred = b.bounty_type === 'credential';
const isFp = b.bounty_type === 'fingerprint';
const isArt = b.bounty_type === 'artifact';