Skip to content
Empty file.
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface PluginFieldSchema {
help?: string
options?: PluginFieldOption[]
validation?: Record<string, unknown>
sensitive?: boolean
}
export interface PluginAvailability {
runnable: boolean
Expand Down
158 changes: 158 additions & 0 deletions frontend/src/components/CommandPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { useCallback, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import type { PluginFieldSchema } from '../api'
import {
buildCommandTokens,
getMissingFields,
tokensToString,
type PreviewToken,
} from '../utils/commandPreview'

interface CommandPreviewProps {
commandTemplate: string[]
fields: PluginFieldSchema[]
inputs: Record<string, unknown>
}

function TokenChip({ token }: { token: PreviewToken }) {
const styles: Record<PreviewToken['kind'], string> = {
command: 'text-rag-green font-black',
flag: 'text-rag-blue',
value: 'text-silver-bright',
redacted: 'text-rag-amber font-mono bg-rag-amber/10 px-1 rounded',
missing: 'text-rag-red font-mono bg-rag-red/10 px-1 rounded animate-pulse',
placeholder: 'text-silver/40 italic',
}

return (
<span className={`inline-block whitespace-nowrap ${styles[token.kind]}`}>
{token.text}
</span>
)
}

export default function CommandPreview({ commandTemplate, fields, inputs }: CommandPreviewProps) {
const [copied, setCopied] = useState(false)
const [expanded, setExpanded] = useState(true)

const tokens = buildCommandTokens(commandTemplate, fields, inputs)
const missingFields = getMissingFields(fields, inputs)
const plainText = tokensToString(tokens)

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(plainText)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {
// clipboard not available
}
}, [plainText])

return (
<section
className="bg-charcoal border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] overflow-hidden"
aria-label="Command preview"
>
{/* ── Header bar ────────────────────────────────────────────────────── */}
<div className="flex items-center justify-between px-6 py-4 border-b-4 border-black bg-charcoal-dark">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-rag-blue text-lg">terminal</span>
<span className="text-[10px] font-black uppercase tracking-[0.4em] text-silver-bright italic">
Cmd_Preview
</span>
<span className="text-[9px] px-2 py-0.5 border-2 border-silver/20 text-silver/40 uppercase tracking-widest font-black">
sanitized · local
</span>
</div>

<div className="flex items-center gap-2">
<button
onClick={handleCopy}
title="Copy sanitized command"
className="flex items-center gap-1.5 px-3 py-1.5 border-2 border-black text-[9px] font-black uppercase tracking-widest text-silver hover:bg-rag-blue hover:text-black transition-all shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
>
<span className="material-symbols-outlined text-sm">
{copied ? 'check' : 'content_copy'}
</span>
{copied ? 'Copied' : 'Copy'}
</button>
<button
onClick={() => setExpanded((v) => !v)}
title={expanded ? 'Collapse preview' : 'Expand preview'}
className="w-8 h-8 flex items-center justify-center border-2 border-black text-silver hover:bg-charcoal-light transition-all"
>
<span className="material-symbols-outlined text-sm">
{expanded ? 'expand_less' : 'expand_more'}
</span>
</button>
</div>
</div>

{/* ── Token display ─────────────────────────────────────────────────── */}
<AnimatePresence initial={false}>
{expanded && (
<motion.div
key="preview-body"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="px-6 py-5">
<pre
className="font-mono text-xs leading-relaxed whitespace-pre-wrap break-all flex flex-wrap gap-x-1.5 gap-y-1"
aria-label={`Preview command: ${plainText}`}
>
{tokens.map((token, i) => (
<TokenChip key={i} token={token} />
))}
</pre>
</div>

{/* ── Legend ──────────────────────────────────────────────────── */}
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 px-6 pb-4 border-t-2 border-black/40 pt-3">
<LegendItem color="text-rag-green" label="binary" />
<LegendItem color="text-rag-blue" label="flag" />
<LegendItem color="text-silver-bright" label="value" />
<LegendItem color="text-rag-amber" label="redacted secret" />
<LegendItem color="text-rag-red" label="missing required" />
</div>

{/* ── Missing fields warning ───────────────────────────────────── */}
{missingFields.length > 0 && (
<div className="mx-6 mb-5 border-2 border-rag-red/60 bg-rag-red/5 px-4 py-3 flex items-start gap-3">
<span className="material-symbols-outlined text-rag-red text-base mt-0.5 shrink-0">
warning
</span>
<div>
<p className="text-[9px] font-black uppercase tracking-widest text-rag-red mb-1">
Missing required fields
</p>
<p className="text-[9px] text-silver/60 uppercase tracking-widest">
{missingFields.map((f) => f.label).join(', ')}
</p>
</div>
</div>
)}

{/* ── Disclaimer ──────────────────────────────────────────────── */}
<p className="px-6 pb-4 text-[9px] text-silver/25 uppercase tracking-widest leading-relaxed">
⚠ This preview is locally generated and sanitized. Runtime normalisation may alter
the final command. Secrets and credentials are always redacted here.
</p>
</motion.div>
)}
</AnimatePresence>
</section>
)
}

