v0.9.6: InfoNet hashchain, Wormhole gate encryption, mesh reputation, 16 community contributors

Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them
through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation
system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery,
killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption
keys and chain state during updates.

New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers,
CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets,
desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing).

Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami,
@chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const,
@Elhard1, @ttulttul
This commit is contained in:
anoracleofra-code
2026-03-26 05:58:04 -06:00
parent d363013742
commit 668ce16dc7
363 changed files with 170430 additions and 23203 deletions
+941
View File
@@ -0,0 +1,941 @@
'use client';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
AlertTriangle,
ChevronDown,
ChevronUp,
Download,
KeyRound,
Radar,
RefreshCw,
Save,
Search,
Server,
ShieldAlert,
Upload,
} from 'lucide-react';
import type { SelectedEntity } from '@/types/dashboard';
import type {
ShodanCountResponse,
ShodanHost,
ShodanSearchMatch,
ShodanStatusResponse,
ShodanStyleConfig,
ShodanMarkerShape,
ShodanMarkerSize,
} from '@/types/shodan';
import { countShodan, fetchShodanStatus, lookupShodanHost, searchShodan } from '@/lib/shodanClient';
type Mode = 'search' | 'count' | 'host';
type ShodanPreset = {
id: string;
label: string;
mode: Mode;
query: string;
page: number;
facets: string;
hostIp: string;
style?: ShodanStyleConfig;
};
const SHODAN_PRESETS_KEY = 'sb_shodan_presets_v1';
const SHODAN_STYLE_KEY = 'sb_shodan_style_v1';
const DEFAULT_STYLE: ShodanStyleConfig = { shape: 'circle', color: '#16a34a', size: 'md' };
const SHAPE_OPTIONS: { value: ShodanMarkerShape; label: string; glyph: string }[] = [
{ value: 'circle', label: 'Circle', glyph: '●' },
{ value: 'triangle', label: 'Triangle', glyph: '▲' },
{ value: 'diamond', label: 'Diamond', glyph: '◆' },
{ value: 'square', label: 'Square', glyph: '■' },
];
const SIZE_OPTIONS: { value: ShodanMarkerSize; label: string }[] = [
{ value: 'sm', label: 'SM' },
{ value: 'md', label: 'MD' },
{ value: 'lg', label: 'LG' },
];
const COLOR_SWATCHES = [
'#16a34a', '#ef4444', '#3b82f6', '#06b6d4',
'#f97316', '#eab308', '#ec4899', '#e2e8f0',
];
interface Props {
onOpenSettings: () => void;
onResultsChange: (results: ShodanSearchMatch[], queryLabel: string) => void;
onSelectEntity: (entity: SelectedEntity | null) => void;
onStyleChange: (style: ShodanStyleConfig) => void;
currentResults: ShodanSearchMatch[];
isMinimized?: boolean;
onMinimizedChange?: (minimized: boolean) => void;
/** When true the settings modal is open — status auto-refreshes on close. */
settingsOpen?: boolean;
}
function toSelectedEntity(match: ShodanSearchMatch): SelectedEntity {
return {
id: match.id,
type: 'shodan_host',
name: `${match.ip}${match.port ? `:${match.port}` : ''}`,
extra: { ...match },
};
}
function fromHost(host: ShodanHost): ShodanSearchMatch {
return {
id: host.id,
ip: host.ip,
port: host.ports?.[0] ?? null,
lat: host.lat,
lng: host.lng,
city: host.city,
region_code: host.region_code,
country_code: host.country_code,
country_name: host.country_name,
location_label: host.location_label,
asn: host.asn,
org: host.org,
isp: host.isp,
os: host.os,
product: host.services?.[0]?.product ?? null,
transport: host.services?.[0]?.transport ?? null,
timestamp: host.services?.[0]?.timestamp ?? null,
hostnames: host.hostnames,
domains: host.domains,
tags: host.tags,
vulns: host.vulns,
data_snippet: host.services?.[0]?.banner_excerpt ?? null,
attribution: host.attribution,
};
}
function facetList(raw: string): string[] {
return raw
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.slice(0, 8);
}
function downloadText(filename: string, content: string, mime = 'application/json') {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function buildCsv(rows: ShodanSearchMatch[]): string {
const headers = [
'source',
'attribution',
'ip',
'port',
'country_code',
'location_label',
'org',
'asn',
'product',
'transport',
'timestamp',
];
const esc = (value: unknown) => `"${String(value ?? '').replaceAll('"', '""')}"`;
return [
headers.join(','),
...rows.map((row) =>
[
'Shodan',
row.attribution || 'Data from Shodan',
row.ip,
row.port ?? '',
row.country_code ?? '',
row.location_label ?? '',
row.org ?? '',
row.asn ?? '',
row.product ?? '',
row.transport ?? '',
row.timestamp ?? '',
]
.map(esc)
.join(','),
),
].join('\n');
}
export default function ShodanPanel({
onOpenSettings,
onResultsChange,
onSelectEntity,
onStyleChange,
currentResults,
isMinimized: isMinimizedProp,
onMinimizedChange,
settingsOpen,
}: Props) {
const [internalMinimized, setInternalMinimized] = useState(true);
const isMinimized = isMinimizedProp !== undefined ? isMinimizedProp : internalMinimized;
const setIsMinimized = (val: boolean | ((prev: boolean) => boolean)) => {
const newVal = typeof val === 'function' ? val(isMinimized) : val;
setInternalMinimized(newVal);
onMinimizedChange?.(newVal);
};
const [mode, setMode] = useState<Mode>('search');
const [status, setStatus] = useState<ShodanStatusResponse | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [query, setQuery] = useState('port:443');
const [page, setPage] = useState(1);
const [facets, setFacets] = useState('country,port,org');
const [hostIp, setHostIp] = useState('');
const [presetLabel, setPresetLabel] = useState('');
const [presets, setPresets] = useState<ShodanPreset[]>([]);
const [countSummary, setCountSummary] = useState<ShodanCountResponse | null>(null);
const [hostSummary, setHostSummary] = useState<ShodanHost | null>(null);
const [styleConfig, setStyleConfig] = useState<ShodanStyleConfig>(DEFAULT_STYLE);
const [customHex, setCustomHex] = useState('');
const [lastAction, setLastAction] = useState<(() => void) | null>(null);
const [unmappedCount, setUnmappedCount] = useState(0);
const prevSettingsOpen = useRef(settingsOpen);
const presetImportRef = useRef<HTMLInputElement | null>(null);
const resultImportRef = useRef<HTMLInputElement | null>(null);
const refreshStatus = useCallback(async () => {
try {
const next = await fetchShodanStatus();
setStatus(next);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load Shodan status');
}
}, []);
useEffect(() => {
void refreshStatus();
}, [refreshStatus]);
// Auto-refresh status when settings modal closes (key may have changed)
useEffect(() => {
if (prevSettingsOpen.current && !settingsOpen) {
void refreshStatus();
}
prevSettingsOpen.current = settingsOpen;
}, [settingsOpen, refreshStatus]);
useEffect(() => {
try {
const raw = window.localStorage.getItem(SHODAN_PRESETS_KEY);
if (!raw) return;
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) {
setPresets(parsed);
}
} catch {
// ignore bad local preset state
}
}, []);
useEffect(() => {
window.localStorage.setItem(SHODAN_PRESETS_KEY, JSON.stringify(presets));
}, [presets]);
// Load persisted style config
useEffect(() => {
try {
const raw = window.localStorage.getItem(SHODAN_STYLE_KEY);
if (!raw) return;
const parsed = JSON.parse(raw) as ShodanStyleConfig;
if (parsed && parsed.shape && parsed.color && parsed.size) {
setStyleConfig(parsed);
// Defer parent update to avoid setState-during-render
queueMicrotask(() => onStyleChange(parsed));
}
} catch { /* ignore */ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updateStyle = useCallback((patch: Partial<ShodanStyleConfig>) => {
setStyleConfig((prev) => {
const next = { ...prev, ...patch };
window.localStorage.setItem(SHODAN_STYLE_KEY, JSON.stringify(next));
// Defer parent update out of the setState updater
queueMicrotask(() => onStyleChange(next));
return next;
});
}, [onStyleChange]);
const handleSearch = useCallback(async () => {
setBusy(true);
setError(null);
setLastAction(() => () => void handleSearch());
try {
const resp = await searchShodan(query, page, facetList(facets));
const mapped = resp.matches.filter((match) => match.lat != null && match.lng != null);
setUnmappedCount(resp.matches.length - mapped.length);
onResultsChange(mapped, resp.query);
setCountSummary({
ok: true,
source: resp.source,
attribution: resp.attribution,
query: resp.query,
total: resp.total,
facets: resp.facets,
note: resp.note,
});
setHostSummary(null);
setLastAction(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Shodan search failed');
} finally {
setBusy(false);
}
}, [facets, onResultsChange, page, query]);
const handleCount = useCallback(async () => {
setBusy(true);
setError(null);
setLastAction(() => () => void handleCount());
try {
const resp = await countShodan(query, facetList(facets));
setCountSummary(resp);
setHostSummary(null);
setLastAction(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Shodan count failed');
} finally {
setBusy(false);
}
}, [facets, query]);
const handleHost = useCallback(async () => {
setBusy(true);
setError(null);
setLastAction(() => () => void handleHost());
try {
const resp = await lookupShodanHost(hostIp);
setHostSummary(resp.host);
setCountSummary(null);
const mapped = fromHost(resp.host);
onResultsChange(
resp.host.lat != null && resp.host.lng != null ? [mapped] : [],
`HOST ${resp.host.ip}`,
);
onSelectEntity({
id: mapped.id,
type: 'shodan_host',
name: `${mapped.ip}${mapped.port ? `:${mapped.port}` : ''}`,
extra: { ...resp.host, ...mapped },
});
setLastAction(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Shodan host lookup failed');
} finally {
setBusy(false);
}
}, [hostIp, onResultsChange, onSelectEntity]);
const handleClear = useCallback(() => {
onResultsChange([], '');
onSelectEntity(null);
setCountSummary(null);
setHostSummary(null);
setError(null);
setLastAction(null);
setUnmappedCount(0);
}, [onResultsChange, onSelectEntity]);
const handleSavePreset = useCallback(() => {
const label =
presetLabel.trim() ||
(mode === 'host' ? hostIp.trim() || 'Host Lookup' : query.trim() || 'Shodan Query');
const preset: ShodanPreset = {
id: `preset-${Date.now()}`,
label,
mode,
query,
page,
facets,
hostIp,
style: { ...styleConfig },
};
setPresets((prev) => [preset, ...prev].slice(0, 16));
setPresetLabel('');
}, [facets, hostIp, mode, page, presetLabel, query, styleConfig]);
const applyPreset = useCallback((preset: ShodanPreset) => {
setMode(preset.mode);
setQuery(preset.query);
setPage(preset.page);
setFacets(preset.facets);
setHostIp(preset.hostIp);
if (preset.style) {
updateStyle(preset.style);
}
}, [updateStyle]);
const removePreset = useCallback((id: string) => {
setPresets((prev) => prev.filter((preset) => preset.id !== id));
}, []);
const exportPresets = useCallback(() => {
downloadText(
`shadowbroker-shodan-presets-${new Date().toISOString().slice(0, 10)}.json`,
JSON.stringify({ source: 'ShadowBroker', type: 'shodan-presets', presets }, null, 2),
);
}, [presets]);
const importPresets = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
const parsed = JSON.parse(text) as { presets?: ShodanPreset[] };
const incoming = Array.isArray(parsed?.presets) ? parsed.presets : [];
const sanitized = incoming
.filter((preset) => preset && typeof preset.label === 'string')
.map((preset) => ({
id: preset.id || `preset-${Date.now()}-${Math.random()}`,
label: String(preset.label || 'Imported Preset'),
mode: (preset.mode === 'host' || preset.mode === 'count' ? preset.mode : 'search') as Mode,
query: String(preset.query || ''),
page: Math.max(1, Math.min(2, Number(preset.page) || 1)),
facets: String(preset.facets || ''),
hostIp: String(preset.hostIp || ''),
}));
setPresets((prev) => [...sanitized, ...prev].slice(0, 16));
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to import Shodan presets');
} finally {
event.target.value = '';
}
},
[],
);
const exportResultsJson = useCallback(() => {
if (!currentResults.length) return;
downloadText(
`shadowbroker-shodan-results-${new Date().toISOString().replace(/[:.]/g, '-')}.json`,
JSON.stringify(
{
source: 'Shodan',
attribution: 'Data from Shodan',
exported_at: new Date().toISOString(),
results: currentResults,
},
null,
2,
),
);
}, [currentResults]);
const exportResultsCsv = useCallback(() => {
if (!currentResults.length) return;
downloadText(
`shadowbroker-shodan-results-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`,
buildCsv(currentResults),
'text/csv',
);
}, [currentResults]);
const importResults = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
const parsed = JSON.parse(text) as { results?: ShodanSearchMatch[]; attribution?: string };
const incoming = Array.isArray(parsed?.results) ? parsed.results : [];
const sanitized = incoming
.filter((row) => row && typeof row.ip === 'string')
.map((row) => ({
...row,
id: String(row.id || `shodan-import-${row.ip}-${row.port || 'na'}`),
ip: String(row.ip),
port: row.port == null ? null : Number(row.port),
lat: row.lat == null ? null : Number(row.lat),
lng: row.lng == null ? null : Number(row.lng),
hostnames: Array.isArray(row.hostnames) ? row.hostnames.map(String) : [],
domains: Array.isArray(row.domains) ? row.domains.map(String) : [],
tags: Array.isArray(row.tags) ? row.tags.map(String) : [],
vulns: Array.isArray(row.vulns) ? row.vulns.map(String) : [],
attribution: String(row.attribution || parsed?.attribution || 'Data from Shodan'),
}))
.filter((row) => row.lat != null && row.lng != null);
onResultsChange(sanitized, 'IMPORTED RESULTS');
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to import Shodan results');
} finally {
event.target.value = '';
}
},
[onResultsChange],
);
const resultSummary = useMemo(() => {
if (hostSummary) {
return `${hostSummary.ip} · ${hostSummary.location_label || 'unmapped'} · ${hostSummary.ports.length} ports`;
}
if (countSummary) {
const unmappedNote = unmappedCount > 0 ? ` · ${unmappedCount} without coordinates` : '';
return `${countSummary.total.toLocaleString()} matching hosts${unmappedNote}`;
}
if (currentResults.length) {
const unmappedNote = unmappedCount > 0 ? ` · ${unmappedCount} without coordinates` : '';
return `${currentResults.length.toLocaleString()} mapped results${unmappedNote}`;
}
return 'No local Shodan overlay loaded';
}, [countSummary, currentResults.length, hostSummary, unmappedCount]);
return (
<div className="pointer-events-auto flex-shrink-0 border border-green-700/40 bg-black/75 backdrop-blur-sm shadow-[0_0_18px_rgba(34,197,94,0.12)]">
<div
className="flex items-center justify-between border-b border-green-700/30 bg-green-950/20 px-3 py-2 cursor-pointer"
onClick={() => setIsMinimized((prev) => !prev)}
>
<div className="flex items-center gap-2">
<Radar size={13} className="text-green-400" />
<span className="text-[12px] font-mono font-bold tracking-[0.25em] text-green-400">
SHODAN CONNECTOR
</span>
</div>
<div className="flex items-center gap-2 text-[8px] font-mono">
<span className="border border-green-700/40 px-1.5 py-0.5 text-green-300">
{currentResults.length.toLocaleString()} MAP
</span>
<span className="border border-green-700/40 px-1.5 py-0.5 text-green-500/80">
LOCAL
</span>
{isMinimized ? (
<ChevronUp size={12} className="text-green-500" />
) : (
<ChevronDown size={12} className="text-green-500" />
)}
</div>
</div>
{!isMinimized && (
<>
<div className="border-b border-green-900/40 bg-green-950/10 px-3 py-2 text-[10px] font-mono leading-relaxed text-green-200/90">
<div className="flex items-start gap-2">
<AlertTriangle size={12} className="mt-0.5 text-green-400" />
<div>
<div className="font-bold tracking-wider text-green-400">PAID API / OPERATOR-SUPPLIED KEY</div>
<div>
Data from Shodan is fetched with the local <span className="text-green-400">SHODAN_API_KEY</span>,
rendered as a temporary overlay, and remains the operator&apos;s responsibility.
</div>
</div>
</div>
</div>
<div className="px-3 py-2">
<div className="mb-2 flex items-center gap-2 text-[9px] font-mono">
{(['search', 'count', 'host'] as Mode[]).map((item) => (
<button
key={item}
onClick={() => setMode(item)}
className={`border px-2 py-1 tracking-[0.2em] transition-colors ${
mode === item
? 'border-green-500/50 bg-green-950/30 text-green-300'
: 'border-green-900/40 text-green-600 hover:border-green-700/60 hover:text-green-400'
}`}
>
{item.toUpperCase()}
</button>
))}
<button
onClick={refreshStatus}
className="ml-auto border border-green-900/40 px-2 py-1 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400"
>
STATUS
</button>
</div>
{!status?.configured && (
<div className="mb-3 border border-yellow-700/30 bg-yellow-950/10 px-3 py-2 text-[10px] font-mono text-yellow-300">
<div className="mb-2 flex items-center gap-2 font-bold tracking-wide">
<KeyRound size={12} /> SHODAN_API_KEY REQUIRED
</div>
<button
onClick={onOpenSettings}
className="border border-green-600/40 px-2 py-1 text-green-400 transition-colors hover:border-green-500/70"
>
OPEN SETTINGS
</button>
</div>
)}
<div className="space-y-2 text-[10px] font-mono">
{mode !== 'host' ? (
<>
<div className="flex items-center gap-2">
<Search size={12} className="text-green-500" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={'query (e.g. port:443 org:"Amazon")'}
className="flex-1 border border-green-900/50 bg-black/70 px-2 py-1.5 text-green-300 outline-none transition-colors focus:border-green-500/60"
/>
</div>
<div className="flex items-center gap-2">
<input
value={facets}
onChange={(e) => setFacets(e.target.value)}
placeholder="facets (country,port,org)"
className="flex-1 border border-green-900/50 bg-black/70 px-2 py-1.5 text-green-300 outline-none transition-colors focus:border-green-500/60"
/>
{mode === 'search' && (
<input
type="number"
min={1}
max={2}
value={page}
onChange={(e) => setPage(Math.max(1, Math.min(2, Number(e.target.value) || 1)))}
className="w-16 border border-green-900/50 bg-black/70 px-2 py-1.5 text-green-300 outline-none transition-colors focus:border-green-500/60"
/>
)}
</div>
</>
) : (
<div className="flex items-center gap-2">
<Server size={12} className="text-green-500" />
<input
value={hostIp}
onChange={(e) => setHostIp(e.target.value)}
placeholder="host IP (e.g. 8.8.8.8)"
className="flex-1 border border-green-900/50 bg-black/70 px-2 py-1.5 text-green-300 outline-none transition-colors focus:border-green-500/60"
/>
</div>
)}
</div>
<div className="mt-3 flex items-center gap-2 text-[9px] font-mono">
{mode === 'search' && (
<button
onClick={() => void handleSearch()}
disabled={busy || !status?.configured}
className="border border-green-600/40 px-2.5 py-1.5 text-green-400 transition-colors hover:border-green-500/70 disabled:cursor-not-allowed disabled:opacity-40"
>
SEARCH / MAP
</button>
)}
{mode === 'count' && (
<button
onClick={() => void handleCount()}
disabled={busy || !status?.configured}
className="border border-green-600/40 px-2.5 py-1.5 text-green-400 transition-colors hover:border-green-500/70 disabled:cursor-not-allowed disabled:opacity-40"
>
COUNT / FACETS
</button>
)}
{mode === 'host' && (
<button
onClick={() => void handleHost()}
disabled={busy || !status?.configured}
className="border border-green-600/40 px-2.5 py-1.5 text-green-400 transition-colors hover:border-green-500/70 disabled:cursor-not-allowed disabled:opacity-40"
>
LOOKUP / MAP
</button>
)}
<button
onClick={handleClear}
className="border border-green-900/40 px-2.5 py-1.5 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400"
>
CLEAR
</button>
</div>
{/* ── Marker Style Configurator ── */}
<div className="mt-3 border border-green-900/40 bg-black/80 px-3 py-2">
<div className="mb-2 flex items-center justify-between">
<span className="text-[9px] font-mono tracking-[0.22em] text-green-500">MARKER STYLE</span>
<span className="text-[14px] leading-none" style={{ color: styleConfig.color }}>
{SHAPE_OPTIONS.find((s) => s.value === styleConfig.shape)?.glyph ?? '●'}
</span>
</div>
{/* Shape */}
<div className="mb-2">
<div className="mb-1 text-[8px] font-mono tracking-widest text-green-600">SHAPE</div>
<div className="flex items-center gap-1.5">
{SHAPE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => updateStyle({ shape: opt.value })}
className={`flex items-center justify-center w-8 h-7 border text-[13px] transition-colors ${
styleConfig.shape === opt.value
? 'border-green-500/60 bg-green-950/40 text-green-300'
: 'border-green-900/40 text-green-700 hover:border-green-700/60 hover:text-green-400'
}`}
title={opt.label}
>
{opt.glyph}
</button>
))}
</div>
</div>
{/* Color */}
<div className="mb-2">
<div className="mb-1 text-[8px] font-mono tracking-widest text-green-600">COLOR</div>
<div className="flex items-center gap-1.5 flex-wrap">
{COLOR_SWATCHES.map((hex) => (
<button
key={hex}
onClick={() => { updateStyle({ color: hex }); setCustomHex(''); }}
className={`w-5 h-5 border transition-all ${
styleConfig.color === hex && !customHex
? 'border-white scale-110'
: 'border-green-900/40 hover:border-green-600/60'
}`}
style={{ backgroundColor: hex }}
title={hex}
/>
))}
<input
value={customHex}
onChange={(e) => {
const v = e.target.value;
setCustomHex(v);
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
updateStyle({ color: v });
}
}}
placeholder="#hex"
maxLength={7}
className="w-16 border border-green-900/50 bg-black/70 px-1.5 py-0.5 text-[9px] font-mono text-green-300 outline-none focus:border-green-500/60"
/>
</div>
</div>
{/* Size */}
<div>
<div className="mb-1 text-[8px] font-mono tracking-widest text-green-600">SIZE</div>
<div className="flex items-center gap-1.5">
{SIZE_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => updateStyle({ size: opt.value })}
className={`px-2.5 py-1 border text-[9px] font-mono tracking-wider transition-colors ${
styleConfig.size === opt.value
? 'border-green-500/60 bg-green-950/40 text-green-300'
: 'border-green-900/40 text-green-700 hover:border-green-700/60 hover:text-green-400'
}`}
>
{opt.label}
</button>
))}
</div>
</div>
</div>
<div className="mt-3 border border-green-900/40 bg-black/80 px-3 py-2">
<div className="mb-2 text-[9px] font-mono tracking-[0.22em] text-green-500">PRESETS / EXPORT</div>
<div className="mb-2 flex items-center gap-2">
<input
value={presetLabel}
onChange={(e) => setPresetLabel(e.target.value)}
placeholder="preset label"
className="flex-1 border border-green-900/50 bg-black/70 px-2 py-1.5 text-[10px] text-green-300 outline-none transition-colors focus:border-green-500/60"
/>
<button
onClick={handleSavePreset}
className="border border-green-600/40 px-2 py-1.5 text-[9px] font-mono text-green-400 transition-colors hover:border-green-500/70"
>
<span className="inline-flex items-center gap-1">
<Save size={10} /> SAVE
</span>
</button>
</div>
<div className="flex flex-wrap gap-2 text-[9px] font-mono">
<button
onClick={exportPresets}
disabled={!presets.length}
className="border border-green-900/40 px-2 py-1.5 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400 disabled:opacity-40"
>
<span className="inline-flex items-center gap-1">
<Download size={10} /> EXPORT PRESETS
</span>
</button>
<button
onClick={() => presetImportRef.current?.click()}
className="border border-green-900/40 px-2 py-1.5 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400"
>
<span className="inline-flex items-center gap-1">
<Upload size={10} /> IMPORT PRESETS
</span>
</button>
<button
onClick={exportResultsJson}
disabled={!currentResults.length}
className="border border-green-900/40 px-2 py-1.5 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400 disabled:opacity-40"
>
<span className="inline-flex items-center gap-1">
<Download size={10} /> RESULTS JSON
</span>
</button>
<button
onClick={exportResultsCsv}
disabled={!currentResults.length}
className="border border-green-900/40 px-2 py-1.5 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400 disabled:opacity-40"
>
<span className="inline-flex items-center gap-1">
<Download size={10} /> RESULTS CSV
</span>
</button>
<button
onClick={() => resultImportRef.current?.click()}
className="border border-green-900/40 px-2 py-1.5 text-green-600 transition-colors hover:border-green-700/60 hover:text-green-400"
>
<span className="inline-flex items-center gap-1">
<Upload size={10} /> IMPORT RESULTS
</span>
</button>
<input
ref={presetImportRef}
type="file"
accept=".json,application/json"
className="hidden"
onChange={(e) => void importPresets(e)}
/>
<input
ref={resultImportRef}
type="file"
accept=".json,application/json"
className="hidden"
onChange={(e) => void importResults(e)}
/>
</div>
{presets.length > 0 && (
<div className="mt-3 max-h-32 space-y-1 overflow-y-auto styled-scrollbar">
{presets.map((preset) => (
<div
key={preset.id}
className="flex items-center justify-between border border-green-950/40 bg-green-950/10 px-2 py-1.5"
>
<button
onClick={() => applyPreset(preset)}
className="min-w-0 flex-1 truncate text-left text-[10px] font-mono text-green-300 transition-colors hover:text-green-200"
>
{preset.label}
</button>
<button
onClick={() => removePreset(preset.id)}
className="ml-2 text-[9px] font-mono text-green-700/70 transition-colors hover:text-red-300"
>
DELETE
</button>
</div>
))}
</div>
)}
</div>
<div className="mt-3 border border-green-900/40 bg-black/80 px-3 py-2 text-[10px] font-mono">
<div className="mb-1 flex items-center gap-2 text-green-500">
<ShieldAlert size={12} />
<span className="tracking-[0.25em]">SESSION STATUS</span>
</div>
<div className="text-green-300/90">{resultSummary}</div>
{status?.warning && <div className="mt-1 text-green-500/80">{status.warning}</div>}
{error && (
<div className="mt-2 flex items-center justify-between border border-red-900/40 bg-red-950/20 px-2 py-1.5 text-red-300">
<span>{error}</span>
{lastAction && (
<button
onClick={() => { setError(null); lastAction(); }}
disabled={busy}
className="ml-2 inline-flex shrink-0 items-center gap-1 border border-red-700/40 px-1.5 py-0.5 text-[9px] font-mono text-red-300 transition-colors hover:border-red-500/60 hover:text-red-200 disabled:opacity-40"
>
<RefreshCw size={9} /> RETRY
</button>
)}
</div>
)}
</div>
{countSummary && (
<div className="mt-3 max-h-40 space-y-2 overflow-y-auto border border-green-900/40 bg-black/80 p-3 styled-scrollbar">
<div className="text-[9px] font-mono tracking-[0.22em] text-green-500">FACETS</div>
{Object.entries(countSummary.facets).length === 0 ? (
<div className="text-[10px] font-mono text-green-300/80">No facet buckets returned.</div>
) : (
Object.entries(countSummary.facets).map(([name, buckets]) => (
<div key={name}>
<div className="mb-1 text-[9px] font-mono text-green-400">{name.toUpperCase()}</div>
<div className="space-y-1">
{buckets.map((bucket) => (
<div key={`${name}-${bucket.value}`} className="flex items-center justify-between text-[10px] font-mono text-green-300/90">
<span className="truncate pr-3">{bucket.value || 'UNKNOWN'}</span>
<span>{bucket.count.toLocaleString()}</span>
</div>
))}
</div>
</div>
))
)}
</div>
)}
{hostSummary && (
<div className="mt-3 max-h-40 overflow-y-auto border border-green-900/40 bg-black/80 p-3 styled-scrollbar text-[10px] font-mono">
<div className="mb-2 flex items-center justify-between text-green-400">
<span>{hostSummary.ip}</span>
<span>{hostSummary.location_label || 'UNMAPPED'}</span>
</div>
<div className="grid grid-cols-2 gap-2 text-green-300/90">
<span>ORG</span>
<span className="text-right">{hostSummary.org || 'UNKNOWN'}</span>
<span>ASN</span>
<span className="text-right">{hostSummary.asn || 'UNKNOWN'}</span>
<span>ISP</span>
<span className="text-right">{hostSummary.isp || 'UNKNOWN'}</span>
<span>PORTS</span>
<span className="text-right">{hostSummary.ports.slice(0, 8).join(', ') || 'NONE'}</span>
</div>
</div>
)}
{currentResults.length > 0 && (
<div className="mt-3 max-h-44 overflow-y-auto border border-green-900/40 bg-black/80 p-2 styled-scrollbar">
<div className="mb-2 flex items-center justify-between text-[9px] font-mono text-green-500">
<span className="tracking-[0.22em]">MAPPED HOSTS</span>
<span>{currentResults.length.toLocaleString()}</span>
</div>
<div className="space-y-1.5">
{currentResults.slice(0, 12).map((match) => (
<button
key={match.id}
onClick={() => onSelectEntity(toSelectedEntity(match))}
className="flex w-full items-center justify-between border border-green-950/40 bg-green-950/10 px-2 py-1.5 text-left transition-colors hover:border-green-700/60 hover:bg-green-950/20"
>
<div className="min-w-0">
<div className="truncate text-[10px] font-mono text-green-300">
{match.ip}
{match.port ? `:${match.port}` : ''}
</div>
<div className="truncate text-[9px] font-mono text-green-600">
{match.location_label || match.org || 'UNMAPPED'}
</div>
</div>
<div className="ml-3 shrink-0 text-[8px] font-mono text-green-500">
{match.product || match.transport || 'HOST'}
</div>
</button>
))}
</div>
</div>
)}
</div>
</>
)}
</div>
);
}