feat: centralize API error localization

Replace the err instanceof ApiError ? err.message : t(fallback) anti-
pattern -- which showed raw English backend text for the common case and
only used the localized fallback for the rare network-failure case -- at
all 13 call sites across 7 files.

New frontend/src/api/errorMessages.ts (describeApiError) resolves a
caught error to a localized {title, explanation, nextStep?, technical}
by checking the 32 known AppError codes first, then known HTTP statuses
(401/403/404/409/422/500), then a fully generic fallback. New
ApiErrorNotice (PageChrome.tsx) renders title/explanation/nextStep with
the raw text demoted to a "Technical details"/"Details techniques"
disclosure -- never shown as the primary message.

ApiError itself is split out of client.ts into a standalone
api/apiError.ts with no import.meta.env dependency, so errorMessages.ts
(and its tests) can be loaded outside a Vite/browser context.
This commit is contained in:
NuklearRabbit
2026-08-04 03:08:07 +02:00
parent 94cfb7bcbb
commit d17af1c52a
16 changed files with 940 additions and 82 deletions
+6 -5
View File
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { api, ApiError } from "../api/client";
import { api } from "../api/client";
import { describeApiError, type ApiErrorInfo } from "../api/errorMessages";
import type { AutomationRun, IntegrationStatus, KnowledgeHealth } from "../api/types";
import { StatusBadge } from "../components/Badge";
import { useAuth } from "../context/AuthContext";
import { useLocaleFormat } from "../i18n/format";
import { ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
import { ApiErrorNotice, ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
import { N8N_STATE_META, MCP_STATE_META } from "../data/integrationLabels";
type ViewFilter = "attention" | "recent" | "succeeded" | "all";
@@ -25,7 +26,7 @@ export function Automation() {
const [status, setStatus] = useState("");
const [view, setView] = useState<ViewFilter>("attention");
const [expandSucceeded, setExpandSucceeded] = useState(false);
const [retryError, setRetryError] = useState<string | null>(null);
const [retryError, setRetryError] = useState<ApiErrorInfo | null>(null);
const [retrying, setRetrying] = useState<string | null>(null);
const [knowledge, setKnowledge] = useState<KnowledgeHealth | null>(null);
const [integrationStatus, setIntegrationStatus] = useState<IntegrationStatus | null>(null);
@@ -70,7 +71,7 @@ export function Automation() {
load();
loadIntegrationStatus();
} catch (err) {
setRetryError(err instanceof ApiError ? err.message : t("ledger.retryFailed"));
setRetryError(describeApiError(t, err, "ledger.retryFailed"));
} finally {
setRetrying(null);
}
@@ -242,7 +243,7 @@ export function Automation() {
</form>
{error && <ErrorState message={error} />}
{retryError && <p className="error" role="alert">{retryError}</p>}
<ApiErrorNotice error={retryError} />
{!error && !runs && <LoadingState label={t("ledger.loading")} />}
{visibleRuns && visibleRuns.length === 0 && <p>{t("ledger.empty")}</p>}