diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx
index 47471280..3315e346 100644
--- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx
+++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx
@@ -3,7 +3,7 @@
import { useState, useMemo } from "react";
import { EnrollmentEvent, Session } from "@/types";
import { normalizeEventDataForDisplay, shortenBuildHashInMessage } from "../utils/eventHelpers";
-import { getErrorCodeEntry, formatErrorCode } from "@/utils/errorCodeMap";
+import { getErrorCodeEntry, getEnrichedOrLookup, formatErrorCode } from "@/utils/errorCodeMap";
interface EventTimelineProps {
filteredEvents: EnrollmentEvent[];
@@ -447,8 +447,10 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr
const hr = event.data?.hresultFromWin32 ?? event.data?.hresult_from_win32;
const hasNonZero = (ec && String(ec) !== "0") || (hr && String(hr) !== "0");
if (!hasNonZero) return null;
- const ecEntry = ec ? getErrorCodeEntry(String(ec)) : null;
- const hrEntry = hr ? getErrorCodeEntry(String(hr)) : null;
+ // Prefer backend-enriched *Info sibling, fall back to local lookup for older
+ // responses that pre-date the backend ErrorCodeEnricher.
+ const ecEntry = ec ? getEnrichedOrLookup(event.data?.exitCodeInfo, String(ec)) : null;
+ const hrEntry = hr ? getEnrichedOrLookup(event.data?.hresultFromWin32Info, String(hr)) : null;
return (
{ec && String(ec) !== "0" && (
@@ -489,7 +491,7 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr
const isAdvisory = event.eventType === "esp_failure_advisory";
if (!code && !isAdvisory) return null;
const codeStr = code ? String(code) : null;
- const entry = codeStr ? getErrorCodeEntry(codeStr) : null;
+ const entry = codeStr ? getEnrichedOrLookup(event.data?.errorCodeInfo, codeStr) : null;
// Advisory path uses warning-color palette (amber); the device continued past the
// failure via ContinueAnyway, so this is not a hard error. PR1 Session 4fa5a2d4.
const badgeBg = isAdvisory ? "bg-amber-100 text-amber-800" : "bg-red-100 text-red-800";
diff --git a/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts b/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts
index 0633c863..ecc30285 100644
--- a/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts
+++ b/src/Web/autopilot-monitor-web/utils/__tests__/errorCodeMap.test.ts
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
getErrorCodeDescription,
getErrorCodeEntry,
+ getEnrichedOrLookup,
formatErrorCode,
} from "../errorCodeMap";
import catalogFile from "../error-codes.json";
@@ -67,6 +68,43 @@ describe("errorCodeMap", () => {
});
});
+ describe("getEnrichedOrLookup", () => {
+ it("prefers backend-enriched sibling when present", () => {
+ const enriched = {
+ description: "Backend-provided description",
+ confidence: "high" as const,
+ source: "Backend Catalog v2",
+ };
+ const result = getEnrichedOrLookup(enriched, "0x80070005");
+ expect(result).toBe(enriched);
+ expect(result?.description).toBe("Backend-provided description");
+ });
+
+ it("falls back to local catalog when info is null", () => {
+ const result = getEnrichedOrLookup(null, "0x80070005");
+ expect(result).not.toBeNull();
+ expect(result?.description).toContain("Access denied");
+ });
+
+ it("falls back to local catalog when info is undefined", () => {
+ const result = getEnrichedOrLookup(undefined, "0x80070005");
+ expect(result).not.toBeNull();
+ expect(result?.description).toContain("Access denied");
+ });
+
+ it("returns null when both info and lookup miss", () => {
+ const result = getEnrichedOrLookup(null, "0xDEADBEEF");
+ expect(result).toBeNull();
+ });
+
+ it("ignores malformed info objects and falls back to lookup", () => {
+ // Backend would never emit this but guard against bad clients.
+ const malformed = { description: 42 } as unknown as { description: string; confidence: "high"; source: string };
+ const result = getEnrichedOrLookup(malformed, "0x80070005");
+ expect(result?.description).toContain("Access denied");
+ });
+ });
+
describe("formatErrorCode", () => {
it("uppercases hex codes", () => {
expect(formatErrorCode("0x80070005")).toBe("0X80070005");
diff --git a/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts b/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts
index a7f13572..23365c63 100644
--- a/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts
+++ b/src/Web/autopilot-monitor-web/utils/errorCodeMap.ts
@@ -95,6 +95,26 @@ export function getErrorCodeEntry(code: string | number | null | undefined): Err
return lookupEntry(code);
}
+/**
+ * Backend-enriched info entry. The backend ErrorCodeEnricher injects sibling
+ * {codeKey}Info properties into event Data containing the same shape as
+ * . When present we prefer the server-side value (uses the
+ * authoritative catalog version + Confidence-level), and only fall back to the local
+ * lookup for older responses that don't carry the enrichment yet.
+ *
+ * Example: an event with { errorCode: "0x80070005", errorCodeInfo: {...} }
+ * passes the info through without a second lookup.
+ */
+export function getEnrichedOrLookup(
+ info: ErrorCodeEntry | null | undefined,
+ code: string | number | null | undefined
+): ErrorCodeEntry | null {
+ if (info && typeof info === "object" && typeof info.description === "string") {
+ return info;
+ }
+ return lookupEntry(code);
+}
+
/**
* Format a raw numeric error code for display.
* Signed-decimal HRESULTs are converted to hex notation.