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:
@@ -7,20 +7,52 @@
|
|||||||
.ak-filter-row {
|
.ak-filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ak-filter-row .chip {
|
.attackers-root .seg-group {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
transition: opacity 0.15s;
|
border: 1px solid var(--border);
|
||||||
|
background: var(--panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ak-filter-row button {
|
.attackers-root .seg-group button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(0, 255, 65, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attackers-root .seg-group button:last-child { border-right: none; }
|
||||||
|
|
||||||
|
.attackers-root .seg-group button.active {
|
||||||
|
background: var(--violet-tint-10);
|
||||||
|
color: var(--violet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attackers-root .seg-group button:hover:not(.active) { color: var(--matrix); }
|
||||||
|
|
||||||
|
.ak-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ak-dot-active { background: var(--alert); box-shadow: 0 0 5px var(--alert); }
|
||||||
|
.ak-dot-passive { background: #ffaa00; }
|
||||||
|
.ak-dot-inactive { background: rgba(0, 255, 65, 0.3); }
|
||||||
|
|
||||||
.attackers-root .controls-row {
|
.attackers-root .controls-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -134,7 +134,10 @@ const Attackers: React.FC = () => {
|
|||||||
<div className="attackers-root">
|
<div className="attackers-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>ATTACKERS</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Users size={22} className="violet-accent" />
|
||||||
|
<h1>ATTACKERS</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{total.toLocaleString()} UNIQUE SOURCES · {activityCounts.active} ACTIVE · {activityCounts.passive} PASSIVE · {activityCounts.inactive} INACTIVE
|
{total.toLocaleString()} UNIQUE SOURCES · {activityCounts.active} ACTIVE · {activityCounts.passive} PASSIVE · {activityCounts.inactive} INACTIVE
|
||||||
</span>
|
</span>
|
||||||
@@ -160,39 +163,35 @@ const Attackers: React.FC = () => {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="ak-filter-row">
|
<div className="ak-filter-row">
|
||||||
{(['active', 'passive', 'inactive'] as ActivityTier[]).map(tier => (
|
<div className="seg-group" role="tablist">
|
||||||
<button
|
<button type="button" className={!activityFilter ? 'active' : ''} onClick={() => setActivity('')}>
|
||||||
key={tier}
|
ALL
|
||||||
type="button"
|
|
||||||
className={`chip ${activityFilter === tier ? (tier === 'active' ? 'alert-chip' : tier === 'passive' ? 'violet' : 'matrix') : 'dim-chip'}`}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => setActivity(tier)}
|
|
||||||
>
|
|
||||||
<span className={`dot status-dot ${tier === 'active' ? 'hot' : tier === 'passive' ? 'warn' : ''}`} style={{ marginRight: 4 }} />
|
|
||||||
{tier.toUpperCase()} {activityCounts[tier] > 0 ? activityCounts[tier] : ''}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{countries.length > 0 && <span className="dim" style={{ fontSize: '0.65rem', letterSpacing: 1, opacity: 0.4, alignSelf: 'center' }}>|</span>}
|
|
||||||
{countries.map(cc => (
|
|
||||||
<button
|
|
||||||
key={cc}
|
|
||||||
type="button"
|
|
||||||
className={`chip ${countryFilter === cc ? 'violet' : 'dim-chip'}`}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
onClick={() => setCountry(cc)}
|
|
||||||
>
|
|
||||||
{cc}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{(activityFilter || countryFilter || serviceFilter) && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="chip dim-chip"
|
|
||||||
style={{ cursor: 'pointer', opacity: 0.6 }}
|
|
||||||
onClick={() => setSearchParams(_params({ activity: '', country: '', service: '' }))}
|
|
||||||
>
|
|
||||||
CLEAR ×
|
|
||||||
</button>
|
</button>
|
||||||
|
{(['active', 'passive', 'inactive'] as ActivityTier[]).map(tier => (
|
||||||
|
<button
|
||||||
|
key={tier}
|
||||||
|
type="button"
|
||||||
|
className={activityFilter === tier ? 'active' : ''}
|
||||||
|
onClick={() => setActivity(tier)}
|
||||||
|
>
|
||||||
|
<span className={`ak-dot ak-dot-${tier}`} />
|
||||||
|
{tier.toUpperCase()}{activityCounts[tier] > 0 ? ` ${activityCounts[tier]}` : ''}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{countries.length > 0 && (
|
||||||
|
<div className="seg-group" role="tablist">
|
||||||
|
{countries.map(cc => (
|
||||||
|
<button
|
||||||
|
key={cc}
|
||||||
|
type="button"
|
||||||
|
className={countryFilter === cc ? 'active' : ''}
|
||||||
|
onClick={() => setCountry(cc)}
|
||||||
|
>
|
||||||
|
{cc}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ const Bounty: React.FC = () => {
|
|||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
let url = `/bounty?limit=${limit}&offset=${offset}`;
|
let url = `/bounty?limit=${limit}&offset=${offset}`;
|
||||||
if (query) url += `&search=${encodeURIComponent(query)}`;
|
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);
|
const res = await api.get(url);
|
||||||
setBounties(res.data.data);
|
setBounties(res.data.data);
|
||||||
setTotal(res.data.total);
|
setTotal(res.data.total);
|
||||||
@@ -94,10 +95,9 @@ const Bounty: React.FC = () => {
|
|||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / limit));
|
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 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) => {
|
const handleSortCol = (col: string) => {
|
||||||
if (sortCol === col) {
|
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(() => {
|
const sortedBounties = useMemo(() => {
|
||||||
if (!sortCol) return bounties;
|
if (!sortCol) return filteredBounties;
|
||||||
return [...bounties].sort((a, b) => {
|
return [...filteredBounties].sort((a, b) => {
|
||||||
let av: string | number = '';
|
let av: string | number = '';
|
||||||
let bv: string | number = '';
|
let bv: string | number = '';
|
||||||
if (sortCol === 'time') { av = a.timestamp; bv = b.timestamp; }
|
if (sortCol === 'time') { av = a.timestamp; bv = b.timestamp; }
|
||||||
@@ -122,7 +127,7 @@ const Bounty: React.FC = () => {
|
|||||||
const cmp = String(av).localeCompare(String(bv));
|
const cmp = String(av).localeCompare(String(bv));
|
||||||
return sortDir === 'asc' ? cmp : -cmp;
|
return sortDir === 'asc' ? cmp : -cmp;
|
||||||
});
|
});
|
||||||
}, [bounties, sortCol, sortDir]);
|
}, [filteredBounties, sortCol, sortDir]);
|
||||||
|
|
||||||
const SortTh: React.FC<{ col: string; children: React.ReactNode }> = ({ col, children }) => (
|
const SortTh: React.FC<{ col: string; children: React.ReactNode }> = ({ col, children }) => (
|
||||||
<th
|
<th
|
||||||
@@ -136,10 +141,9 @@ const Bounty: React.FC = () => {
|
|||||||
|
|
||||||
const SEGMENTS: [string, string][] = [
|
const SEGMENTS: [string, string][] = [
|
||||||
['', 'ALL'],
|
['', 'ALL'],
|
||||||
['credential', 'CREDENTIALS'],
|
|
||||||
['payload', 'PAYLOADS'],
|
|
||||||
['artifact', 'ARTIFACTS'],
|
['artifact', 'ARTIFACTS'],
|
||||||
['fingerprint', 'FINGERPRINTS'],
|
['fingerprint', 'FINGERPRINTS'],
|
||||||
|
['mail', 'EMAIL'],
|
||||||
];
|
];
|
||||||
|
|
||||||
const formatBytes = (n: any): string => {
|
const formatBytes = (n: any): string => {
|
||||||
@@ -159,7 +163,7 @@ const Bounty: React.FC = () => {
|
|||||||
<h1>BOUNTY VAULT</h1>
|
<h1>BOUNTY VAULT</h1>
|
||||||
</div>
|
</div>
|
||||||
<span className="page-sub">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Plus, Upload, X, AlertTriangle, Search,
|
Plus, Upload, X, AlertTriangle, Search, Target,
|
||||||
} from '../icons';
|
} from '../icons';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import { useEscapeKey } from '../hooks/useEscapeKey';
|
import { useEscapeKey } from '../hooks/useEscapeKey';
|
||||||
@@ -948,7 +948,10 @@ const CanaryTokens: React.FC = () => {
|
|||||||
<div className="fleet-root canary-tokens-root" style={{ padding: '24px', color: 'var(--text-color)' }}>
|
<div className="fleet-root canary-tokens-root" style={{ padding: '24px', color: 'var(--text-color)' }}>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>CANARY TOKENS</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Target size={22} className="violet-accent" />
|
||||||
|
<h1>CANARY TOKENS</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{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'}
|
{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'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -170,6 +170,23 @@ const Credentials: React.FC = () => {
|
|||||||
});
|
});
|
||||||
}, [creds, sortCol, sortDir]);
|
}, [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 }) => (
|
const SortTh: React.FC<{ col: string; children: React.ReactNode }> = ({ col, children }) => (
|
||||||
<th
|
<th
|
||||||
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
|
style={{ cursor: 'pointer', userSelect: 'none', whiteSpace: 'nowrap' }}
|
||||||
@@ -382,18 +399,18 @@ const Credentials: React.FC = () => {
|
|||||||
<table className="logs-table">
|
<table className="logs-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>LAST SEEN</th>
|
<SortTh col="seen">LAST SEEN</SortTh>
|
||||||
<th>PRINCIPAL</th>
|
<SortTh col="principal">PRINCIPAL</SortTh>
|
||||||
<th>KIND</th>
|
<SortTh col="kind">KIND</SortTh>
|
||||||
<th>TARGETS</th>
|
<SortTh col="targets">TARGETS</SortTh>
|
||||||
<th>ATTEMPTS</th>
|
<SortTh col="attempts">ATTEMPTS</SortTh>
|
||||||
<th>DECKIES</th>
|
<th>DECKIES</th>
|
||||||
<th>SERVICES</th>
|
<th>SERVICES</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{reuseRows.length > 0 ? reuseRows.map(r => {
|
{sortedReuseRows.length > 0 ? sortedReuseRows.map(r => {
|
||||||
const isPlain = r.secret_kind === 'plaintext';
|
const isPlain = r.secret_kind === 'plaintext';
|
||||||
const moreDeckies = Math.max(0, r.deckies.length - 3);
|
const moreDeckies = Math.max(0, r.deckies.length - 3);
|
||||||
const moreServices = Math.max(0, r.services.length - 3);
|
const moreServices = Math.max(0, r.services.length - 3);
|
||||||
|
|||||||
@@ -264,10 +264,15 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dash-grid > .logs-section {
|
||||||
|
max-height: 480px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.dash-grid > .logs-section .logs-table-container {
|
.dash-grid > .logs-section .logs-table-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 420px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dash-side {
|
.dash-side {
|
||||||
@@ -281,11 +286,13 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dash-side > .logs-section .panel-body {
|
.dash-side > .logs-section .panel-body {
|
||||||
max-height: 260px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Attacker/siege rows */
|
/* Attacker/siege rows */
|
||||||
|
|||||||
@@ -1553,7 +1553,10 @@ const DeckyFleet: React.FC<FleetProps> = ({ searchQuery = '' }) => {
|
|||||||
<div className="fleet-root">
|
<div className="fleet-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>DECOY FLEET</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Server size={22} className="violet-accent" />
|
||||||
|
<h1>DECOY FLEET</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{deckies.length} DECKIES DEPLOYED · {counts.active + counts.hot} ACTIVE · {counts.hot} UNDER SIEGE
|
{deckies.length} DECKIES DEPLOYED · {counts.active + counts.hot} ACTIVE · {counts.hot} UNDER SIEGE
|
||||||
{deployMode && (
|
{deployMode && (
|
||||||
|
|||||||
@@ -182,7 +182,10 @@ const LiveLogs: React.FC = () => {
|
|||||||
<div className="logs-root">
|
<div className="logs-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>LOGS</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Terminal size={22} className="violet-accent" />
|
||||||
|
<h1>LOGS</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{filteredLogs.length.toLocaleString()} SHOWN · {totalLogs.toLocaleString()} MATCHES · STREAM {streaming ? 'LIVE' : 'PAUSED'}
|
{filteredLogs.length.toLocaleString()} SHOWN · {totalLogs.toLocaleString()} MATCHES · STREAM {streaming ? 'LIVE' : 'PAUSED'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Mail, Plus, Pencil, Trash2, Check, AlertTriangle, Upload, Download,
|
Mail, Plus, Pencil, Trash2, Check, AlertTriangle, Upload, Download, Sparkles,
|
||||||
} from '../icons';
|
} from '../icons';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import { useToast } from './Toasts/useToast';
|
import { useToast } from './Toasts/useToast';
|
||||||
@@ -736,7 +736,10 @@ const PersonaGeneration: React.FC<PersonaGenerationProps> = ({ topologyId }) =>
|
|||||||
<div className="fleet-root persona-gen-root">
|
<div className="fleet-root persona-gen-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>{isTopology ? 'TOPOLOGY PERSONAS' : 'PERSONA GENERATION'}</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Sparkles size={22} className="violet-accent" />
|
||||||
|
<h1>{isTopology ? 'TOPOLOGY PERSONAS' : 'PERSONA GENERATION'}</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{personas.length} PERSONA{personas.length === 1 ? '' : 'S'} · {llmHeavyCount} LLM-HEAVY
|
{personas.length} PERSONA{personas.length === 1 ? '' : 'S'} · {llmHeavyCount} LLM-HEAVY
|
||||||
{isTopology
|
{isTopology
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { useToast } from '../Toasts/useToast';
|
import { useToast } from '../Toasts/useToast';
|
||||||
import { Save, RotateCcw, AlertTriangle } from '../../icons';
|
import { Save, RotateCcw, AlertTriangle, Sliders } from '../../icons';
|
||||||
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
||||||
// Reuse the DeckyFleet shell (page-header / btn / fleet-* / dim / mono) and
|
// Reuse the DeckyFleet shell (page-header / btn / fleet-* / dim / mono) and
|
||||||
// the persona-page tweaks (info-banner, .input) so the realism config panel
|
// the persona-page tweaks (info-banner, .input) so the realism config panel
|
||||||
@@ -168,7 +168,10 @@ const RealismConfig: React.FC = () => {
|
|||||||
<div className="fleet-root realism-config-root">
|
<div className="fleet-root realism-config-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>REALISM CONFIG</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Sliders size={22} className="violet-accent" />
|
||||||
|
<h1>REALISM CONFIG</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
USER {totals.user} · SYSTEM {totals.system} · CANARY {totals.canary} ·
|
USER {totals.user} · SYSTEM {totals.system} · CANARY {totals.canary} ·
|
||||||
{' '}CANARY PROB {(config.canary_probability * 100).toFixed(1)}%
|
{' '}CANARY PROB {(config.canary_probability * 100).toFixed(1)}%
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
||||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||||
import { X } from '../../icons';
|
import { X, FileText } from '../../icons';
|
||||||
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
import { contentClassLabel, isCanaryClass } from '../../realism/labels';
|
||||||
// Reuse the DeckyFleet shell + the persona-page tweaks so this page reads
|
// Reuse the DeckyFleet shell + the persona-page tweaks so this page reads
|
||||||
// the same as the rest of the realism nav group.
|
// the same as the rest of the realism nav group.
|
||||||
@@ -241,7 +241,10 @@ const SyntheticFiles: React.FC = () => {
|
|||||||
<div className="fleet-root synthetic-files-root">
|
<div className="fleet-root synthetic-files-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>SYNTHETIC FILES</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<FileText size={22} className="violet-accent" />
|
||||||
|
<h1>SYNTHETIC FILES</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{total} TOTAL · PAGE {page + 1} / {totalPages}
|
{total} TOTAL · PAGE {page + 1} / {totalPages}
|
||||||
{filtersActive ? ' · FILTERED' : ''}
|
{filtersActive ? ' · FILTERED' : ''}
|
||||||
|
|||||||
@@ -154,7 +154,10 @@ const TopologyList: React.FC = () => {
|
|||||||
<div className="tlist-root tlist-page">
|
<div className="tlist-root tlist-page">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>TOPOLOGIES</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Network size={22} className="violet-accent" />
|
||||||
|
<h1>TOPOLOGIES</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{loading ? 'LOADING…' : `${rows.length} ${rows.length === 1 ? 'TOPOLOGY' : 'TOPOLOGIES'}`}
|
{loading ? 'LOADING…' : `${rows.length} ${rows.length === 1 ? 'TOPOLOGY' : 'TOPOLOGIES'}`}
|
||||||
{err && <span className="alert-text"> · {err}</span>}
|
{err && <span className="alert-text"> · {err}</span>}
|
||||||
|
|||||||
@@ -292,7 +292,10 @@ const Webhooks: React.FC = () => {
|
|||||||
<div className="webhooks-root">
|
<div className="webhooks-root">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
<div className="page-title-group">
|
<div className="page-title-group">
|
||||||
<h1>WEBHOOKS</h1>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<WebhookIcon size={22} className="violet-accent" />
|
||||||
|
<h1>WEBHOOKS</h1>
|
||||||
|
</div>
|
||||||
<span className="page-sub">
|
<span className="page-sub">
|
||||||
{webhooks.length} CONFIGURED · {enabledCount} ENABLED
|
{webhooks.length} CONFIGURED · {enabledCount} ENABLED
|
||||||
{trippedCount > 0 && ` · ${trippedCount} TRIPPED`}
|
{trippedCount > 0 && ` · ${trippedCount} TRIPPED`}
|
||||||
|
|||||||
Reference in New Issue
Block a user