refactor: Update UI components for consistency and improve layout

This commit is contained in:
elipeter 2026-05-06 04:38:04 -04:00
parent da619171cf
commit 77be7f10d9
74 changed files with 3186 additions and 618 deletions

View file

@ -170,7 +170,7 @@ function KvGrid({ entries }: { entries: Array<[string, React.ReactNode]> }) {
}
function fmt(v: unknown): React.ReactNode {
if (v === null || v === undefined) return <span className="muted"></span>;
if (v === null || v === undefined) return <span className="muted">-</span>;
if (typeof v === 'boolean')
return (
<span className={v ? 'pill pill-on' : 'pill pill-off'}>
@ -387,7 +387,7 @@ function RawEditor() {
value={draft ?? ''}
spellCheck={false}
onChange={(e) => setDraft(e.target.value)}
placeholder="# nyx.local overrides for the default config.&#10;# Anything you set here wins over nyx.conf."
placeholder="# nyx.local - overrides for the default config.&#10;# Anything you set here wins over nyx.conf."
/>
<p className="config-help">
Edits are validated against the full config schema before being written.
@ -608,7 +608,7 @@ export function ConfigPage() {
setProfileName('');
}, [profileName, addProfile]);
if (configLoading) return <LoadingState message="Loading configuration" />;
if (configLoading) return <LoadingState message="Loading configuration..." />;
if (configError) return <ErrorState message={configError.message} />;
const cfg = config as Record<string, Record<string, unknown>> | undefined;
@ -616,15 +616,7 @@ export function ConfigPage() {
const triageSyncOn = !!server?.triage_sync;
return (
<>
<div className="page-header">
<h2>Config</h2>
<span className="page-header-sub">
Edit defaults, rules, profiles, and the raw <code>nyx.local</code>{' '}
file
</span>
</div>
<div className="config-page page-shell">
<div className="config-tabs">
{(
[
@ -866,6 +858,6 @@ export function ConfigPage() {
)}
{tab === 'raw' && <RawEditor />}
</>
</div>
);
}

View file

@ -67,7 +67,7 @@ const STATE_REMEDIATION_HINTS: Record<string, string[]> = {
'Prefer a language-native cleanup pattern (defer, with, try-with-resources, RAII).',
],
'state-resource-leak-possible': [
'Ensure the resource is closed on all code paths including error and early-return paths.',
'Ensure the resource is closed on all code paths, including error and early-return paths.',
'Put cleanup in a finally/defer block rather than after the happy path.',
],
'state-unauthed-access': [
@ -636,7 +636,7 @@ const TAINT_REMEDIATION: Record<string, string[]> = {
'If HTML is unavoidable, run input through a well-maintained sanitizer (DOMPurify, Bleach).',
],
sql: [
'Use parameterized queries or a prepared statement — never concatenate user input into SQL.',
'Use parameterized queries or a prepared statement. Never concatenate user input into SQL.',
'Prefer an ORM or query builder that escapes parameters automatically.',
'Validate input type (integer, enum, allowlist) before the query.',
],
@ -878,6 +878,12 @@ export function FindingDetailPage() {
const hasRelated = f.related_findings && f.related_findings.length > 0;
const hasLabels = f.labels && f.labels.length > 0;
const hasCode = !!f.code_context;
const sourcePath = evidence?.source
? `${evidence.source.path}:${evidence.source.line}:${evidence.source.col}`
: null;
const sinkPath = evidence?.sink
? `${evidence.sink.path}:${evidence.sink.line}:${evidence.sink.col}`
: null;
const metaParts: string[] = [];
if (f.category) metaParts.push(f.category);
@ -894,7 +900,7 @@ export function FindingDetailPage() {
}
return (
<div className="detail-panel finding-detail">
<div className="detail-panel finding-detail page-shell">
<div className="detail-title-row">
<h2 className="finding-heading">
<span
@ -931,6 +937,22 @@ export function FindingDetailPage() {
))}
</div>
{(sourcePath || sinkPath) && (
<div className="path-trace" aria-label="Source to sink path">
<div className="path-trace-card">
<span className="path-trace-label">Source</span>
<code className="path-trace-path">{sourcePath || 'Unknown'}</code>
</div>
<div className="path-trace-arrow" aria-hidden>
&rarr;
</div>
<div className="path-trace-card">
<span className="path-trace-label">Sink</span>
<code className="path-trace-path">{sinkPath || 'Unknown'}</code>
</div>
</div>
)}
<StatusControl
finding={f}
onTriage={handleTriage}
@ -974,13 +996,6 @@ export function FindingDetailPage() {
<HowToFix finding={f} />
</CollapsibleSection>
{/* Evidence (collapsed by default — overlaps with taint flow) */}
{hasEvidence && (
<CollapsibleSection title="Evidence" defaultOpen={false}>
<EvidenceSection evidence={evidence!} skipStateCard={isState} />
</CollapsibleSection>
)}
{/* Analysis Notes */}
{hasNotes && (
<CollapsibleSection title="Analysis Notes" defaultOpen={false}>
@ -1002,20 +1017,6 @@ export function FindingDetailPage() {
</CollapsibleSection>
)}
{/* Labels */}
{hasLabels && (
<CollapsibleSection title="Labels" defaultOpen={false}>
<div className="label-list">
{f.labels.map(([k, v], i) => (
<span key={i} className="label-item">
<span className="label-key">{k}:</span>{' '}
<span className="label-value">{v}</span>
</span>
))}
</div>
</CollapsibleSection>
)}
{/* Code Preview */}
{hasCode && (
<CollapsibleSection title="Code Preview" defaultOpen={false}>

View file

@ -567,15 +567,7 @@ export function FindingsPage() {
const totalPages = Math.ceil(data.total / data.per_page) || 1;
return (
<>
<div className="page-header">
<h2>Findings</h2>
<span className="filter-count">
{data.total} finding{data.total !== 1 ? 's' : ''}
{hasActiveFilters ? ' (filtered)' : ''}
</span>
</div>
<div className="findings-page page-shell">
{/* Filter bar */}
<div className="filter-bar">
<input
@ -793,6 +785,6 @@ export function FindingsPage() {
onClose={() => setSuppressModalOpen(false)}
/>
)}
</>
</div>
);
}

View file

@ -64,7 +64,7 @@ export function OverviewPage() {
const categoryItems = (overview.issue_categories || [])
.slice(0, 8)
.map((b) => ({ label: b.label, value: b.count, color: '#72f3d7' }));
.map((b) => ({ label: b.label, value: b.count, color: 'var(--accent)' }));
const trendData = (trends || []).map((t) => ({
label: t.timestamp,
@ -74,11 +74,7 @@ export function OverviewPage() {
const hotSinks = overview.hot_sinks || [];
return (
<>
<div className="page-header">
<h2>Overview</h2>
</div>
<div className="overview-page page-shell">
{/* Baseline strip */}
<BaselinePinControl
baseline={overview.baseline}
@ -117,7 +113,7 @@ export function OverviewPage() {
</div>
)}
{/* Stat cards kept lean: 5 cards, severity stacks live in Top Files
{/* Stat cards kept lean: 5 cards, severity stacks live in Top Files
and Per-Language. Cross-file / Symex moved into Scanner Quality. */}
<div className="overview-stat-grid overview-stat-grid-5">
<StatCard
@ -145,7 +141,7 @@ export function OverviewPage() {
/>
</div>
{/* Charts */}
{/* Charts — 3-col: Findings (col1 span2) | OWASP+Confidence (col2) | Categories (col3 span2) */}
<div className="overview-chart-grid">
<div className="card">
<div className="card-header">Findings Over Time</div>
@ -158,14 +154,8 @@ export function OverviewPage() {
)}
</div>
<div className="card">
<div className="card-header">OWASP Top 10 (2021)</div>
{overview.owasp_buckets && overview.owasp_buckets.length > 0 ? (
<OwaspChart buckets={overview.owasp_buckets} />
) : (
<div className="empty-state" style={{ padding: 16 }}>
<p>No OWASP-mapped findings.</p>
</div>
)}
<div className="card-header">Issue Categories</div>
<HorizontalBarChart items={categoryItems} />
</div>
<div className="card">
<div className="card-header">Confidence Distribution</div>
@ -180,8 +170,14 @@ export function OverviewPage() {
)}
</div>
<div className="card">
<div className="card-header">Issue Categories</div>
<HorizontalBarChart items={categoryItems} />
<div className="card-header">OWASP Top 10 (2021)</div>
{overview.owasp_buckets && overview.owasp_buckets.length > 0 ? (
<OwaspChart buckets={overview.owasp_buckets} />
) : (
<div className="empty-state" style={{ padding: 16 }}>
<p>No OWASP-mapped findings.</p>
</div>
)}
</div>
</div>
@ -289,7 +285,7 @@ export function OverviewPage() {
)}
</div>
</div>
</>
</div>
);
}

View file

@ -271,20 +271,7 @@ export function RulesPage() {
if (error) return <ErrorState message={error.message} />;
return (
<>
<div className="page-header">
<h2>Rules</h2>
<span
style={{
color: 'var(--text-secondary)',
fontSize: 'var(--text-sm)',
marginLeft: 'var(--space-3)',
}}
>
{(rules || []).length} rules
</span>
</div>
<div className="rules-page page-shell">
<div className="rules-layout">
<div className="rules-list-panel">
<div className="rules-filters">
@ -310,14 +297,7 @@ export function RulesPage() {
</option>
))}
</select>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
fontSize: 'var(--text-sm)',
}}
>
<label className="rules-custom-toggle">
<input
type="checkbox"
checked={customOnly}
@ -357,6 +337,6 @@ export function RulesPage() {
)}
</div>
</div>
</>
</div>
);
}

View file

@ -221,7 +221,7 @@ function CompareByGroup({
}, [data, groupField]);
return (
<>
<div className="scan-compare-page page-shell">
{groups.map(([key, items]) => {
const counts = { new: 0, fixed: 0, changed: 0, unchanged: 0 };
items.forEach(
@ -267,7 +267,7 @@ function CompareByGroup({
</CollapsibleSection>
);
})}
</>
</div>
);
}
@ -300,16 +300,10 @@ export function ScanComparePage() {
return (
<>
<button
className="btn btn-sm"
style={{ marginBottom: 'var(--space-4)' }}
onClick={() => navigate('/scans')}
>
Back to Scans
</button>
<div className="page-header">
<h2>Scan Comparison</h2>
<div className="page-action-row">
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
Back to Scans
</button>
</div>
<div className="compare-header">

View file

@ -71,125 +71,127 @@ function SummaryTab({ scan }: { scan: ScanView }) {
</div>
</div>
<div className="card">
<div className="card-header">Details</div>
<table>
<tbody>
<tr>
<td style={{ color: 'var(--text-secondary)', width: 140 }}>
Scan ID
</td>
<td
style={{
fontFamily: 'var(--font-mono)',
fontSize: 'var(--text-xs)',
}}
>
{scan.id}
</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Root</td>
<td
style={{
fontFamily: 'var(--font-mono)',
fontSize: 'var(--text-sm)',
}}
>
{scan.scan_root}
</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Engine</td>
<td>{scan.engine_version || '-'}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Started</td>
<td>{fmtDate(scan.started_at)}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Finished</td>
<td>{fmtDate(scan.finished_at)}</td>
</tr>
{scan.error && (
<div className="scan-summary-grid">
<div className="card scan-detail-card">
<div className="card-header">Details</div>
<table>
<tbody>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Error</td>
<td style={{ color: 'var(--sev-high)' }}>{scan.error}</td>
<td style={{ color: 'var(--text-secondary)', width: 140 }}>
Scan ID
</td>
<td
style={{
fontFamily: 'var(--font-mono)',
fontSize: 'var(--text-xs)',
}}
>
{scan.id}
</td>
</tr>
)}
</tbody>
</table>
</div>
{timing && total > 0 && (
<div className="card" style={{ marginTop: 'var(--space-4)' }}>
<div className="card-header">Timing Breakdown</div>
<div className="timing-bar">
<div
className="timing-bar-segment walk"
style={{ width: `${pct(timing.walk_ms)}%` }}
title={`Walk: ${timing.walk_ms}ms`}
></div>
<div
className="timing-bar-segment pass1"
style={{ width: `${pct(timing.pass1_ms)}%` }}
title={`Pass 1: ${timing.pass1_ms}ms`}
></div>
<div
className="timing-bar-segment callgraph"
style={{ width: `${pct(timing.call_graph_ms)}%` }}
title={`Call Graph: ${timing.call_graph_ms}ms`}
></div>
<div
className="timing-bar-segment pass2"
style={{ width: `${pct(timing.pass2_ms)}%` }}
title={`Pass 2: ${timing.pass2_ms}ms`}
></div>
<div
className="timing-bar-segment postprocess"
style={{ width: `${pct(timing.post_process_ms)}%` }}
title={`Post-process: ${timing.post_process_ms}ms`}
></div>
</div>
<div className="timing-legend">
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--sev-low)' }}
></span>{' '}
Walk {timing.walk_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--accent)' }}
></span>{' '}
Pass 1 {timing.pass1_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--sev-medium)' }}
></span>{' '}
Call Graph {timing.call_graph_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--success)' }}
></span>{' '}
Pass 2 {timing.pass2_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--text-tertiary)' }}
></span>{' '}
Post {timing.post_process_ms}ms
</span>
</div>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Root</td>
<td
style={{
fontFamily: 'var(--font-mono)',
fontSize: 'var(--text-sm)',
}}
>
{scan.scan_root}
</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Engine</td>
<td>{scan.engine_version || '-'}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Started</td>
<td>{fmtDate(scan.started_at)}</td>
</tr>
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Finished</td>
<td>{fmtDate(scan.finished_at)}</td>
</tr>
{scan.error && (
<tr>
<td style={{ color: 'var(--text-secondary)' }}>Error</td>
<td style={{ color: 'var(--sev-high)' }}>{scan.error}</td>
</tr>
)}
</tbody>
</table>
</div>
)}
{timing && total > 0 && (
<div className="card scan-timing-card">
<div className="card-header">Timing Breakdown</div>
<div className="timing-bar">
<div
className="timing-bar-segment walk"
style={{ width: `${pct(timing.walk_ms)}%` }}
title={`Walk: ${timing.walk_ms}ms`}
></div>
<div
className="timing-bar-segment pass1"
style={{ width: `${pct(timing.pass1_ms)}%` }}
title={`Pass 1: ${timing.pass1_ms}ms`}
></div>
<div
className="timing-bar-segment callgraph"
style={{ width: `${pct(timing.call_graph_ms)}%` }}
title={`Call Graph: ${timing.call_graph_ms}ms`}
></div>
<div
className="timing-bar-segment pass2"
style={{ width: `${pct(timing.pass2_ms)}%` }}
title={`Pass 2: ${timing.pass2_ms}ms`}
></div>
<div
className="timing-bar-segment postprocess"
style={{ width: `${pct(timing.post_process_ms)}%` }}
title={`Post-process: ${timing.post_process_ms}ms`}
></div>
</div>
<div className="timing-legend">
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--sev-low)' }}
></span>{' '}
Walk {timing.walk_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--accent)' }}
></span>{' '}
Pass 1 {timing.pass1_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--sev-medium)' }}
></span>{' '}
Call Graph {timing.call_graph_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--success)' }}
></span>{' '}
Pass 2 {timing.pass2_ms}ms
</span>
<span className="timing-legend-item">
<span
className="timing-legend-dot"
style={{ background: 'var(--text-tertiary)' }}
></span>{' '}
Post {timing.post_process_ms}ms
</span>
</div>
</div>
)}
</div>
</>
);
}
@ -212,7 +214,7 @@ function FindingsTab({ scanId }: { scanId: string }) {
}
return (
<>
<div className="scan-detail-page page-shell">
<div className="table-wrap">
<table>
<thead>
@ -266,7 +268,7 @@ function FindingsTab({ scanId }: { scanId: string }) {
>
Showing {data.findings.length} of {data.total} findings
</div>
</>
</div>
);
}
@ -416,31 +418,19 @@ export function ScanDetailPage() {
return (
<>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--space-2)',
marginBottom: 'var(--space-4)',
}}
>
<div className="page-action-row">
<button className="btn btn-sm" onClick={() => navigate('/scans')}>
Back to Scans
</button>
{prevScanId && (
<button
className="btn btn-sm"
style={{ marginLeft: 'auto' }}
className="btn btn-sm page-action-push"
onClick={() => navigate(`/scans/compare/${prevScanId}/${id}`)}
>
Compare with Previous
</button>
)}
</div>
<div className="page-header">
<h2>Scan Detail</h2>
<span className={`status-badge ${scan.status}`}>
<span className={`status-badge ${scan.status}`} style={{ marginLeft: 'auto' }}>
<span className={`status-dot ${scan.status}`}></span>
{scan.status}
</span>

View file

@ -174,11 +174,7 @@ export function ScansPage() {
const showCheckboxes = completedScans.length >= 2;
return (
<>
<div className="page-header">
<h2>Scans</h2>
</div>
<div className="scans-page page-shell">
{(runningScans.length > 0 || isScanRunning) && scanProgress && (
<ScanProgress data={scanProgress} />
)}
@ -210,90 +206,103 @@ export function ScansPage() {
</div>
) : (
<div className="table-wrap">
<table>
<table className="scans-table">
<thead>
<tr>
{showCheckboxes && <th style={{ width: 32 }}></th>}
<th>Status</th>
<th>Root</th>
<th>Duration</th>
<th>Findings</th>
<th>Languages</th>
<th>Started</th>
<th style={{ width: 60 }}></th>
{showCheckboxes && <th className="scan-select-col"></th>}
<th className="scan-status-col">Status</th>
<th className="scan-root-col">Root</th>
<th className="scan-duration-col">Duration</th>
<th className="scan-findings-col">Findings</th>
<th className="scan-languages-col">Languages</th>
<th className="scan-started-col">Started</th>
<th className="scan-actions-col"></th>
</tr>
</thead>
<tbody>
{scans.map((s: ScanView) => (
<tr
key={s.id}
className="clickable"
onClick={() => navigate(`/scans/${s.id}`)}
>
{showCheckboxes && (
{scans.map((s: ScanView) => {
const languages = s.languages || [];
const visibleLanguages = languages.slice(0, 4);
const hiddenLanguageCount =
languages.length - visibleLanguages.length;
return (
<tr
key={s.id}
className="clickable"
onClick={() => navigate(`/scans/${s.id}`)}
>
{showCheckboxes && (
<td>
{s.status === 'completed' && (
<input
type="checkbox"
className="scan-compare-cb"
checked={selectedScans.has(s.id)}
onClick={(e) => handleCheckbox(e, s.id)}
onChange={() => {}}
/>
)}
</td>
)}
<td>
{s.status === 'completed' && (
<input
type="checkbox"
className="scan-compare-cb"
checked={selectedScans.has(s.id)}
onClick={(e) => handleCheckbox(e, s.id)}
onChange={() => {}}
/>
<span className={`status-badge ${s.status}`}>
<span className={`status-dot ${s.status}`}></span>
{s.status}
</span>
</td>
<td className="scan-root-cell" title={s.scan_root}>
{truncPath(s.scan_root)}
</td>
<td className="scan-number-cell">
{s.duration_secs != null
? s.duration_secs.toFixed(2) + 's'
: '-'}
</td>
<td className="scan-number-cell">
{s.finding_count ?? '-'}
</td>
<td className="scan-languages-cell">
{languages.length > 0 ? (
<span className="scan-language-list">
{visibleLanguages.map((l) => (
<span key={l} className="lang-badge">
{l}
</span>
))}
{hiddenLanguageCount > 0 && (
<span className="lang-badge lang-badge-more">
+{hiddenLanguageCount}
</span>
)}
</span>
) : (
'-'
)}
</td>
)}
<td>
<span className={`status-badge ${s.status}`}>
<span className={`status-dot ${s.status}`}></span>
{s.status}
</span>
</td>
<td
style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.82rem',
}}
>
{truncPath(s.scan_root)}
</td>
<td>
{s.duration_secs != null
? s.duration_secs.toFixed(2) + 's'
: '-'}
</td>
<td>{s.finding_count ?? '-'}</td>
<td>
{(s.languages || []).length > 0
? (s.languages || []).map((l) => (
<span key={l} className="lang-badge">
{l}
</span>
))
: '-'}
</td>
<td>{relTime(s.started_at)}</td>
<td>
{s.status !== 'running' && (
<button
className="btn btn-sm btn-danger"
onClick={(e) => {
e.stopPropagation();
if (confirm('Delete this scan?')) {
deleteScan.mutate(s.id);
}
}}
>
Delete
</button>
)}
</td>
</tr>
))}
<td>{relTime(s.started_at)}</td>
<td className="scan-actions-cell">
{s.status !== 'running' && (
<button
className="btn btn-sm btn-danger"
onClick={(e) => {
e.stopPropagation();
if (confirm('Delete this scan?')) {
deleteScan.mutate(s.id);
}
}}
>
Delete
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</>
</div>
);
}

View file

@ -130,6 +130,22 @@ function TriageSummary({
<div className="triage-hero">
<div className="triage-hero-row">
<h1 className="triage-hero-title">{headline}</h1>
{showSeverity && totalCount > 0 && (
<div className="triage-hero-severity">
{SEVERITIES.map((sev) => (
<span
key={sev}
className={`triage-sev-stat triage-sev-${sev.toLowerCase()}`}
>
<span className="triage-sev-dot" aria-hidden />
<span className="triage-sev-count">
{(openBySev[sev] ?? 0).toLocaleString()}
</span>
<span className="triage-sev-name">{sev}</span>
</span>
))}
</div>
)}
{totalCount > 0 && (
<button
type="button"
@ -142,22 +158,6 @@ function TriageSummary({
</button>
)}
</div>
{showSeverity && totalCount > 0 && (
<div className="triage-hero-severity">
{SEVERITIES.map((sev) => (
<span
key={sev}
className={`triage-sev-stat triage-sev-${sev.toLowerCase()}`}
>
<span className="triage-sev-dot" aria-hidden />
<span className="triage-sev-count">
{(openBySev[sev] ?? 0).toLocaleString()}
</span>
<span className="triage-sev-name">{sev}</span>
</span>
))}
</div>
)}
{expanded && (
<div className="triage-state-row">
<button
@ -1124,7 +1124,7 @@ export function TriagePage() {
GROUP_MODES.find((g) => g.value === groupMode)?.label ?? 'None';
return (
<div className="triage-page">
<div className="triage-page page-shell">
<TriageSummary
totalCount={totalCount}
needsAttention={needsAttention}
@ -1260,7 +1260,7 @@ export function TriagePage() {
<input
className="triage-search"
type="search"
placeholder="Search rule, file, message"
placeholder="Search rule, file, message..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>

View file

@ -85,7 +85,7 @@ export function FunctionSelector({
}
function formatFunctionLabel(fn: FunctionInfo): string {
const sig = `(${fn.param_count} params) L${fn.line}`;
const sig = `(${fn.param_count} params), L${fn.line}`;
if (fn.func_kind === 'closure' && fn.container) {
return `${fn.name} [closure in ${fn.container}] ${sig}`;
}

View file

@ -63,7 +63,7 @@ export function TypeFactsAnalysisPanel({
{securityFacts.length > 0 && (
<TypeFactGroup
title="Security-Relevant Types"
subtitle="HttpClient, DatabaseConnection, Url, … — drive type-qualified callee resolution and sink suppression"
subtitle="HttpClient, DatabaseConnection, Url, and related types drive type-qualified callee resolution and sink suppression"
facts={securityFacts}
highlight
/>