fix: localize the primary data-quality evidence summary (live-caught on Unraid)

Live validation on the deployed fix branch caught a real bug: every data-quality
issue's top-of-page "Evidence summary" line rendered the raw, always-English legacy
evidence.summary string unconditionally -- in all three languages -- even though the
backend has been emitting structured, localizable evidence.signals for a while
(app/services/data_quality.py already documented this exact intent). The frontend
side of that conversion was never finished.

- DataQualityIssueDetail.tsx now renders evidence.signals through the operator's
  locale as the primary summary; the raw evidence.summary string is only visible
  inside "Technical details" (via the existing EvidenceDisclosure JSON dump).
- The four DQ-DEMO-* seed rows that anchor the guided demo's scripted scenarios now
  carry real, accurate signals computed at seed time (duplicate-customer's similarity
  score is the actual SequenceMatcher ratio on the seeded names, not invented) instead
  of only a legacy English sentence.
- Rows with no structured signals (generic filler seed data) fall back to the raw
  text rather than showing a blank summary; the one known placeholder string gets its
  own localized rendering so it never displays as English filler either.
- New regression test: the vehicle_status_conflict evidence summary must show
  localized text and must never contain the specific raw English sentence that was
  live-visible before this fix, in all 3 languages.

151 backend tests, Ruff, mypy green; full local Playwright suite green (a couple of
sequential-run-only flakes, both confirmed to pass in isolation and unrelated to this
change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-04 00:44:06 +02:00
co-authored by Claude Sonnet 5
parent cda2c32bd0
commit 2e4fb43f09
7 changed files with 188 additions and 14 deletions
+77 -1
View File
@@ -46,6 +46,79 @@ function EvidenceDisclosure({ issue }: { issue: IssueDetail }) {
);
}
interface EvidenceSignal {
code: string;
params?: Record<string, unknown>;
}
// The backend never emits prose for evidence -- only stable signal codes + raw data
// params (see app/services/data_quality.py::_open_issue). This is the one place that
// turns them into the operator's selected language; the legacy `evidence.summary`
// string is a technical fallback only, shown solely inside EvidenceDisclosure above.
// A handful of seed-only rows (generic filler, not tied to a scripted demo scenario)
// carry no structured signals -- rather than show nothing, this known placeholder
// summary gets a localized rendering. Any other un-signalled legacy text still falls
// back to the raw string (better than a blank primary evidence area), but only
// "Technical details" is meant to guarantee raw-text visibility.
const SEED_PLACEHOLDER_SUMMARY = "Synthetic deterministic seed issue";
function EvidenceSignalList({ issue }: { issue: IssueDetail }) {
const { t } = useTranslation("quality");
const { formatNumber } = useLocaleFormat();
const signals = (issue.evidence.signals as EvidenceSignal[] | undefined) ?? [];
if (signals.length === 0) {
const rawSummary = String(issue.evidence.summary ?? "");
if (!rawSummary) return null;
const text =
rawSummary === SEED_PLACEHOLDER_SUMMARY
? t("detail.evidence.seedPlaceholder")
: rawSummary;
return (
<ul className="evidence-signal-list">
<li>{text}</li>
</ul>
);
}
function describe(signal: EvidenceSignal): string {
const { code, params = {} } = signal;
if (code.startsWith("vehicle.")) {
return t(`detail.vehicleStatusConflict.reasonCodes.${code}`, { defaultValue: code });
}
switch (code) {
case "missing_field":
return t("detail.evidence.missingField", {
field: t(`detail.missingField.fields.${params.field}`, { defaultValue: String(params.field) }),
});
case "overlap.reserved_bookings":
return t("detail.evidence.overlapReservedBookings", {
refs: Array.isArray(params.refs) ? params.refs.join(" ↔ ") : "",
});
case "odometer.regression":
return t("detail.evidence.odometerRegression", {
laterRef: params.later_ref,
laterKm: formatNumber(Number(params.later_km ?? 0)),
earlierRef: params.earlier_ref,
earlierKm: formatNumber(Number(params.earlier_km ?? 0)),
});
case "duplicate.similar_name":
return t("detail.evidence.duplicateSimilarName", { score: params.score });
case "attention.upcoming_booking_missing_inspection":
return t("detail.evidence.upcomingBookingMissingInspection", { bookingRef: params.booking_ref });
default:
return t(`detail.evidence.${code}`, { defaultValue: code });
}
}
return (
<ul className="evidence-signal-list">
{signals.map((signal, index) => (
<li key={`${signal.code}-${index}`}>{describe(signal)}</li>
))}
</ul>
);
}
function DuplicateCustomerPanel({ issue, onResolved }: { issue: IssueDetail; onResolved: () => void }) {
const { t } = useTranslation("quality");
const { user } = useAuth();
@@ -726,8 +799,11 @@ export function DataQualityIssueDetail() {
<section className="record-surface" aria-label={t("detail.summary.rule")}><dl className="detail-grid">
<div><dt>{t("detail.summary.rule")}</dt><dd>{t(`ruleTypes.${issue.rule_type}`, { defaultValue: issue.rule_type.replace(/_/g, " ") })}</dd></div>
<div><dt>{t("detail.summary.entity")}</dt><dd>{issue.entity_type === "vehicle" ? <Link to={`/vehicles/${issue.entity_ref}`}>{issue.entity_ref}</Link> : issue.entity_ref}</dd></div>
<div><dt>{t("detail.summary.evidenceSummary")}</dt><dd>{String(issue.evidence.summary ?? "")}</dd></div>
</dl>
<div className="record-surface-evidence">
<dt>{t("detail.summary.evidenceSummary")}</dt>
<EvidenceSignalList issue={issue} />
</div>
<EvidenceDisclosure issue={issue} /></section>
<RuleExplainer ruleType={issue.rule_type} />