feat(desktop): unify frontend error notices

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Senior Frontend Engineer
2026-03-28 11:08:01 +00:00
parent 8d50243d36
commit a9524dfbf6
9 changed files with 825 additions and 96 deletions

View File

@@ -0,0 +1,89 @@
import { describe, expect, it } from "vitest";
import {
createAccentNotice,
createNeutralNotice,
toBackendError,
toBackendErrorNotice,
} from "./operator-notices";
describe("operator notices", () => {
it("creates explicit accent notices for successful actions", () => {
expect(createAccentNotice("Saved")).toEqual({
tone: "accent",
message: "Saved",
});
});
it("normalizes unknown thrown values into internal backend errors", () => {
expect(toBackendError(new Error("boom"))).toEqual({
code: "internal",
message: "boom",
detail: null,
});
});
it("maps authentication failures to a consistent operator-facing notice", () => {
expect(
toBackendErrorNotice(
{
code: "authentication_failed",
message: "Authentication failed.",
detail: "WRONGPASS invalid username-password pair",
},
{
operation: "connection_test",
connectionName: "redis-local-fixture",
databaseLabel: "db0",
},
),
).toEqual({
tone: "danger",
message:
"Redis rejected the credentials used while testing redis-local-fixture on db0. Verify username, password, and ACL scope for the selected connection. Backend detail: Authentication failed. (WRONGPASS invalid username-password pair).",
});
});
it("keeps unsupported operations in a non-destructive neutral notice", () => {
expect(
toBackendErrorNotice(
{
code: "unsupported_operation",
message: "Only string replacement is supported in V1.",
detail: null,
},
{
operation: "value_write",
connectionName: "redis-local-fixture",
databaseLabel: "db0",
keyName: "session:42",
},
),
).toEqual(
createNeutralNotice(
"The current desktop contract does not allow saving session:42 on redis-local-fixture / db0 yet. Stay on the visible read-only path or wait for a backend contract change before retrying. Backend detail: Only string replacement is supported in V1.",
),
);
});
it("describes command failures with command and scope context", () => {
expect(
toBackendErrorNotice(
{
code: "command_failed",
message: "ERR unknown command 'FOOO'",
detail: null,
},
{
operation: "command_execution",
connectionName: "redis-local-fixture",
databaseLabel: "db0",
commandName: "fooo",
},
),
).toEqual({
tone: "danger",
message:
"Redis returned an error while running FOOO on redis-local-fixture / db0. Adjust the Redis input first; retrying the same command unchanged is unlikely to help. Backend detail: ERR unknown command 'FOOO'.",
});
});
});

View File

