feat(lists): read and show the list cap on every capped list

Twelve routes answer a capped list with meta.truncated / meta.limit on
the envelope, and the web client read only data, so no surface said a
list had stopped short. apiGetList / apiPostList are now the one client
reader of the cap and ListCapNotice the one line that shows it; every
capped list on the web reads its cap and shows the notice under the list,
the paged readers (side effects, illness episodes) stop on the route's
answer instead of a full-page guess, and the ECG list moves its five
readers onto one hook. The record activity feed sent its cap twice, in
data and on the envelope; it rides the envelope alone now. A guard
derives the capped routes from the tree and binds each to a client that
reads its cap.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 10:04:11 +02:00
1 parent be79603d53
commit dc5120b59a
49 files changed
+574 -204

No files matched your search

@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
import { readListCap } from "@/lib/api/list-cap-shape";
import { ListCapNotice } from "../list-cap-notice";
const render = (node: React.ReactNode) =>
renderToStaticMarkup(<I18nProvider initialLocale="en">{node}</I18nProvider>);
describe("ListCapNotice", () => {
it("says the list stopped at its ceiling, with the number", () => {
const html = render(<ListCapNotice cap={{ truncated: true, limit: 50 }} />);
expect(html).toContain('data-slot="list-cap-notice"');
expect(html).toContain("Showing the latest 50");
});
it("renders nothing for a complete list or no cap", () => {
expect(
render(<ListCapNotice cap={{ truncated: false, limit: 50 }} />),
).toBe("");
expect(render(<ListCapNotice cap={null} />)).toBe("");
});
});
describe("readListCap", () => {
it("reads the envelope's cap and nothing else", () => {
expect(
readListCap({ truncated: true, limit: 100, requestId: "r" }),
).toEqual({ truncated: true, limit: 100 });
expect(readListCap({ aggregate: {} })).toBeNull();
expect(readListCap({ truncated: "yes", limit: 1 })).toBeNull();
expect(readListCap(undefined)).toBeNull();
});
});
+30
View File
@@ -0,0 +1,30 @@
"use client";
import type { ListCap } from "@/lib/api/list-cap-shape";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
/**
* The line under a list the server cut at its ceiling. Renders nothing for a
* complete list, so a surface passes the cap it read (`apiGetList`) and
* never decides on its own whether to show it. Meta tier: one muted sentence
* at the `text-xs` floor.
*/
export function ListCapNotice({
cap,
className,
}: {
cap: ListCap | null | undefined;
className?: string;
}) {
const { t } = useTranslations();
if (!cap?.truncated) return null;
return (
<p
data-slot="list-cap-notice"
className={cn("text-muted-foreground text-xs", className)}
>
{t("common.listCapped", { count: String(cap.limit) })}
</p>
);
}