diff --git a/e2e/admin-api-tokens-mobile.spec.ts b/e2e/admin-api-tokens-mobile.spec.ts
index 944e837b5..221738589 100644
--- a/e2e/admin-api-tokens-mobile.spec.ts
+++ b/e2e/admin-api-tokens-mobile.spec.ts
@@ -118,4 +118,90 @@ test.describe("/admin/api-tokens mobile no-overflow", () => {
}));
expect(stripDims.scrollWidth).toBeGreaterThan(stripDims.clientWidth);
});
+
+ // v1.4.19 phase A7 (4th attempt) — even with the mobile-strip
+ // `no-scrollbar` fix from v1.4.18, Marc still reported a painted
+ // scrollbar. The card body itself must not contribute any
+ // horizontal-overflow culprit: every element inside the api-tokens
+ // section must satisfy `scrollWidth <= clientWidth + 1`. Pads with
+ // a long token name + long username + a long permission to exercise
+ // the worst case.
+ test("Pixel-5 viewport: nothing inside the api-tokens card overflows horizontally", async ({
+ page,
+ }) => {
+ await page.route("**/api/admin/tokens", (route) =>
+ route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: [
+ {
+ id: "tok1",
+ name: "iOS auto-login 2026-05-05T19:42:11Z device-AABBCCDDEEFF-marathon",
+ permissions: [
+ "measurements:write:audit:legacy:extended",
+ "medications:read",
+ "*",
+ ],
+ lastUsedAt: "2026-05-08T12:00:00Z",
+ expiresAt: null,
+ createdAt: "2026-05-01T08:00:00Z",
+ revoked: false,
+ user: {
+ id: "u1",
+ username: "marc-the-very-long-username-that-overflows",
+ },
+ },
+ ],
+ error: null,
+ }),
+ }),
+ );
+
+ await page.goto("/admin/api-tokens", { waitUntil: "domcontentloaded" });
+ await page.waitForLoadState("networkidle");
+ await expect(page.getByTestId("admin-tokens-mobile-list")).toBeVisible({
+ timeout: 10_000,
+ });
+
+ // Walk every element inside the mobile card-list and assert no
+ // child has a wider scrollWidth than its container. This is the
+ // Pixel-5 worst-case Marc kept hitting.
+ const culprits = await page
+ .getByTestId("admin-tokens-mobile-list")
+ .evaluate((root) => {
+ const out: Array<{
+ tag: string;
+ klass: string;
+ scrollWidth: number;
+ clientWidth: number;
+ }> = [];
+ for (const el of root.querySelectorAll("*")) {
+ if (el.scrollWidth > el.clientWidth + 1) {
+ out.push({
+ tag: el.tagName,
+ klass:
+ typeof el.className === "string"
+ ? el.className.slice(0, 80)
+ : "",
+ scrollWidth: el.scrollWidth,
+ clientWidth: el.clientWidth,
+ });
+ }
+ }
+ return out;
+ });
+ expect(
+ culprits,
+ `overflow culprits: ${JSON.stringify(culprits, null, 2)}`,
+ ).toEqual([]);
+
+ // Document-level horizontal overflow — the page must not paint
+ // any horizontal scroll, period.
+ const dims = await page.evaluate(() => ({
+ scrollWidth: document.documentElement.scrollWidth,
+ innerWidth: window.innerWidth,
+ }));
+ expect(dims.scrollWidth).toBeLessThanOrEqual(dims.innerWidth + 1);
+ });
});
diff --git a/src/components/admin/__tests__/api-token-truncate.test.tsx b/src/components/admin/__tests__/api-token-truncate.test.tsx
new file mode 100644
index 000000000..1aabe41d2
--- /dev/null
+++ b/src/components/admin/__tests__/api-token-truncate.test.tsx
@@ -0,0 +1,133 @@
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+/**
+ * v1.4.19 A7 — `/admin/api-tokens` 4th-attempt scrollbar fix.
+ *
+ * Marc reported a "minimaler Scrollbar" still painting at the
+ * bottom-right of the api-tokens page, even after the v1.4.18 phase
+ * A2 mobile-strip `no-scrollbar` fix. The remaining culprits are
+ * inside the section card itself: long token names, long usernames,
+ * and long permission strings can each push their parent past the
+ * viewport. Cumulatively this leaves the page in an "almost-fits"
+ * state where the visible scrollbar Marc sees is the page-level main
+ * scroll picking up a fraction of pixels of horizontal scroll spill
+ * from the children.
+ *
+ * The fix is aggressive: every cell that could carry a long string
+ * gets `truncate` (overflow-hidden + text-ellipsis + whitespace-nowrap)
+ * AND a tooltip on hover/long-press so the full value is still
+ * discoverable. This is the same pattern UserManagementSection uses
+ * — apply it consistently here.
+ */
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
+ usePathname: () => "/admin/api-tokens",
+}));
+
+const sampleTokens = [
+ {
+ id: "tok1",
+ name: "iOS auto-login 2026-05-05T19:42:11Z device-AABBCCDDEEFF",
+ permissions: ["measurements:write", "medications:read", "*"],
+ lastUsedAt: "2026-05-08T12:00:00Z",
+ expiresAt: null,
+ createdAt: "2026-05-01T08:00:00Z",
+ revoked: false,
+ user: {
+ id: "u1",
+ username: "marc-the-very-long-username-that-overflows",
+ },
+ },
+];
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: sampleTokens,
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+ useMutation: () => ({
+ mutate: vi.fn(),
+ isPending: false,
+ isError: false,
+ error: null,
+ }),
+}));
+
+vi.mock("@/hooks/use-auth", () => ({
+ useAuth: () => ({
+ user: {
+ id: "u1",
+ username: "marc",
+ email: "marc@example.com",
+ role: "ADMIN",
+ },
+ isAuthenticated: true,
+ isLoading: false,
+ refetch: vi.fn(),
+ }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { ApiTokenOverviewSection } from "../api-token-overview-section";
+
+function render() {
+ return renderToStaticMarkup(
+
| - {t("admin.tokenUser")} - | -- {t("admin.tokenName")} - | -- {t("admin.tokenPermissions")} - | -- {t("admin.tokenStatus")} - | -- {t("admin.tokenLastUsed")} - | -- {t("admin.tokenCreated")} - | -
|---|---|---|---|---|---|
| - {token.user.username} - | -{token.name} | -
-
- {token.permissions.map((p) => (
-
- |
-
- {token.revoked ? (
- |
- - {token.lastUsedAt - ? formatDateTime(token.lastUsedAt) - : t("admin.tokenNeverUsed")} - | -- {formatDate(token.createdAt)} - | -
| + {t("admin.tokenUser")} + | ++ {t("admin.tokenName")} + | ++ {t("admin.tokenPermissions")} + | ++ {t("admin.tokenStatus")} + | ++ {t("admin.tokenLastUsed")} + | ++ {t("admin.tokenCreated")} + | +
|---|---|---|---|---|---|
|
+ |
+
+ |
+
+
+ {token.permissions.map((p) => (
- - {token.user.username} - + |
+
+ {token.revoked ? (
+ |
+ + {token.lastUsedAt + ? formatDateTime(token.lastUsedAt) + : t("admin.tokenNeverUsed")} + | ++ {formatDate(token.createdAt)} + | +
- {t("admin.tokenLastUsed")}:{" "} - {token.lastUsedAt - ? formatDateTime(token.lastUsedAt) - : t("admin.tokenNeverUsed")} -
-- {t("admin.tokenCreated")}: {formatDate(token.createdAt)} -
- - ); - })} - - > - )} ++ {t("admin.tokenLastUsed")}:{" "} + {token.lastUsedAt + ? formatDateTime(token.lastUsedAt) + : t("admin.tokenNeverUsed")} +
++ {t("admin.tokenCreated")}: {formatDate(token.createdAt)} +
+