From 429b53cc86e2963085452e130031e31250594daa Mon Sep 17 00:00:00 2001 From: root Date: Mon, 29 Jun 2026 01:07:21 +0200 Subject: [PATCH 1/2] feat(agents): format request counts on the agent detail page Route the lifetime total and per-service totals through formatRequests for consistent locale thousands grouping across the agent detail page. Closes #155 --- src/app/agents/[agent]/page.test.tsx | 66 ++++++++++++++++++++++++++++ src/app/agents/[agent]/page.tsx | 5 ++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/app/agents/[agent]/page.test.tsx b/src/app/agents/[agent]/page.test.tsx index 538acfb..0077fcf 100644 --- a/src/app/agents/[agent]/page.test.tsx +++ b/src/app/agents/[agent]/page.test.tsx @@ -77,4 +77,70 @@ describe("AgentDetailPage", () => { const currentItem = screen.getByText(specialId, { selector: '[aria-current="page"]' }); expect(currentItem).toBeInTheDocument(); }); + + it("formats a large lifetime total with thousands separators", async () => { + mockApiGet.mockReset(); + mockApiGet.mockImplementation((url: string) => { + if (url.endsWith("/total")) return Promise.resolve({ total: 1234567 }) as never; + if (url.endsWith("/usage")) return Promise.resolve({ items: [] }) as never; + return Promise.reject(new Error("Not found")) as never; + }); + + renderPage("agent-big"); + + const strong = await screen.findByText("1,234,567"); + expect(strong.tagName).toBe("STRONG"); + expect(strong.parentElement).toHaveTextContent("1,234,567 requests"); + }); + + it("formats per-service totals with thousands separators", async () => { + mockApiGet.mockReset(); + mockApiGet.mockImplementation((url: string) => { + if (url.endsWith("/total")) return Promise.resolve({ total: 0 }) as never; + if (url.endsWith("/usage")) + return Promise.resolve({ + items: [ + { serviceId: "svc-a", total: 999 }, + { serviceId: "svc-b", total: 1500000 }, + ], + }) as never; + return Promise.reject(new Error("Not found")) as never; + }); + + renderPage("agent-svc"); + + await screen.findByText(/Lifetime total/i); + + expect(screen.getByText("999 requests")).toBeInTheDocument(); + expect(screen.getByText("1,500,000 requests")).toBeInTheDocument(); + }); + + it("renders zero total without grouping", async () => { + mockApiGet.mockReset(); + mockApiGet.mockImplementation((url: string) => { + if (url.endsWith("/total")) return Promise.resolve({ total: 0 }) as never; + if (url.endsWith("/usage")) return Promise.resolve({ items: [] }) as never; + return Promise.reject(new Error("Not found")) as never; + }); + + renderPage("agent-zero"); + + const strong = await screen.findByText("0"); + expect(strong.tagName).toBe("STRONG"); + }); + + it("does not render lifetime total when the optional total fetch fails", async () => { + mockApiGet.mockReset(); + mockApiGet.mockImplementation((url: string) => { + if (url.endsWith("/total")) return Promise.reject(new Error("boom")) as never; + if (url.endsWith("/usage")) return Promise.resolve({ items: [] }) as never; + return Promise.reject(new Error("Not found")) as never; + }); + + renderPage("agent-no-total"); + + await screen.findByText(/No services consumed yet/i); + + expect(screen.queryByText(/Lifetime total/i)).not.toBeInTheDocument(); + }); }); diff --git a/src/app/agents/[agent]/page.tsx b/src/app/agents/[agent]/page.tsx index 8489b19..a53b67e 100644 --- a/src/app/agents/[agent]/page.tsx +++ b/src/app/agents/[agent]/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, use } from "react"; import { Breadcrumb } from "@/components/Breadcrumb"; import { apiGet } from "@/lib/apiClient"; +import { formatRequests } from "@/lib/format"; type Usage = { agent: string; items: { serviceId: string; total: number }[] }; @@ -44,7 +45,7 @@ export default function AgentDetailPage({ )} {total !== null && (

- Lifetime total: {total} requests + Lifetime total: {formatRequests(total)} requests

)} {items && items.length === 0 && ( @@ -55,7 +56,7 @@ export default function AgentDetailPage({ {items.map((s) => (
  • {s.serviceId} - {s.total} requests + {formatRequests(s.total)} requests
  • ))} From d85d1a78618b7368b744099e615fd579cbceb101 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 29 Jun 2026 11:31:58 +0200 Subject: [PATCH 2/2] feat(services): adopt loading-aware Button with aria-busy for New Service submit - Add prop to shared Button component - Set when loading is true, removed on completion - Pass from NewServicePage to Button - Add test assertions for aria-busy state during submit --- src/app/services/new/page.test.tsx | 8 ++++++++ src/app/services/new/page.tsx | 1 + src/components/Button.tsx | 3 +++ 3 files changed, 12 insertions(+) diff --git a/src/app/services/new/page.test.tsx b/src/app/services/new/page.test.tsx index c7b8729..51962a7 100644 --- a/src/app/services/new/page.test.tsx +++ b/src/app/services/new/page.test.tsx @@ -197,11 +197,19 @@ describe("NewServicePage", () => { expect(submitButton).toBeDisabled(); expect(submitButton).toHaveTextContent("Saving…"); + // Button should have aria-busy while loading + expect(submitButton).toHaveAttribute("aria-busy", "true"); + // Resolve post request resolvePost({}); await waitFor(() => { expect(mockPush).toHaveBeenCalledWith("/services"); }); + + // aria-busy should be removed after submission + await waitFor(() => { + expect(submitButton).not.toHaveAttribute("aria-busy"); + }); }); }); diff --git a/src/app/services/new/page.tsx b/src/app/services/new/page.tsx index b9ea5e8..af7fafd 100644 --- a/src/app/services/new/page.tsx +++ b/src/app/services/new/page.tsx @@ -64,6 +64,7 @@ export default function NewServicePage() {