fix: safe status-recommendation flow, MO-016 order independence, brand constant, message codes

- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
  used identically by the data-quality scanner, a new non-mutating status-recommendation
  preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
  revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
  "maintenance + active booking -> auto rented" shortcut. Frontend
  DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
  why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
  <status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
  after the status-conflict issue now converges on the same final vehicle status,
  proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
  backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
  appeared in locale prose; add a permanent test guarding against a translation file
  ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
  reasons, audit field/actor-type labels, automation last_error, and search
  section/vehicle/booking/issue results all now carry codes the frontend localizes,
  with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
  decision table documenting the evaluator's rules and safe-status principles.

148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-03 21:37:34 +02:00
co-authored by Claude Sonnet 5
parent 18344bc8b7
commit 6deb95524d
68 changed files with 1734 additions and 225 deletions
+139 -15
View File
@@ -6,6 +6,7 @@ import type {
ApplyRecommendedStatusResult,
DataQualityIssueDetail as IssueDetail,
EntitySnapshot,
StatusRecommendation,
} from "../api/types";
import { SeverityBadge, StatusBadge } from "../components/Badge";
import { useAuth } from "../context/AuthContext";
@@ -443,15 +444,62 @@ function BookingOverlapPanel({ issue, onResolved }: { issue: IssueDetail; onReso
);
}
function statusLabel(t: (key: string, opts?: Record<string, unknown>) => string, status: string): string {
const raw = t(`fleet:statuses.${status}`, { defaultValue: status });
return raw.charAt(0).toUpperCase() + raw.slice(1);
}
function StatusRecommendationEvidence({ recommendation }: { recommendation: StatusRecommendation }) {
const { t } = useTranslation("quality");
const { formatNumber } = useLocaleFormat();
const facts = recommendation.facts;
const lines: string[] = [];
if (facts.active_booking_refs.length > 0) {
lines.push(
t("detail.vehicleStatusConflict.evidence.activeBookings", { refs: facts.active_booking_refs.join(", ") }),
);
}
if (facts.overlapping_booking_pairs.length > 0) {
lines.push(
t("detail.vehicleStatusConflict.evidence.overlappingBookings", {
pairs: facts.overlapping_booking_pairs.map((pair) => pair.join(" ↔ ")).join(", "),
}),
);
}
if (facts.service_threshold_reached) {
lines.push(
t("detail.vehicleStatusConflict.evidence.serviceThresholdReached", {
odometer: formatNumber(facts.odometer_km),
threshold: formatNumber(facts.next_service_km),
}),
);
}
if (facts.open_booking_overlap_issue_ref) {
lines.push(
t("detail.vehicleStatusConflict.evidence.openOverlapIssue", { ref: facts.open_booking_overlap_issue_ref }),
);
}
if (lines.length === 0) return null;
return (
<ul className="evidence-list">
{lines.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
);
}
function VehicleStatusConflictPanel({ issue, onResolved }: { issue: IssueDetail; onResolved: () => void }) {
const { t } = useTranslation("quality");
const navigate = useNavigate();
const { manifest } = useDemoManifest();
const { open: guideOpen, currentIndex, completeAndAdvance } = useDemoGuide();
const [recommendation, setRecommendation] = useState<StatusRecommendation | null>(null);
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
const [stale, setStale] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ApplyRecommendedStatusResult | null>(null);
const [submitting, setSubmitting] = useState(false);
const [confirming, setConfirming] = useState(false);
function continueDemo() {
completeAndAdvance();
@@ -465,23 +513,52 @@ function VehicleStatusConflictPanel({ issue, onResolved }: { issue: IssueDetail;
// visibility via `result` rather than disappearing the instant the reload lands.
if (issue.status !== "open" && !result) return null;
async function loadRecommendation() {
setError(null);
setStale(false);
setLoadingRecommendation(true);
try {
const preview = await api.post<StatusRecommendation>(
`/api/v1/data-quality/issues/${issue.public_ref}/status-recommendation`,
);
setRecommendation(preview);
} catch (err) {
setError(err instanceof ApiError ? err.message : t("detail.vehicleStatusConflict.recommendationFailed"));
} finally {
setLoadingRecommendation(false);
}
}
async function handleApply() {
if (!recommendation) return;
setError(null);
setSubmitting(true);
try {
const applied = await api.post<ApplyRecommendedStatusResult>(
`/api/v1/data-quality/issues/${issue.public_ref}/apply-recommended-status`,
{ recommendation_token: recommendation.recommendation_token },
);
setResult(applied);
onResolved();
} catch (err) {
setError(err instanceof ApiError ? err.message : t("detail.vehicleStatusConflict.applyFailed"));
setConfirming(false);
if (err instanceof ApiError && err.code === "RECOMMENDATION_STALE") {
setError(t("detail.vehicleStatusConflict.staleRecommendation"));
setRecommendation(null);
setStale(true);
} else {
setError(err instanceof ApiError ? err.message : t("detail.vehicleStatusConflict.applyFailed"));
}
} finally {
setSubmitting(false);
}
}
const needsManualReview =
recommendation !== null && (recommendation.manual_review_required || !recommendation.safe_to_apply);
const noChangeNeeded = recommendation !== null && !needsManualReview && recommendation.recommended_status === null;
const hasSafeRecommendation =
recommendation !== null && !needsManualReview && recommendation.recommended_status !== null;
return (
<section className="panel" aria-labelledby="status-conflict-heading">
<SectionHeading
@@ -496,7 +573,12 @@ function VehicleStatusConflictPanel({ issue, onResolved }: { issue: IssueDetail;
{result ? (
<>
<p className="quiet-empty">
<Icon name="check" /> {t("detail.vehicleStatusConflict.applied", { status: result.applied_status, reason: result.reason })}
<Icon name="check" /> {t("detail.vehicleStatusConflict.applied", {
status: statusLabel(t, result.applied_status),
reason: t(`detail.vehicleStatusConflict.reasonCodes.${result.reason_code}`, {
defaultValue: result.reason_code,
}),
})}
</p>
<div className="result-links">
<Link className="button button-secondary" to="/audit">{t("detail.resolved.viewAudit")}<Icon name="chevron" /></Link>
@@ -508,21 +590,63 @@ function VehicleStatusConflictPanel({ issue, onResolved }: { issue: IssueDetail;
)}
</div>
</>
) : !confirming ? (
<button type="button" className="button button-primary" onClick={() => setConfirming(true)}>
{t("detail.vehicleStatusConflict.calculateAndApply")}
) : !recommendation ? (
<button type="button" className="button button-primary" onClick={loadRecommendation} disabled={loadingRecommendation}>
{loadingRecommendation
? t("detail.vehicleStatusConflict.loadingRecommendation")
: t(stale ? "detail.vehicleStatusConflict.reviewAgain" : "detail.vehicleStatusConflict.reviewRecommendation")}
</button>
) : (
<div className="confirm-bar" role="alertdialog" aria-label={t("detail.vehicleStatusConflict.confirmTitle")}>
<p>{t("detail.vehicleStatusConflict.confirmBody")}</p>
) : needsManualReview ? (
<div className="status-decision" role="status">
<h3>{t("detail.vehicleStatusConflict.manualReview.heading")}</h3>
<p>{t("detail.vehicleStatusConflict.manualReview.body")}</p>
<StatusRecommendationEvidence recommendation={recommendation} />
<p className="table-subtext">{t("detail.vehicleStatusConflict.manualReview.hint")}</p>
</div>
) : noChangeNeeded ? (
<div className="status-decision" role="status">
<h3>{t("detail.vehicleStatusConflict.noConflict.heading")}</h3>
<p>{t("detail.vehicleStatusConflict.noConflict.body")}</p>
</div>
) : hasSafeRecommendation && recommendation.recommended_status ? (
<div className="status-decision" role="group" aria-label={t("detail.vehicleStatusConflict.recommendedStatus")}>
<dl className="detail-grid">
<div>
<dt>{t("detail.vehicleStatusConflict.recommendedStatus")}</dt>
<dd>
<StatusBadge
status={recommendation.recommended_status}
label={t(`fleet:statuses.${recommendation.recommended_status}`, {
defaultValue: recommendation.recommended_status,
})}
/>
</dd>
</div>
</dl>
<h3>{t("detail.vehicleStatusConflict.whyHeading")}</h3>
<p>{t(`detail.vehicleStatusConflict.reasonCodes.${recommendation.recommendation_code}`, {
defaultValue: recommendation.recommendation_code,
})}</p>
<h3>{t("detail.vehicleStatusConflict.evidenceHeading")}</h3>
<StatusRecommendationEvidence recommendation={recommendation} />
<h3>{t("detail.vehicleStatusConflict.consequenceHeading")}</h3>
<ul className="evidence-list">
<li>{t("detail.vehicleStatusConflict.consequence.statusWillChange", {
status: statusLabel(t, recommendation.recommended_status),
})}</li>
<li>{t("detail.vehicleStatusConflict.consequence.issueWillBeRechecked")}</li>
<li>{t("detail.vehicleStatusConflict.consequence.changeWillBeAudited")}</li>
<li>{t("detail.vehicleStatusConflict.consequence.bookingsNotDeleted")}</li>
</ul>
<button type="button" className="button button-primary" onClick={handleApply} disabled={submitting}>
{submitting ? t("detail.vehicleStatusConflict.applying") : t("detail.vehicleStatusConflict.confirmYes")}
</button>
<button type="button" className="button button-secondary" onClick={() => setConfirming(false)} disabled={submitting}>
{t("list.cancel")}
{submitting
? t("detail.vehicleStatusConflict.applying")
: t("detail.vehicleStatusConflict.changeStatusTo", {
status: statusLabel(t, recommendation.recommended_status),
})}
</button>
</div>
)}
) : null}
</section>
);
}