@@ -0,0 +1,205 @@
export type NoticeTone = "neutral" | "accent" | "danger";
export type OperatorNotice = {
tone: NoticeTone;
message: string;
};
export type BackendErrorCode =
| "invalid_connection_config"
| "unsupported_tls_mode"
| "unsupported_operation"
| "connection_failed"
| "authentication_failed"
| "command_failed"
| "internal";
export type BackendError = {
code: BackendErrorCode;
message: string;
detail: string | null;
};
export type BackendErrorContext =
| {
operation: "connection_test";
connectionName: string;
databaseLabel: string;
}
| {
operation: "key_browse";
connectionName: string;
databaseLabel: string;
}
| {
operation: "value_read" | "value_write" | "ttl_update" | "ttl_remove" | "inspector_refresh";
connectionName: string;
databaseLabel: string;
keyName: string;
}
| {
operation: "command_execution";
connectionName: string;
databaseLabel: string;
commandName: string;
};
export function createNeutralNotice(message: string): OperatorNotice {
return {
tone: "neutral",
message,
};
}
export function createAccentNotice(message: string): OperatorNotice {
return {
tone: "accent",
message,
};
}
export function createDangerNotice(message: string): OperatorNotice {
return {
tone: "danger",
message,
};
}
export function toBackendError(error: unknown): BackendError {
if (
typeof error === "object" &&
error !== null &&
"message" in error &&
typeof error.message === "string"
) {
return {
code: normalizeBackendErrorCode("code" in error ? error.code : undefined),
message: error.message,
detail:
"detail" in error && typeof error.detail === "string"
? error.detail
: null,
};
}
return {
code: "internal",
message: error instanceof Error ? error.message : "Unknown backend error.",
detail: null,
};
}
export function toBackendErrorNotice(
error: unknown,
context: BackendErrorContext,
): OperatorNotice {
const backendError = toBackendError(error);
const tone =
backendError.code === "invalid_connection_config" ||
backendError.code === "unsupported_operation" ||
backendError.code === "unsupported_tls_mode"
? "neutral"
: "danger";
const headline = buildHeadline(backendError, context);
const hint = buildHint(backendError.code, context);
const detail = buildTechnicalDetail(backendError);
return {
tone,
message: [headline, hint, detail].filter(Boolean).join(" "),
};
}
function normalizeBackendErrorCode(value: unknown): BackendErrorCode {
switch (value) {
case "invalid_connection_config":
case "unsupported_tls_mode":
case "unsupported_operation":
case "connection_failed":
case "authentication_failed":
case "command_failed":
case "internal":
return value;
default:
return "internal";
}
}
function buildHeadline(error: BackendError, context: BackendErrorContext) {
const target = describeContext(context);
switch (error.code) {
case "invalid_connection_config":
return `The request for ${target} was rejected before Redis was contacted.`;
case "unsupported_tls_mode":
return `The request for ${target} requires TLS handling that this foundation build does not support yet.`;
case "unsupported_operation":
return `The current desktop contract does not allow ${target} yet.`;
case "connection_failed":
return `Redis could not be reached while ${target}.`;
case "authentication_failed":
return `Redis rejected the credentials used while ${target}.`;
case "command_failed":
return `Redis returned an error while ${target}.`;
case "internal":
return `The desktop bridge failed while ${target}.`;
default:
return `The desktop bridge failed while ${target}.`;
}
}
function buildHint(code: BackendErrorCode, context: BackendErrorContext) {
switch (code) {
case "invalid_connection_config":
return "Check host, port, database, search pattern, and key input before retrying.";
case "unsupported_tls_mode":
return "Keep TLS disabled in this foundation build unless product scope explicitly adds transport controls.";
case "unsupported_operation":
return "Stay on the visible read-only path or wait for a backend contract change before retrying.";
case "connection_failed":
return `Verify that ${context.connectionName} is reachable from this machine and that the selected database is available.`;
case "authentication_failed":
return "Verify username, password, and ACL scope for the selected connection.";
case "command_failed":
return "Adjust the Redis input first; retrying the same command unchanged is unlikely to help.";
case "internal":
return "Retry once, then capture the QA snapshot and escalate if the same failure repeats.";
default:
return "";
}
}
function buildTechnicalDetail(error: BackendError) {
const detail = error.detail && error.detail !== error.message ? error.detail : null;
const summary = detail ? `${error.message} (${detail})` : error.message;
return `Backend detail: ${ensureTrailingPeriod(summary)}`;
}
function describeContext(context: BackendErrorContext) {
switch (context.operation) {
case "connection_test":
return `testing ${context.connectionName} on ${context.databaseLabel}`;
case "key_browse":
return `loading keys from ${context.connectionName} / ${context.databaseLabel}`;
case "value_read":
return `reading ${context.keyName} on ${context.connectionName} / ${context.databaseLabel}`;
case "value_write":
return `saving ${context.keyName} on ${context.connectionName} / ${context.databaseLabel}`;
case "ttl_update":
return `updating TTL for ${context.keyName} on ${context.connectionName} / ${context.databaseLabel}`;
case "ttl_remove":
return `removing TTL from ${context.keyName} on ${context.connectionName} / ${context.databaseLabel}`;
case "inspector_refresh":
return `refreshing ${context.keyName} on ${context.connectionName} / ${context.databaseLabel}`;
case "command_execution":
return `running ${context.commandName.toUpperCase()} on ${context.connectionName} / ${context.databaseLabel}`;
default:
return "handling a desktop Redis request";
}
}
function ensureTrailingPeriod(value: string) {
return /[.!?]$/.test(value) ? value : `${value}.`;
}