Fix remaining Dutch string in DataQualityView

This commit is contained in:
mo
2026-06-25 10:00:07 +00:00
parent 41a8a3b18e
commit ff2c31a6b5
10 changed files with 223 additions and 88 deletions
+15 -15
View File
@@ -105,9 +105,9 @@ AGENTS = [
"motto": "Guardian of every row", "motto": "Guardian of every row",
"capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"], "capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"],
"suggested_prompts": [ "suggested_prompts": [
"Hoeveel data zit er in de databases?", "How much data is in the databases?",
"Wat staat er in PostgreSQL sales_orders?", "What is in PostgreSQL sales_orders?",
"MongoDB supplychain overzicht", "MongoDB supplychain overview",
], ],
}, },
{ {
@@ -332,21 +332,21 @@ async def ask_llm(
log: Any | None = None, log: Any | None = None,
) -> str | None: ) -> str | None:
agent = next(a for a in AGENTS if a["id"] == agent_id) agent = next(a for a in AGENTS if a["id"] == agent_id)
system = f"""Je bent {agent['name']}, een autonomous ops agent in het Dell ATC data lab. system = f"""You are {agent['name']}, an autonomous ops agent in the Dell ATC data lab.
Specialisatie: {agent['role']}. Specialization: {agent['role']}.
Motto: {agent.get('motto', '')} Motto: {agent.get('motto', '')}.
Je antwoordt namens je domein maar hebt zicht op de HELE lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, en GPU/vLLM. You respond on behalf of your domain but have visibility into the FULL lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, and GPU/vLLM.
Regels: Rules:
- Antwoord in dezelfde taal als de gebruiker (Nederlands of Engels). - Always respond in English.
- Je hebt volledige zicht op de HELE cluster: alle VMs, zones, connectors, GPU, Hadoop, ObjectScale en Command Center. - You have full visibility into the entire cluster: all VMs, zones, connectors, GPU, Hadoop, ObjectScale and Command Center.
- Gebruik ALLEEN de live data hieronder — verzin geen hosts, poorten, cijfers of connector namen. - Use ONLY the live data below — do not invent hosts, ports, numbers or connector names.
- Gebruik exact de container/connector namen uit de data (bijv. mysql-hr-connector, niet "Debezium"). - Use exact container/connector names from the data (e.g. mysql-hr-connector, not "Debezium").
- Als iets DOWN of 0 GB is, zeg dat eerlijk. - If something is DOWN or 0 GB, say so honestly.
- Kort en behulpzaam (max ~10 zinnen); bullet lists mogen als het overzicht helpt. - Be concise and helpful (max ~10 sentences); bullet lists are fine when they aid clarity.
--- LIVE LAB DATA (primary domain eerst, daarna volledige stack) --- --- LIVE LAB DATA (primary domain first, then full stack) ---
{context} {context}
""" """
if log: if log:
@@ -32,7 +32,7 @@ export function ActivityStream({ feed, agents, filterAgentId, opsOnly }: Props)
<div className="scrollbar-thin flex-1 overflow-y-auto px-2 pb-2"> <div className="scrollbar-thin flex-1 overflow-y-auto px-2 pb-2">
{items.length === 0 && ( {items.length === 0 && (
<p className="py-8 text-center text-[11px] text-foreground-faint"> <p className="py-8 text-center text-[11px] text-foreground-faint">
{opsOnly ? 'Geen operationele events — antwords staan in Chat.' : 'No activity yet — agents are on standby.'} {opsOnly ? 'No operational events — responses appear in Chat.' : 'No activity yet — agents are on standby.'}
</p> </p>
)} )}
{items.map((e) => { {items.map((e) => {
+1 -1
View File
@@ -53,7 +53,7 @@ export function ChatDrawer({
Chat & Activity Chat & Activity
{unread > 0 && ( {unread > 0 && (
<span className="rounded-full bg-docker/20 px-2 py-0.5 font-mono text-[10px] text-docker"> <span className="rounded-full bg-docker/20 px-2 py-0.5 font-mono text-[10px] text-docker">
{unread} bericht{unread !== 1 ? 'en' : ''} {unread} message{unread !== 1 ? 's' : ''}
</span> </span>
)} )}
{approvals.length > 0 && ( {approvals.length > 0 && (
+3 -3
View File
@@ -21,9 +21,9 @@ export function CommandBar({ busy, selectedAgent, onSubmit }: Props) {
} }
const suggestions = selectedAgent?.suggested_prompts?.slice(0, 3) || [ const suggestions = selectedAgent?.suggested_prompts?.slice(0, 3) || [
'Hoeveel data zit er in de databases?', 'How much data is in the databases?',
'Wat staat er in PostgreSQL?', 'What is in PostgreSQL?',
'MongoDB supplychain overzicht', 'MongoDB supplychain overview',
] ]
return ( return (
+1 -1
View File
@@ -58,7 +58,7 @@ export function CommsPanel({ messages, agents, selectedAgent, busy }: Props) {
{busy && ( {busy && (
<div className="flex items-center gap-2 px-2 py-2 text-[10px] text-foreground-muted"> <div className="flex items-center gap-2 px-2 py-2 text-[10px] text-foreground-muted">
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-accent" /> <span className="h-1.5 w-1.5 animate-bounce rounded-full bg-accent" />
<span>Agent verzamelt cluster-data en vraagt Llama 70B verwacht ~3090 sec</span> <span>Agent gathering cluster data and querying Llama 70B expect ~3090 sec</span>
</div> </div>
)} )}
<div ref={bottomRef} /> <div ref={bottomRef} />
+191 -56
View File
@@ -37,8 +37,36 @@ type ColumnProfile = {
top_values?: { value: string; count: number }[] top_values?: { value: string; count: number }[]
} }
type GxCheck = { suite: string; expectation: string; success: boolean; result: string; column?: string } type Violations = {
type SodaCheck = { suite: string; name: string; check: string; outcome: string; detail: string } violation_count?: number
violation_pct?: number
sample_rows?: { row_index: number; values: Record<string, unknown> }[]
location_hint?: string
affected_columns?: string[]
}
type DqCheck = {
id?: string
suite: string
success?: boolean
status?: string
outcome?: string
expectation?: string
name?: string
metric?: string
check?: string
test?: string
rule?: string
monitor?: string
result?: string
detail?: string
column?: string
violations?: Violations
meta?: Record<string, unknown>
}
type GxCheck = DqCheck & { expectation: string; success: boolean; result: string }
type SodaCheck = DqCheck & { name: string; check: string; outcome: string; detail: string }
type DocStructure = { type DocStructure = {
pages: number pages: number
@@ -67,10 +95,24 @@ type AssessResult = {
dimensions: Dimension[] dimensions: Dimension[]
column_profiles: ColumnProfile[] column_profiles: ColumnProfile[]
action_items: { priority: string; dimension: string; score: number; action: string }[] action_items: { priority: string; dimension: string; score: number; action: string }[]
checks?: { great_expectations: GxCheck[]; soda_core: SodaCheck[] } checks?: {
checks_summary: { great_expectations: GxCheck[]
great_expectations: { total: number; passed: number } soda_core: SodaCheck[]
soda_core: { total: number; warnings: number } deequ?: DqCheck[]
pandera?: DqCheck[]
dbt_expectations?: DqCheck[]
monte_carlo?: DqCheck[]
affirm?: DqCheck[]
}
checks_summary: Record<string, { total: number; passed?: number; warnings?: number }>
executive_summary?: {
headline: string
dataset: string
checks_run: number
issues_found: number
flagged_columns: number
tools: string[]
recommendation: string
} }
docling?: { used: boolean; parse_id?: string; document_structure?: DocStructure; stats?: Record<string, number>; images?: DocImage[] } docling?: { used: boolean; parse_id?: string; document_structure?: DocStructure; stats?: Record<string, number>; images?: DocImage[] }
rag_ingest?: { ok: boolean; duplicate?: boolean; chunks?: number; message?: string; error?: string } rag_ingest?: { ok: boolean; duplicate?: boolean; chunks?: number; message?: string; error?: string }
@@ -140,8 +182,9 @@ export function DataQualityView() {
const [reports, setReports] = useState<ReportSummary[]>([]) const [reports, setReports] = useState<ReportSummary[]>([])
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [expandedCol, setExpandedCol] = useState<string | null>(null) const [expandedCol, setExpandedCol] = useState<string | null>(null)
const [showGx, setShowGx] = useState(false) const [expandedCheck, setExpandedCheck] = useState<string | null>(null)
const [showSoda, setShowSoda] = useState(false) const [expandedDim, setExpandedDim] = useState<string | null>(null)
const [activeTool, setActiveTool] = useState<string>('great_expectations')
const loadMeta = useCallback(async () => { const loadMeta = useCallback(async () => {
try { try {
@@ -240,7 +283,7 @@ export function DataQualityView() {
{caps && ( {caps && (
<div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-4"> <div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
<CapCard title="Maturity Engine" items={caps.maturity_dimensions.map((d) => d.label)} icon={Layers} /> <CapCard title="Maturity Engine" items={caps.maturity_dimensions.map((d) => d.label)} icon={Layers} />
<CapCard title="Data Quality Tools" items={['Great Expectations', 'Soda Core', 'Pandas Profiling']} icon={CheckCircle2} /> <CapCard title="Data Quality Tools" items={['Great Expectations', 'Soda Core', 'Deequ', 'Pandera', 'dbt', 'Monte Carlo', 'Affirm', 'Pandas']} icon={CheckCircle2} />
<CapCard title="Document Parsing" items={caps.tools.docling?.capabilities || ['PDF', 'PPTX', 'DOCX']} icon={FileText} /> <CapCard title="Document Parsing" items={caps.tools.docling?.capabilities || ['PDF', 'PPTX', 'DOCX']} icon={FileText} />
<CapCard title="File formats" items={[...caps.supported_data_formats.slice(0, 4), ...caps.supported_document_formats.slice(0, 3)]} icon={Upload} /> <CapCard title="File formats" items={[...caps.supported_data_formats.slice(0, 4), ...caps.supported_document_formats.slice(0, 3)]} icon={Upload} />
</div> </div>
@@ -287,8 +330,8 @@ export function DataQualityView() {
<StatCard label="Overall Score" value={`${assess.overall_score}`} sub="/100" accent /> <StatCard label="Overall Score" value={`${assess.overall_score}`} sub="/100" accent />
<StatCard label="Maturity Level" value={assess.maturity_level} sub={assess.maturity_description} /> <StatCard label="Maturity Level" value={assess.maturity_level} sub={assess.maturity_description} />
<StatCard label="Dataset" value={`${assess.rows.toLocaleString()}`} sub={`${assess.columns} columns`} /> <StatCard label="Dataset" value={`${assess.rows.toLocaleString()}`} sub={`${assess.columns} columns`} />
<StatCard label="Great Expectations" value={`${assess.checks_summary.great_expectations.passed}/${assess.checks_summary.great_expectations.total}`} sub="checks passed" /> <StatCard label="DQ Checks" value={String(assess.executive_summary?.checks_run || Object.values(assess.checks_summary).reduce((s, x) => s + (x.total || 0), 0))} sub="across 7 tools" />
<StatCard label="Soda Core" value={String(assess.checks_summary.soda_core.warnings)} sub="warnings" warn={assess.checks_summary.soda_core.warnings > 0} /> <StatCard label="Issues" value={String(assess.executive_summary?.issues_found ?? 0)} sub="failed/warn checks" warn={(assess.executive_summary?.issues_found ?? 0) > 0} />
</div> </div>
{assess.docling?.used && assess.docling.document_structure && ( {assess.docling?.used && assess.docling.document_structure && (
@@ -316,38 +359,69 @@ export function DataQualityView() {
</div> </div>
)} )}
{assess.executive_summary && (
<section className="rounded-lg border border-docker/30 bg-docker/5 p-4">
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-docker">Executive Summary Customer Report</h3>
<p className="text-[13px] font-semibold text-foreground">{assess.executive_summary.headline}</p>
<p className="mt-1 text-[11px] text-foreground-muted">{assess.executive_summary.dataset}</p>
<div className="mt-2 flex flex-wrap gap-3 text-[10px]">
<span>{assess.executive_summary.checks_run} checks run</span>
<span className={assess.executive_summary.issues_found > 0 ? 'text-warning' : 'text-success'}>{assess.executive_summary.issues_found} issues found</span>
<span>{assess.executive_summary.flagged_columns} flagged columns</span>
</div>
<p className="mt-2 border-t border-border/50 pt-2 text-[11px] text-foreground-muted">
<strong className="text-foreground">Recommendation:</strong> {assess.executive_summary.recommendation}
</p>
</section>
)}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<a href={assess.report_url} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px] font-medium', subTabActive)}> <a href={assess.report_url} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px] font-medium', subTabActive)}>
Full HTML report Full HTML report
</a> </a>
<button type="button" onClick={() => setShowGx(!showGx)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showGx ? subTabActive : subTabIdle)}> <a href={assess.report_url + '/json'} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px]', subTabIdle)}>
GE checks ({assess.checks?.great_expectations.length || 0}) JSON export
</button> </a>
<button type="button" onClick={() => setShowSoda(!showSoda)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showSoda ? subTabActive : subTabIdle)}>
Soda checks ({assess.checks?.soda_core.length || 0})
</button>
</div> </div>
{showGx && assess.checks?.great_expectations && ( <section>
<CheckTable title="Great Expectations" rows={assess.checks.great_expectations.map((c) => ({ <h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">Data Quality Tools click any row for error details</h3>
name: c.column ? `${c.expectation} [${c.column}]` : c.expectation, <div className="mb-2 flex flex-wrap gap-1">
status: c.success ? 'pass' : 'fail', {([
detail: c.result, ['great_expectations', 'Great Expectations'],
}))} /> ['soda_core', 'Soda Core'],
)} ['deequ', 'AWS Deequ'],
{showSoda && assess.checks?.soda_core && ( ['pandera', 'Pandera'],
<CheckTable title="Soda Core" rows={assess.checks.soda_core.map((c) => ({ ['dbt_expectations', 'dbt Expectations'],
name: c.name, ['monte_carlo', 'Monte Carlo'],
status: c.outcome, ['affirm', 'Affirm'],
detail: `${c.check}${c.detail}`, ] as const).map(([key, label]) => {
}))} /> const items = assess.checks?.[key] || []
)} if (!items.length) return null
const summary = assess.checks_summary?.[key]
const badge = summary?.passed !== undefined ? `${summary.passed}/${summary.total}` : `${items.length}`
return (
<button key={key} type="button" onClick={() => setActiveTool(key)} className={cn('rounded-md px-2.5 py-1 text-[10px]', activeTool === key ? subTabActive : subTabIdle)}>
{label} ({badge})
</button>
)
})}
</div>
{assess.checks && (
<CheckTable
title={activeTool.replace(/_/g, ' ')}
checks={normalizeChecks(assess.checks[activeTool as keyof typeof assess.checks] || [])}
expandedId={expandedCheck}
onToggle={setExpandedCheck}
/>
)}
</section>
<section> <section>
<h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">6 Maturity Dimensions</h3> <h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">6 Maturity Dimensions</h3>
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-3"> <div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-3">
{assess.dimensions.map((d) => ( {assess.dimensions.map((d) => (
<DimensionCard key={d.id} dimension={d} /> <DimensionCard key={d.id} dimension={d} expanded={expandedDim === d.id} onToggle={() => setExpandedDim(expandedDim === d.id ? null : d.id)} />
))} ))}
</div> </div>
</section> </section>
@@ -383,7 +457,7 @@ export function DataQualityView() {
<div className="space-y-5"> <div className="space-y-5">
<p className="text-[12px] leading-relaxed text-foreground-muted"> <p className="text-[12px] leading-relaxed text-foreground-muted">
Docling extracts text, tables, images and document structure from PDF, PowerPoint, Word, Excel and images. Docling extracts text, tables, images and document structure from PDF, PowerPoint, Word, Excel and images.
Resultaat: Markdown, HTML, JSON met pagina&apos;s, plaatjes, tabellen en outline. Output: Markdown, HTML, JSON with pages, images, tables and document outline.
</p> </p>
<UploadZone <UploadZone
loading={loading} loading={loading}
@@ -398,8 +472,8 @@ export function DataQualityView() {
{parse && ( {parse && (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">
<StatCard label="Bestand" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} /> <StatCard label="File" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} />
<StatCard label="Verwerking" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} /> <StatCard label="Processing" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} />
<StatCard label="Pages" value={String(parse.document_structure?.pages ?? parse.stats.pages ?? 0)} icon={Layers} /> <StatCard label="Pages" value={String(parse.document_structure?.pages ?? parse.stats.pages ?? 0)} icon={Layers} />
<StatCard label="Images" value={String(parse.document_structure?.pictures ?? 0)} icon={Image} accent /> <StatCard label="Images" value={String(parse.document_structure?.pictures ?? 0)} icon={Image} accent />
<StatCard label="Tables" value={String(parse.document_structure?.tables ?? 0)} icon={Table2} /> <StatCard label="Tables" value={String(parse.document_structure?.tables ?? 0)} icon={Table2} />
@@ -498,7 +572,7 @@ function ImageGallery({ images, parseId }: { images: DocImage[]; parseId: string
return ( return (
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4"> <section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint"> <h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
Images gedetecteerd ({images.length}) no embedded export Images detected ({images.length}) no embedded export
</h3> </h3>
<p className="text-[11px] text-foreground-muted">Re-upload the document to extract images (embedded mode).</p> <p className="text-[11px] text-foreground-muted">Re-upload the document to extract images (embedded mode).</p>
</section> </section>
@@ -508,7 +582,7 @@ function ImageGallery({ images, parseId }: { images: DocImage[]; parseId: string
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4"> <section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
<h3 className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint"> <h3 className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
<Image className="h-4 w-4 text-docker" /> <Image className="h-4 w-4 text-docker" />
Images die Docling ziet ({available.length}) Images extracted by Docling ({available.length})
</h3> </h3>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{available.map((img) => ( {available.map((img) => (
@@ -601,9 +675,13 @@ function DocStructurePanel({ structure, title }: { structure: DocStructure; titl
) )
} }
function DimensionCard({ dimension: d }: { dimension: Dimension }) { function normalizeChecks(items: DqCheck[]): DqCheck[] {
return items.map((c, i) => ({ ...c, id: c.id || `${c.suite}_${i}` }))
}
function DimensionCard({ dimension: d, expanded, onToggle }: { dimension: Dimension; expanded?: boolean; onToggle?: () => void }) {
return ( return (
<div className="rounded-lg border border-border bg-surface-overlay/40 p-3"> <button type="button" onClick={onToggle} className="w-full rounded-lg border border-border bg-surface-overlay/40 p-3 text-left transition-colors hover:border-docker/30">
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
<span className="text-[12px] font-semibold">{d.label}</span> <span className="text-[12px] font-semibold">{d.label}</span>
<span className={cn('text-base font-bold', SCORE_COLOR(d.score))}>{d.score}</span> <span className={cn('text-base font-bold', SCORE_COLOR(d.score))}>{d.score}</span>
@@ -617,10 +695,16 @@ function DimensionCard({ dimension: d }: { dimension: Dimension }) {
<li key={f} className="flex gap-1"><span className="text-docker"></span>{f}</li> <li key={f} className="flex gap-1"><span className="text-docker"></span>{f}</li>
))} ))}
</ul> </ul>
{d.recommended_actions && d.recommended_actions.length > 0 && ( {expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
<p className="mt-2 border-t border-border pt-2 text-[9px] text-warning"> {d.recommended_actions[0]}</p> <div className="mt-2 border-t border-border pt-2">
<p className="mb-1 text-[9px] font-semibold uppercase text-foreground-faint">Recommended actions</p>
{d.recommended_actions.map((a) => <p key={a} className="text-[9px] text-warning"> {a}</p>)}
</div>
)} )}
</div> {!expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
<p className="mt-2 border-t border-border pt-2 text-[9px] text-foreground-faint">Click for {d.recommended_actions.length} recommended action(s)</p>
)}
</button>
) )
} }
@@ -662,24 +746,75 @@ function ColumnTable({ profiles, expandedCol, onToggle }: { profiles: ColumnProf
) )
} }
function CheckTable({ title, rows }: { title: string; rows: { name: string; status: string; detail: string }[] }) { function checkLabel(c: DqCheck): string {
const base = c.expectation || c.name || c.metric || c.check || c.test || c.rule || c.monitor || 'check'
return c.column ? `${base} [${c.column}]` : base
}
function checkStatus(c: DqCheck): string {
return c.status || (c.success === false ? 'fail' : c.success ? 'pass' : c.outcome || 'unknown')
}
function checkDetail(c: DqCheck): string {
return c.result || c.detail || ''
}
function CheckTable({ title, checks, expandedId, onToggle }: { title: string; checks: DqCheck[]; expandedId: string | null; onToggle: (id: string | null) => void }) {
return ( return (
<div className="overflow-x-auto rounded-lg border border-border"> <div className="overflow-x-auto rounded-lg border border-border">
<p className="border-b border-border bg-surface-overlay px-3 py-2 text-[11px] font-semibold">{title}</p> <p className="border-b border-border bg-surface-overlay px-3 py-2 text-[11px] font-semibold capitalize">{title}</p>
<table className="w-full text-[10px]"> <table className="w-full text-[10px]">
<tbody> <tbody>
{rows.map((r, i) => ( {checks.map((c) => {
<tr key={i} className="border-t border-border"> const id = c.id || checkLabel(c)
<td className="px-3 py-1.5"> const status = checkStatus(c)
<span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase', const viol = c.violations
r.status === 'pass' ? 'bg-success/20 text-success' : r.status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}> return (
{r.status} <Fragment key={id}>
</span> <tr className="cursor-pointer border-t border-border hover:bg-surface-overlay/50" onClick={() => onToggle(expandedId === id ? null : id)}>
{r.name} <td className="px-3 py-1.5">
</td> <span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
<td className="px-3 py-1.5 text-foreground-muted">{r.detail}</td> status === 'pass' ? 'bg-success/20 text-success' : status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}>
</tr> {status}
))} </span>
{checkLabel(c)}
{viol?.violation_count ? <span className="ml-2 text-[9px] text-danger">{viol.violation_count} violations</span> : null}
</td>
<td className="px-3 py-1.5 text-foreground-muted">{checkDetail(c)}</td>
</tr>
{expandedId === id && (
<tr className="border-t border-border bg-surface-overlay/20">
<td colSpan={2} className="px-4 py-3 text-[10px]">
<p className="mb-2 font-semibold text-docker">Error location &amp; samples</p>
{viol?.violation_count ? (
<>
<p className="text-foreground-muted">{viol.violation_count} rows affected ({viol.violation_pct}%){viol.location_hint ? ` · ${viol.location_hint}` : ''}</p>
{viol.sample_rows && viol.sample_rows.length > 0 && (
<div className="mt-2 overflow-x-auto rounded border border-border">
<table className="w-full text-[9px]">
<thead><tr className="bg-surface-overlay"><th className="px-2 py-1">Row #</th>{Object.keys(viol.sample_rows[0].values).map((k) => <th key={k} className="px-2 py-1">{k}</th>)}</tr></thead>
<tbody>
{viol.sample_rows.map((s) => (
<tr key={s.row_index} className="border-t border-border/50">
<td className="px-2 py-1 font-mono text-warning">{s.row_index}</td>
{Object.values(s.values).map((v, j) => <td key={j} className="max-w-[120px] truncate px-2 py-1">{v === null ? 'NULL' : String(v)}</td>)}
</tr>
))}
</tbody>
</table>
</div>
)}
</>
) : (
<p className="text-success">No row-level violations check passed.</p>
)}
{c.meta && <pre className="mt-2 rounded bg-black/30 p-2 font-mono text-[9px]">{JSON.stringify(c.meta, null, 2)}</pre>}
</td>
</tr>
)}
</Fragment>
)
})}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -43,7 +43,7 @@ export function InfraQuickAccess({
<div className="mb-2 flex items-center justify-between gap-2"> <div className="mb-2 flex items-center justify-between gap-2">
<div> <div>
<h3 className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">Infrastructure & Apps</h3> <h3 className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">Infrastructure & Apps</h3>
<p className="text-[8px] text-foreground-faint">Klik voor inspector · Shell kopieert SSH en opent live terminal · UI opent de applicatie</p> <p className="text-[8px] text-foreground-faint">Click for inspector · Shell copies SSH and opens live terminal · UI opens the app</p>
</div> </div>
{workload && ( {workload && (
<span className="shrink-0 font-mono text-[8px] text-foreground-faint"> <span className="shrink-0 font-mono text-[8px] text-foreground-faint">
@@ -95,16 +95,16 @@ export function InspectorPanel({
{!d && !agent && ( {!d && !agent && (
<div className="scrollbar-thin flex-1 space-y-3 overflow-y-auto p-3"> <div className="scrollbar-thin flex-1 space-y-3 overflow-y-auto p-3">
<div className="rounded-lg border border-docker/25 bg-docker-light/50 p-2.5 dark:bg-blue-500/10"> <div className="rounded-lg border border-docker/25 bg-docker-light/50 p-2.5 dark:bg-blue-500/10">
<p className="mb-1.5 text-[10px] font-semibold text-foreground">Snel starten</p> <p className="mb-1.5 text-[10px] font-semibold text-foreground">Quick start</p>
<ol className="list-decimal space-y-1 pl-4 text-[10px] leading-relaxed text-foreground-muted"> <ol className="list-decimal space-y-1 pl-4 text-[10px] leading-relaxed text-foreground-muted">
<li>Select an <strong className="text-foreground">infrastructure card</strong> or topology node</li> <li>Select an <strong className="text-foreground">infrastructure card</strong> or topology node</li>
<li>Klik <strong className="text-foreground">Shell</strong> voor SSH + live terminal output</li> <li>Click <strong className="text-foreground">Shell</strong> for SSH + live terminal output</li>
<li>Klik <strong className="text-foreground">UI</strong> om Airflow, Trino, Kafka UI, etc. te openen</li> <li>Click <strong className="text-foreground">UI</strong> to open Airflow, Trino, Kafka UI, etc.</li>
<li>Stel vragen via <strong className="text-foreground">Chat</strong> onderaan agents zien de hele cluster</li> <li>Ask questions via <strong className="text-foreground">Chat</strong> below agents see the full cluster</li>
</ol> </ol>
</div> </div>
<div> <div>
<p className="mb-1.5 text-[9px] uppercase tracking-wider text-foreground-faint">Agents & domeinen</p> <p className="mb-1.5 text-[9px] uppercase tracking-wider text-foreground-faint">Agents & domains</p>
<div className="space-y-1"> <div className="space-y-1">
{agents.filter((a) => !a.supervisor).map((a) => { {agents.filter((a) => !a.supervisor).map((a) => {
const meta = getAgentMeta(a.id) const meta = getAgentMeta(a.id)
@@ -201,7 +201,7 @@ export function InspectorPanel({
{infra && ( {infra && (
<Button size="sm" variant="outline" onClick={runShell} disabled={busy}> <Button size="sm" variant="outline" onClick={runShell} disabled={busy}>
<Terminal className="h-3 w-3" /> <Terminal className="h-3 w-3" />
{shellCopied ? 'SSH gekopieerd' : 'Shell'} {shellCopied ? 'SSH copied' : 'Shell'}
</Button> </Button>
)} )}
<Button size="sm" variant="outline" onClick={onProbe} disabled={busy}> <Button size="sm" variant="outline" onClick={onProbe} disabled={busy}>
@@ -275,7 +275,7 @@ export function InspectorPanel({
)} )}
</div> </div>
<form onSubmit={submit} className="flex gap-1 border-t border-border p-2"> <form onSubmit={submit} className="flex gap-1 border-t border-border p-2">
<Input value={input} onChange={(e) => setInput(e.target.value)} placeholder={`Vraag over ${d.label}`} disabled={busy} className="text-xs" /> <Input value={input} onChange={(e) => setInput(e.target.value)} placeholder={`Ask about ${d.label}`} disabled={busy} className="text-xs" />
<Button type="submit" size="sm" disabled={busy || !input.trim()}>Send</Button> <Button type="submit" size="sm" disabled={busy || !input.trim()}>Send</Button>
</form> </form>
</> </>
+2 -2
View File
@@ -49,11 +49,11 @@ export function TerminalDock({ subjectId, subjectLabel, lines, busy, expanded, o
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 pb-2 font-mono text-[9px] leading-relaxed"> <div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 pb-2 font-mono text-[9px] leading-relaxed">
{infra && ( {infra && (
<p className="mb-1 text-foreground-faint"> <p className="mb-1 text-foreground-faint">
<span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(gekopieerd bij Shell-knop)</span> <span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(copied via Shell button)</span>
</p> </p>
)} )}
{lines.length === 0 && ( {lines.length === 0 && (
<p className="py-4 text-center text-foreground-faint">Selecteer een node of agent · klik Shell of Probe om output te zien</p> <p className="py-4 text-center text-foreground-faint">Select a node or agent · click Shell or Probe to see output</p>
)} )}
{lines.map((line) => ( {lines.map((line) => (
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}> <div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
@@ -55,7 +55,7 @@ export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props
e.preventDefault() e.preventDefault()
}} }}
className="flex h-2.5 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-surface-overlay/60 hover:bg-docker/10" className="flex h-2.5 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-surface-overlay/60 hover:bg-docker/10"
title="Sleep omhoog/omlaag om grootte aan te passen" title="Drag up/down to resize"
> >
<GripHorizontal className="h-3 w-3 text-foreground-faint" /> <GripHorizontal className="h-3 w-3 text-foreground-faint" />
</div> </div>