From d34c2be19bb2fced811e1060e0ee11eb95357791 Mon Sep 17 00:00:00 2001
From: woahwhattheheck
Date: Thu, 8 Oct 2026 06:03:57 -0400
Subject: [PATCH 1/6] feat(services): add stable dashboard skeleton
---
.../services/ServicesDashboardSkeleton.tsx | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 src/app/services/ServicesDashboardSkeleton.tsx
diff --git a/src/app/services/ServicesDashboardSkeleton.tsx b/src/app/services/ServicesDashboardSkeleton.tsx
new file mode 100644
index 0000000..b9f11c3
--- /dev/null
+++ b/src/app/services/ServicesDashboardSkeleton.tsx
@@ -0,0 +1,39 @@
+type ServicesDashboardSkeletonProps = {
+ rows?: number;
+};
+
+/**
+ * Mirrors the service-row footprint so loading-to-content swaps keep the
+ * dashboard geometry stable while the status region announces progress.
+ */
+export function ServicesDashboardSkeleton({
+ rows = 5,
+}: ServicesDashboardSkeletonProps) {
+ const rowCount = Math.max(1, Math.floor(rows));
+
+ return (
+
+
Loading services
+
+ {Array.from({ length: rowCount }, (_, index) => (
+ -
+
+
+
+ ))}
+
+
+ );
+}
From b1f17b9e1d934c6f50fb55594861c38e74c396a5 Mon Sep 17 00:00:00 2001
From: woahwhattheheck
Date: Thu, 8 Oct 2026 06:04:08 -0400
Subject: [PATCH 2/6] feat(services): contain dashboard section crashes
---
src/app/services/ServicesSectionBoundary.tsx | 75 ++++++++++++++++++++
1 file changed, 75 insertions(+)
create mode 100644 src/app/services/ServicesSectionBoundary.tsx
diff --git a/src/app/services/ServicesSectionBoundary.tsx b/src/app/services/ServicesSectionBoundary.tsx
new file mode 100644
index 0000000..1e8bf1d
--- /dev/null
+++ b/src/app/services/ServicesSectionBoundary.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import {
+ Component,
+ type ErrorInfo,
+ type ReactNode,
+} from "react";
+
+type ServicesSectionBoundaryProps = {
+ sectionName: string;
+ children: ReactNode;
+};
+
+type ServicesSectionBoundaryState = {
+ hasError: boolean;
+};
+
+/**
+ * Keeps one services-dashboard subtree from taking down its siblings.
+ * Render failures are logged for observability while the UI intentionally
+ * avoids exposing exception details to users.
+ */
+export class ServicesSectionBoundary extends Component<
+ ServicesSectionBoundaryProps,
+ ServicesSectionBoundaryState
+> {
+ state: ServicesSectionBoundaryState = {
+ hasError: false,
+ };
+
+ static getDerivedStateFromError(): ServicesSectionBoundaryState {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error: Error, info: ErrorInfo) {
+ console.error("Services dashboard section failed", {
+ section: this.props.sectionName,
+ error,
+ componentStack: info.componentStack,
+ });
+ }
+
+ private retry = () => {
+ this.setState({ hasError: false });
+ };
+
+ render() {
+ if (!this.state.hasError) {
+ return this.props.children;
+ }
+
+ return (
+
+
+ {this.props.sectionName} unavailable.
+
+
+ This section hit an unexpected error. The rest of the dashboard is
+ still available.
+
+
+
+ );
+ }
+}
From 46e3f27d89715f549bb0b5d1fc63041f943025ef Mon Sep 17 00:00:00 2001
From: woahwhattheheck
Date: Thu, 8 Oct 2026 06:04:20 -0400
Subject: [PATCH 3/6] feat(services): match route loading to dashboard layout
---
src/app/services/loading.tsx | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/app/services/loading.tsx b/src/app/services/loading.tsx
index 682f836..5c1c73c 100644
--- a/src/app/services/loading.tsx
+++ b/src/app/services/loading.tsx
@@ -1,5 +1,17 @@
-import { PageSkeleton } from "@/components/PageSkeleton";
+import { PageShell } from "@/components/PageShell";
+import { ServicesDashboardSkeleton } from "./ServicesDashboardSkeleton";
export default function Loading() {
- return ;
+ return (
+
+
+
+
+ );
}
From f5668a936f63ae0cea1d8fc51d77ef8b02a25302 Mon Sep 17 00:00:00 2001
From: woahwhattheheck
Date: Thu, 8 Oct 2026 06:04:40 -0400
Subject: [PATCH 4/6] feat(services): isolate dashboard sections
---
src/app/services/page.tsx | 136 ++++++++++++++++++++++----------------
1 file changed, 80 insertions(+), 56 deletions(-)
diff --git a/src/app/services/page.tsx b/src/app/services/page.tsx
index 75dd6ed..e423f3f 100644
--- a/src/app/services/page.tsx
+++ b/src/app/services/page.tsx
@@ -7,7 +7,8 @@ import { ErrorMessage } from "@/components/ErrorMessage";
import { EmptyState } from "@/components/EmptyState";
import { PageShell } from "@/components/PageShell";
import { Pagination } from "@/components/Pagination";
-import { Spinner } from "@/components/Spinner";
+import { ServicesDashboardSkeleton } from "./ServicesDashboardSkeleton";
+import { ServicesSectionBoundary } from "./ServicesSectionBoundary";
import { truncateMiddle } from "@/lib/format";
import { useToast } from "@/components/ToastProvider";
import { useClipboard } from "@/lib/useClipboard";
@@ -177,6 +178,64 @@ export function ServiceCopyButton({ serviceId }: { serviceId: string }) {
);
}
+
+function ServicesResults({
+ services,
+ sortKey,
+ sortDir,
+}: {
+ services: Service[];
+ sortKey: SortKey;
+ sortDir: SortDir;
+}) {
+ const sortedServices = useSorted(services, sortKey, sortDir);
+
+ if (services.length === 0) {
+ return (
+
+ New service
+
+ }
+ />
+ );
+ }
+
+ return (
+
+ {sortedServices?.map((service) => (
+ -
+
+
+ {truncateMiddle(service.serviceId)}
+
+
+ {service.priceStroops} stroops / request
+
+
+
+
+ ))}
+
+ );
+}
+
export default function ServicesPage() {
const [services, setServices] = useState(null);
const [error, setError] = useState(null);
@@ -188,8 +247,6 @@ export default function ServicesPage() {
getInitialSort
);
- const sortedServices = useSorted(services, sortKey, sortDir);
-
const handleSort = (key: SortKey) => {
const next: { key: SortKey; dir: SortDir } =
key === sortKey
@@ -266,59 +323,26 @@ export default function ServicesPage() {
- {loading && (
-
-
-
- )}
- {!loading && services && services.length === 0 && (
-
- New service
-
- }
- />
- )}
- {!loading && services && services.length > 0 && (
-
- {sortedServices?.map((s) => (
- -
-
-
- {truncateMiddle(s.serviceId)}
-
-
- {s.priceStroops} stroops / request
-
-
-
-
- ))}
-
- )}
- {!loading && !error && (
-
- )}
+
+ {loading ? (
+
+ ) : services ? (
+
+ ) : null}
+
+
+ {!loading && !error && (
+
+ )}
+
);
}
From f6edb7b426b338bf91e60c34d28989a20cf3ad8e Mon Sep 17 00:00:00 2001
From: woahwhattheheck
Date: Thu, 8 Oct 2026 06:05:01 -0400
Subject: [PATCH 5/6] test(services): cover scoped loading and recovery
---
src/app/services/resilience.test.tsx | 134 +++++++++++++++++++++++++++
1 file changed, 134 insertions(+)
create mode 100644 src/app/services/resilience.test.tsx
diff --git a/src/app/services/resilience.test.tsx b/src/app/services/resilience.test.tsx
new file mode 100644
index 0000000..a89276a
--- /dev/null
+++ b/src/app/services/resilience.test.tsx
@@ -0,0 +1,134 @@
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import { apiGet } from "../../lib/apiClient";
+import { ToastProvider } from "../../components/ToastProvider";
+import ServicesPage from "./page";
+import { ServicesDashboardSkeleton } from "./ServicesDashboardSkeleton";
+import { ServicesSectionBoundary } from "./ServicesSectionBoundary";
+
+jest.mock("../../lib/apiClient", () => ({
+ apiGet: jest.fn(),
+}));
+
+const apiGetMock = apiGet as jest.MockedFunction;
+
+function renderServicesPage() {
+ return render(
+
+
+
+ );
+}
+
+describe("services dashboard resilience", () => {
+ beforeEach(() => {
+ apiGetMock.mockReset();
+ window.history.replaceState(null, "", "/services");
+ });
+
+ it("announces loading and swaps equal-height skeleton rows for content", async () => {
+ let resolveRequest:
+ | ((value: {
+ services: Array<{ serviceId: string; priceStroops: number }>;
+ page: number;
+ pageCount: number;
+ }) => void)
+ | undefined;
+
+ apiGetMock.mockReturnValueOnce(
+ new Promise((resolve) => {
+ resolveRequest = resolve;
+ }) as never
+ );
+
+ renderServicesPage();
+
+ const status = screen.getByRole("status");
+ expect(status).toHaveAttribute("aria-live", "polite");
+ expect(status).toHaveAttribute("aria-busy", "true");
+
+ const skeletonRows = screen.getAllByTestId("service-row-skeleton");
+ expect(skeletonRows).toHaveLength(5);
+ skeletonRows.forEach((row) => {
+ expect(row.className).toContain("min-h-12");
+ });
+
+ await act(async () => {
+ resolveRequest?.({
+ services: [{ serviceId: "svc-live", priceStroops: 42 }],
+ page: 1,
+ pageCount: 1,
+ });
+ });
+
+ const serviceLink = await screen.findByRole("link", {
+ name: /svc-live/i,
+ });
+ expect(serviceLink.closest("li")?.className).toContain("min-h-12");
+ expect(screen.queryByTestId("service-row-skeleton")).not.toBeInTheDocument();
+ });
+
+ it("renders the requested number of skeleton rows", () => {
+ render();
+
+ expect(screen.getAllByTestId("service-row-skeleton")).toHaveLength(2);
+ });
+
+ it("contains a render crash to one section and logs it", () => {
+ const consoleError = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => undefined);
+
+ function BrokenSection(): never {
+ throw new Error("render exploded");
+ }
+
+ render(
+ <>
+ Healthy dashboard sibling
+
+
+
+ >
+ );
+
+ expect(screen.getByText("Healthy dashboard sibling")).toBeInTheDocument();
+ expect(screen.getByRole("alert")).toHaveTextContent(
+ "Service list unavailable."
+ );
+ expect(consoleError).toHaveBeenCalled();
+
+ consoleError.mockRestore();
+ });
+
+ it("re-renders only the failed section when retry is requested", () => {
+ const consoleError = jest
+ .spyOn(console, "error")
+ .mockImplementation(() => undefined);
+ let shouldThrow = true;
+
+ function FlakySection() {
+ if (shouldThrow) {
+ throw new Error("temporary render failure");
+ }
+ return Recovered service list
;
+ }
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("alert")).toBeInTheDocument();
+
+ shouldThrow = false;
+ fireEvent.click(
+ screen.getByRole("button", { name: "Retry section" })
+ );
+
+ expect(screen.getByText("Recovered service list")).toBeInTheDocument();
+ expect(screen.queryByRole("alert")).not.toBeInTheDocument();
+
+ consoleError.mockRestore();
+ });
+});
From ed32656b273004fe9c7c68cfee2031d3ef1edc52 Mon Sep 17 00:00:00 2001
From: woahwhattheheck
Date: Thu, 8 Oct 2026 06:38:13 -0400
Subject: [PATCH 6/6] fix(services): add stable section error codes
---
src/app/services/ServicesSectionBoundary.tsx | 29 ++++++++++++++++--
src/app/services/page.tsx | 15 +++++++--
src/app/services/resilience.test.tsx | 32 ++++++++++++++++----
3 files changed, 65 insertions(+), 11 deletions(-)
diff --git a/src/app/services/ServicesSectionBoundary.tsx b/src/app/services/ServicesSectionBoundary.tsx
index 1e8bf1d..8333d11 100644
--- a/src/app/services/ServicesSectionBoundary.tsx
+++ b/src/app/services/ServicesSectionBoundary.tsx
@@ -6,8 +6,24 @@ import {
type ReactNode,
} from "react";
+export const SERVICES_SECTION_ERROR_CODES = {
+ serviceList: "SERVICES_LIST_RENDER_FAILED",
+ pagination: "SERVICES_PAGINATION_RENDER_FAILED",
+} as const;
+
+export type ServicesSectionErrorCode =
+ (typeof SERVICES_SECTION_ERROR_CODES)[keyof typeof SERVICES_SECTION_ERROR_CODES];
+
+export type ServicesSectionFailure = {
+ code: ServicesSectionErrorCode;
+ section: string;
+ error: Error;
+ componentStack: string | null;
+};
+
type ServicesSectionBoundaryProps = {
sectionName: string;
+ errorCode: ServicesSectionErrorCode;
children: ReactNode;
};
@@ -33,11 +49,14 @@ export class ServicesSectionBoundary extends Component<
}
componentDidCatch(error: Error, info: ErrorInfo) {
- console.error("Services dashboard section failed", {
+ const failure: ServicesSectionFailure = {
+ code: this.props.errorCode,
section: this.props.sectionName,
error,
componentStack: info.componentStack,
- });
+ };
+
+ console.error("Services dashboard section failed", failure);
}
private retry = () => {
@@ -62,6 +81,12 @@ export class ServicesSectionBoundary extends Component<
This section hit an unexpected error. The rest of the dashboard is
still available.
+
+ Reference: {this.props.errorCode}
+