M8: GUI polish, n8n workflow-3 fixes, RAGcore retrieval root-cause and fix
GUI: dashboard Attention Queue presents a curated severity mix instead of pure severity-sort (grouped Now/Today/Later headers); Today's Movements seed data curated so a fresh reset shows a credible day (2+ departures, 2+ returns), with a new seed-integrity test; About Demo restructured into a compact grid with progressive disclosure for technical sections; Duplicate Merge shows match/conflict counts, hides matching fields by default, and previews the final merged record before confirmation. Repo hygiene: removed a stray empty `backend;C` directory and an untracked 31MB zip export; `.gitignore` now excludes future archive exports. n8n: fixed invalid JSON (a missing `},` between two node objects) in the committed `fleet-ops-vehicle-return.json` -- the file could not be parsed. Live-validated workflow 3 (RAGcore Procedure Sync): found and fixed a real defect (three body parameters had a stray trailing `}}`) and a missing Error Workflow wiring, both via the safe `n8n import:workflow` CLI path; exported the corrected, still- inactive workflow as the new source of truth and updated MANIFEST.md/check_drift.py. Publishing it (starts real daily unattended runs) remains a separate decision. RAGcore: root-caused and fixed (live, approved) the "zero retrieval candidates" bug -- a filesystem permission bug (`embedding_profiles.json` unreadable by the app's own runtime user) that broke every retrieval call before it reached Qdrant. Every other suspect (grants, scope resolution, Qdrant filters, embeddings) was verified healthy first. Found a second, deeper gap: the reranker adapter calls an Ollama HTTP route that does not exist on the deployed Ollama version, so `/v1/answers` still returns `not_answerable`. `KNOWLEDGE_PROVIDER` stays `demo` until that is resolved on the RAGcore side. Evidence-based MCP Hub integration status (real tool-call audit history, not just a boolean flag) replaces the old `configured`/`not_configured` guess. Full findings in `docs/final-integrations/current-state-audit.md`. Backend: 172 tests passing, ruff clean, mypy clean (50 files). Frontend: tsc clean, production build clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3ebca9e9b7
commit
34df66d28c
@@ -67,30 +67,32 @@ export function AboutDemo() {
|
||||
<p>{t("about.scopeBody", { productName: PRODUCT_NAME })}</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.realTitle")}</h2>
|
||||
<p>{t("about.realBody")}</p>
|
||||
</section>
|
||||
<div className="about-grid">
|
||||
<div className="record-surface about-card">
|
||||
<h2>{t("about.realTitle")}</h2>
|
||||
<p>{t("about.realBody")}</p>
|
||||
</div>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.syntheticTitle")}</h2>
|
||||
<p>{t("about.syntheticBody", { testDomain: ".test" })}</p>
|
||||
</section>
|
||||
<div className="record-surface about-card">
|
||||
<h2>{t("about.syntheticTitle")}</h2>
|
||||
<p>{t("about.syntheticBody", { testDomain: ".test" })}</p>
|
||||
</div>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.architectureTitle")}</h2>
|
||||
<p>{t("about.architectureBody")}</p>
|
||||
</section>
|
||||
<details className="record-surface about-card about-details">
|
||||
<summary>{t("about.architectureTitle")}</summary>
|
||||
<p>{t("about.architectureBody")}</p>
|
||||
</details>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.securityTitle")}</h2>
|
||||
<p>{t("about.securityBody")}</p>
|
||||
</section>
|
||||
<details className="record-surface about-card about-details">
|
||||
<summary>{t("about.securityTitle")}</summary>
|
||||
<p>{t("about.securityBody")}</p>
|
||||
</details>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.testingTitle")}</h2>
|
||||
<p>{t("about.testingBody")}</p>
|
||||
</section>
|
||||
<details className="record-surface about-card about-details">
|
||||
<summary>{t("about.testingTitle")}</summary>
|
||||
<p>{t("about.testingBody")}</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<section aria-label={t("about.integrationsTitle")} className="record-surface">
|
||||
<SectionHeading title={t("about.integrationsTitle")} description={t("about.integrationsDescription")} />
|
||||
|
||||
@@ -75,6 +75,17 @@ export function Dashboard() {
|
||||
return matchesSeverity && haystack.includes(query.toLowerCase());
|
||||
}) ?? [], [data, query, severity, t]);
|
||||
|
||||
const isUnfiltered = severity === "all" && query === "";
|
||||
const attentionTiers = useMemo(() => {
|
||||
if (!isUnfiltered) return [{ key: "all", labelKey: "", items: attention.slice(0, 6) }];
|
||||
const bySeverity = (level: string) => attention.filter((item) => item.severity === level);
|
||||
return [
|
||||
{ key: "high", labelKey: "attention.tierNow", items: bySeverity("high") },
|
||||
{ key: "medium", labelKey: "attention.tierToday", items: bySeverity("medium") },
|
||||
{ key: "low", labelKey: "attention.tierLater", items: bySeverity("low") },
|
||||
].filter((tier) => tier.items.length > 0);
|
||||
}, [attention, isUnfiltered]);
|
||||
|
||||
if (error) return <ErrorState message={error} />;
|
||||
if (!data) return <LoadingState label={t("common:status.loading")} />;
|
||||
|
||||
@@ -129,32 +140,39 @@ export function Dashboard() {
|
||||
<label><span className="visually-hidden">{t("attention.severityAriaLabel")}</span><select value={severity} onChange={(event) => setSeverity(event.target.value)}><option value="all">{t("attention.severityAll")}</option><option value="high">{t("attention.severityHigh")}</option><option value="medium">{t("attention.severityMedium")}</option><option value="low">{t("attention.severityLow")}</option></select></label>
|
||||
</div>
|
||||
{attention.length === 0 ? <p className="quiet-empty"><Icon name="check" /> {t("attention.empty")}</p> : (
|
||||
<ul className="attention-list">
|
||||
{attention.slice(0, 6).map((item, index) => {
|
||||
const href = item.issue_ref && canSeeQuality
|
||||
? `/data-quality/${item.issue_ref}`
|
||||
: item.link_type === "vehicle"
|
||||
? `/vehicles/${item.link_ref}`
|
||||
: null;
|
||||
const title = attentionItemTitle(t, item);
|
||||
return (
|
||||
<li key={`${item.link_ref}-${index}`} className={href ? "row-clickable" : ""}>
|
||||
<SeverityBadge severity={item.severity} />
|
||||
<div className="queue-copy">
|
||||
<p className="attention-title">{title}</p>
|
||||
<p className="attention-detail">{item.detail}</p>
|
||||
</div>
|
||||
<span className="queue-ref">{item.link_ref}</span>
|
||||
<Icon name="chevron" className="row-chevron" />
|
||||
{href && (
|
||||
<Link className="row-link" to={href} aria-label={t("attention.openRecord", { title })}>
|
||||
<span className="visually-hidden">{title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<>
|
||||
{attentionTiers.map((tier) => (
|
||||
<div key={tier.key} className="attention-tier">
|
||||
{attentionTiers.length > 1 && <p className="attention-tier-label">{t(tier.labelKey)}</p>}
|
||||
<ul className="attention-list">
|
||||
{tier.items.map((item, index) => {
|
||||
const href = item.issue_ref && canSeeQuality
|
||||
? `/data-quality/${item.issue_ref}`
|
||||
: item.link_type === "vehicle"
|
||||
? `/vehicles/${item.link_ref}`
|
||||
: null;
|
||||
const title = attentionItemTitle(t, item);
|
||||
return (
|
||||
<li key={`${item.link_ref}-${index}`} className={href ? "row-clickable" : ""}>
|
||||
<SeverityBadge severity={item.severity} />
|
||||
<div className="queue-copy">
|
||||
<p className="attention-title">{title}</p>
|
||||
<p className="attention-detail">{item.detail}</p>
|
||||
</div>
|
||||
<span className="queue-ref">{item.link_ref}</span>
|
||||
<Icon name="chevron" className="row-chevron" />
|
||||
{href && (
|
||||
<Link className="row-link" to={href} aria-label={t("attention.openRecord", { title })}>
|
||||
<span className="visually-hidden">{title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
|
||||
const [error, setError] = useState<ApiErrorInfo | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [showMatching, setShowMatching] = useState(false);
|
||||
|
||||
if (!issue.entity_snapshot || !issue.related_snapshots[0]) {
|
||||
return <p className="error">{t("detail.duplicateCustomer.bothMissing")}</p>;
|
||||
@@ -137,6 +138,9 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
|
||||
|
||||
const survivor = survivorRef === a.public_ref ? a : b;
|
||||
const loser = survivorRef === a.public_ref ? b : a;
|
||||
const conflictingFields = MERGE_FIELDS.filter((field) => String(a[field] ?? "") !== String(b[field] ?? ""));
|
||||
const matchingFields = MERGE_FIELDS.filter((field) => !conflictingFields.includes(field));
|
||||
const visibleFields = showMatching ? MERGE_FIELDS : conflictingFields;
|
||||
|
||||
async function handleMerge() {
|
||||
setError(null);
|
||||
@@ -174,6 +178,10 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
|
||||
<SectionHeading headingId="compare-heading" title={t("detail.duplicateCustomer.heading")} description={t("detail.duplicateCustomer.description")} />
|
||||
<ApiErrorNotice error={error} />
|
||||
|
||||
<p className="merge-summary-counts">
|
||||
{t("detail.duplicateCustomer.summaryCounts", { matchCount: matchingFields.length, conflictCount: conflictingFields.length })}
|
||||
</p>
|
||||
|
||||
<fieldset className="choice-fieldset">
|
||||
<legend>{t("detail.duplicateCustomer.keepAsSurvivor")}</legend>
|
||||
<label className={`choice-card ${survivorRef === a.public_ref ? "is-selected" : ""}`}>
|
||||
@@ -205,7 +213,7 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{MERGE_FIELDS.map((field) => {
|
||||
{visibleFields.map((field) => {
|
||||
const valueA = a[field] ? String(a[field]) : "—";
|
||||
const valueB = b[field] ? String(b[field]) : "—";
|
||||
const differ = valueA !== valueB;
|
||||
@@ -248,10 +256,33 @@ function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onR
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{matchingFields.length > 0 && (
|
||||
<button type="button" className="button button-tertiary toggle-matching-fields" onClick={() => setShowMatching((v) => !v)}>
|
||||
{showMatching ? t("detail.duplicateCustomer.hideMatchingFields") : t("detail.duplicateCustomer.showMatchingFields")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<p className="merge-preview">
|
||||
{t("detail.duplicateCustomer.mergePreview", { loser: loser.public_ref, survivor: survivor.public_ref })}
|
||||
</p>
|
||||
|
||||
<div className="merge-record-preview">
|
||||
<p className="merge-record-preview-title">{t("detail.duplicateCustomer.previewTitle", { ref: survivor.public_ref })}</p>
|
||||
<dl>
|
||||
{MERGE_FIELDS.map((field) => {
|
||||
const choice = fieldChoices[field];
|
||||
const chosenSide = choice === "a" ? a : choice === "b" ? b : survivor;
|
||||
const value = (chosenSide[field] ? String(chosenSide[field]) : survivor[field] ? String(survivor[field]) : "—");
|
||||
return (
|
||||
<div key={field}>
|
||||
<dt>{t(`detail.duplicateCustomer.fields.${field}`)}</dt>
|
||||
<dd>{value || "—"}</dd>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{!confirming && (
|
||||
<button type="button" className="button button-primary" onClick={() => setConfirming(true)}>
|
||||
{t("detail.duplicateCustomer.mergeInto", { ref: survivor.public_ref })}
|
||||
|
||||
Reference in New Issue
Block a user