function LegendItem({ color, label }: { color: string; label: string }) {
return (
<span className="flex items-center gap-1.5">
<span className={`text-[10px] font-mono font-black ${color}`}>■</span>
<span className="text-[9px] uppercase tracking-widest text-silver/40">{label}</span>
</span>
)
}
78 changes: 46 additions & 32 deletions frontend/src/pages/Reports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ type Report = {
pages: number
}

type ReportStatus = 'all' | 'ready' | 'generating' | 'failed'

const containerVariants = {
hidden: { opacity: 0 },
visible: {
Expand Down Expand Up @@ -69,8 +67,8 @@ export default function Reports() {
const [reports, setReports] = useState<Report[]>([])
const [summary, setSummary] = useState<any>({ total_findings: 0, total_assets: 0, critical_findings: 0, high_findings: 0, total_attack_surface: 0 })
const [selectedType, setSelectedType] = useState('all')
const [selectedStatus, setSelectedStatus] = useState<ReportStatus>('all')
const [selectedDateRange, setSelectedDateRange] = useState<DateRange>('all')
const [selectedStatus, setSelectedStatus] = useState('all')
const [selectedDate, setSelectedDate] = useState('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { preferred, savePreference } = usePreferredExportFormat()
Expand All @@ -95,11 +93,21 @@ export default function Reports() {
fetchReports()
}, [])

const filteredReports = reports.filter((report) =>
(selectedType === 'all' || report.type === selectedType) &&
(selectedStatus === 'all' || report.status === selectedStatus) &&
isWithinDateRange(report.generated_at, selectedDateRange)
)
const filteredReports = reports.filter((report) => {
if (selectedType !== 'all' && report.type !== selectedType) return false
if (selectedStatus !== 'all' && report.status !== selectedStatus) return false
if (selectedDate === '24h') {
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000)
if (new Date(report.generated_at) < cutoff) return false
} else if (selectedDate === '7d') {
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
if (new Date(report.generated_at) < cutoff) return false
} else if (selectedDate === '30d') {
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
if (new Date(report.generated_at) < cutoff) return false
}
return true
})

return (
<div className="min-h-screen bg-charcoal-dark text-silver p-6 md:p-12 space-y-12">
Expand Down Expand Up @@ -199,7 +207,7 @@ export default function Reports() {
}`}
>
{t} BRIEFINGS
{selectedType === t && <ReportIcon icon={Radar02Icon} size={16} className="text-black" aria-hidden="true" />}
{selectedType === t && <ReportIcon icon={Radar02Icon} size={16} className="text-black" />}
</button>
))}
</div>
Expand All @@ -209,19 +217,19 @@ export default function Reports() {
<div className="space-y-4">
<label className="text-[10px] font-black text-silver-bright uppercase tracking-[0.2em] italic">Status_Filter</label>
<div className="grid grid-cols-1 gap-2">
{([
{ value: 'all', label: 'All Statuses' },
{ value: 'ready', label: 'Ready' },
{[
{ value: 'all', label: 'All Statuses' },
{ value: 'ready', label: 'Ready' },
{ value: 'failed', label: 'Failed' },
{ value: 'generating', label: 'Generating' },
{ value: 'failed', label: 'Failed' },
] as const).map(({ value, label }) => (
].map(({ value, label }) => (
<button
key={value}
onClick={() => setSelectedStatus(value)}
aria-label={`status ${label}`}
onClick={() => setSelectedStatus(value)}
className={`px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest border-4 transition-all flex justify-between items-center ${
selectedStatus === value
? 'bg-rag-amber border-black text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]'
? 'bg-rag-red border-black text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]'
: 'bg-charcoal-dark border-black text-silver/40 hover:border-silver-bright/20'
}`}
>
Expand All @@ -236,29 +244,30 @@ export default function Reports() {
<div className="space-y-4">
<label className="text-[10px] font-black text-silver-bright uppercase tracking-[0.2em] italic">Date_Range</label>
<div className="grid grid-cols-1 gap-2">
{([
{[
{ value: 'all', label: 'All Time' },
{ value: '24h', label: 'Last 24 Hours' },
{ value: '7d', label: 'Last 7 Days' },
{ value: '7d', label: 'Last 7 Days' },
{ value: '30d', label: 'Last 30 Days' },
] as const).map(({ value, label }) => (
].map(({ value, label }) => (
<button
key={value}
onClick={() => setSelectedDateRange(value)}
aria-label={`date ${label}`}
onClick={() => setSelectedDate(value)}
className={`px-6 py-4 text-left text-[10px] font-black uppercase tracking-widest border-4 transition-all flex justify-between items-center ${
selectedDateRange === value
? 'bg-rag-blue border-black text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]'
selectedDate === value
? 'bg-rag-red border-black text-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]'
: 'bg-charcoal-dark border-black text-silver/40 hover:border-silver-bright/20'
}`}
>
{label}
{selectedDateRange === value && <ReportIcon icon={Radar02Icon} size={16} className="text-black" />}
{selectedDate === value && <ReportIcon icon={Radar02Icon} size={16} className="text-black" />}
</button>
))}
</div>
</div>

{/* Integrity Block */}
<div className="p-8 border-4 border-black border-dashed space-y-4 bg-charcoal-dark/50">
<div className="flex items-center gap-3">
<ReportIcon icon={KnightShieldIcon} className="text-rag-green" aria-hidden="true" />
Expand All @@ -268,6 +277,7 @@ export default function Reports() {
Dossiers are cryptographically hashed and recorded. Modifications are strictly detectable by the Enclave audit daemon.
</p>
</div>

</section>
</aside>

Expand Down Expand Up @@ -384,14 +394,18 @@ export default function Reports() {
))}

{filteredReports.length === 0 && (
<div className="col-span-2 py-40 border-4 border-dashed border-black/5 text-center flex flex-col items-center gap-8 bg-charcoal/30">
<ReportIcon icon={Archive02Icon} size={120} className="text-silver/5" aria-hidden="true" />
<div className="space-y-2">
<p className="text-xl font-black text-silver/20 uppercase tracking-[0.4em] italic">Archive Isolated</p>
<p className="text-xs font-mono text-silver/10 uppercase tracking-widest leading-relaxed">No entries match the selected filters</p>
</div>
</div>
)}
<div className="col-span-2 py-40 border-4 border-dashed border-black/5 text-center flex flex-col items-center gap-8 bg-charcoal/30">
<ReportIcon icon={Archive02Icon} size={120} className="text-silver/5" />
<div className="space-y-2">
<p className="text-xl font-black text-silver/20 uppercase tracking-[0.4em] italic">Archive Isolated</p>
<p className="text-xs font-mono text-silver/10 uppercase tracking-widest leading-relaxed">
{selectedStatus !== 'all' || selectedDate !== 'all'
? 'No entries match the selected filters'
: 'System buffer awaiting briefing generation protocols'}
</p>
</div>
</div>
)}
</motion.div>
</AnimatePresence>
</main>
Expand Down
Loading
Loading