- {t(maxObservationCount != null ? "observerPage.countsPartial"
- : effectiveSort === "name" ? "observerPage.countsUnavailableName" : "observerPage.countsUnavailable")}
+ {partial && maxObservationCount == null &&
+ {t(effectiveSort === "name" ? "observerPage.countsUnavailableName" : "observerPage.countsUnavailable")}
}
- {windowEnd != null &&
+
{label}
{sublabel && {sublabel}}
-
{value}
- {presence ?
: spark ?
: sparkSpacer}
+
{value}
+
+ {presence ?
: spark ?
: sparkSpacer}
+
);
}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 84263a1..d9d97e9 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -958,12 +958,14 @@
},
"atlas": {
"addNodes": "Add nodes",
+ "matches": "Matches",
"search": "Find a node",
"searchHelp": "Searches every region by name or public-key prefix (hex, at least {{min}} characters). Up to {{limit}} nodes.",
"searchTooShort": "Type at least {{min}} characters.",
"searchNotHex": "A public key prefix is hex only (0-9, a-f).",
"savedCount_one": "{{count}} / {{limit}} saved",
"savedCount_other": "{{count}} / {{limit}} saved",
+ "savedCountShort": "{{count}}/{{limit}}",
"localOnly": "Saved in this browser",
"storageFailed": "Browser storage is unavailable; changes last only for this visit.",
"limitReached": "All {{limit}} slots are in use. Remove a card to add another.",
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 38a3152..0b03911 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -959,6 +959,7 @@
},
"atlas": {
"addNodes": "Ajouter des nœuds",
+ "matches": "Résultats",
"search": "Trouver un nœud",
"searchHelp": "Recherche dans toutes les régions par nom ou préfixe de clé publique (hexadécimal, au moins {{min}} caractères). Jusqu’à {{limit}} nœuds.",
"searchTooShort": "Saisissez au moins {{min}} caractères.",
@@ -966,6 +967,7 @@
"savedCount_one": "{{count}} / {{limit}} enregistré",
"savedCount_many": "{{count}} / {{limit}} enregistrés",
"savedCount_other": "{{count}} / {{limit}} enregistrés",
+ "savedCountShort": "{{count}}/{{limit}}",
"localOnly": "Enregistré dans ce navigateur",
"storageFailed": "Le stockage du navigateur est indisponible ; les modifications ne durent que cette visite.",
"limitReached": "Les {{limit}} emplacements sont utilisés. Retirez une fiche pour en ajouter une autre.",
diff --git a/tests/features/atlas/AtlasCard.test.tsx b/tests/features/atlas/AtlasCard.test.tsx
index 1b78c22..9711af7 100644
--- a/tests/features/atlas/AtlasCard.test.tsx
+++ b/tests/features/atlas/AtlasCard.test.tsx
@@ -98,6 +98,16 @@ describe("AtlasCard", () => {
expect(actions.parentElement).toHaveClass("flex-wrap");
});
+ it("fills its grid row so neighbouring cards stay the same height whatever their titles wrap to", async () => {
+ mount();
+ await screen.findByRole("button", { name: "Renamed repeater" });
+ const tiles = screen.getByText("Observations").closest(".grid")!;
+ expect(tiles).toHaveClass("flex-1");
+ expect(tiles.parentElement).toHaveClass("flex-1");
+ expect(tiles.closest(".rounded-lg.border.p-3\\.5")).toHaveClass("flex", "h-full", "flex-col");
+ expect(screen.getByText("-106.0 dBm")).toHaveClass("text-xl");
+ });
+
it("labels an unnamed node by its ID prefix, dimmed, as the Nodes page does", async () => {
vi.mocked(api.getNode).mockResolvedValue({ ...node, name: "" });
mount({ pin: { ...pin, name: "" } });
@@ -144,7 +154,8 @@ describe("AtlasCard", () => {
expect(summary).toHaveTextContent(/ago$/);
expect(screen.getByRole("button", { name: "Analytics" })).toHaveAttribute("aria-pressed", "true");
expect(screen.queryByText("Mean SNR")).not.toBeInTheDocument();
- expect(summary.parentElement).toHaveStyle({ minHeight: "240px" });
+ expect(summary.parentElement).toHaveStyle({ height: "240px" });
+ expect(summary.nextElementSibling).toHaveClass("overflow-y-auto");
expect(screen.getAllByRole("button", { name: "Home listener" })).toHaveLength(1);
expect(screen.getAllByText("YKF")[0]).toHaveClass("bg-primary/6");
fireEvent.click(screen.getByRole("button", { name: "Home listener" }));
diff --git a/tests/features/atlas/MyAtlasPage.test.tsx b/tests/features/atlas/MyAtlasPage.test.tsx
index ff7a855..6496a9b 100644
--- a/tests/features/atlas/MyAtlasPage.test.tsx
+++ b/tests/features/atlas/MyAtlasPage.test.tsx
@@ -65,6 +65,33 @@ describe("My Atlas", () => {
expect(api.getNodeObservations).toHaveBeenCalledWith(pin.id, { limit: 200 }, expect.any(AbortSignal));
});
+ it("keeps the search in a toolbar with the saved count chip beside the range picker, and lists matches only while typing", async () => {
+ save();
+ mount();
+ await screen.findByRole("button", { name: "Renamed repeater" });
+ const bar = screen.getByRole("toolbar", { name: "Add nodes" });
+ expect(within(bar).getByRole("textbox", { name: "Find a node" })).toBeInTheDocument();
+ expect(within(bar).getByText("1 / 12 saved")).toHaveClass("rounded-sm");
+ expect(within(bar).getByRole("button", { name: "3d" })).toBeInTheDocument();
+ expect(screen.queryByText("Matches")).not.toBeInTheDocument();
+ typeSearch("repeater");
+ expect(await screen.findByRole("button", { name: "Renamed repeater is saved" })).toBeInTheDocument();
+ expect(screen.getByText("Matches")).toBeInTheDocument();
+ });
+
+ it("keeps the toolbar to one row on a phone: short count chip, picker, no info tip", async () => {
+ vi.stubGlobal("matchMedia", (query: string) => ({ matches: /max-width/.test(query), media: query, addEventListener: () => {}, removeEventListener: () => {} }));
+ save();
+ mount();
+ await screen.findByRole("button", { name: "Renamed repeater" });
+ const bar = screen.getByRole("toolbar", { name: "Add nodes" });
+ expect(bar).not.toHaveClass("flex-wrap");
+ expect(within(bar).getByText("1/12")).toHaveClass("rounded-sm");
+ expect(within(bar).queryByText("1 / 12 saved")).not.toBeInTheDocument();
+ expect(within(bar).getByRole("button", { name: "3d" })).toBeInTheDocument();
+ expect(within(bar).queryByText(/More information/)).not.toBeInTheDocument();
+ });
+
it("shows the empty state and queries nothing with no pins", () => {
mount();
expect(screen.getByText("No nodes saved")).toBeInTheDocument();
diff --git a/tests/features/observers/ObserverSidebar.test.tsx b/tests/features/observers/ObserverSidebar.test.tsx
index a7ebe22..91ce445 100644
--- a/tests/features/observers/ObserverSidebar.test.tsx
+++ b/tests/features/observers/ObserverSidebar.test.tsx
@@ -93,6 +93,17 @@ it.each(["partial", "unavailable"] as const)("renders available traffic and zero
expect(rows[2].querySelector('[style]')).toHaveStyle({ width: "0%" });
expect(screen.queryByText(/counts unavailable/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Name order/)).not.toBeInTheDocument();
- expect(screen.getByText(/coverage is incomplete/)).toBeInTheDocument();
- expect(screen.getByText(/Available history, up to 7 days/)).toBeInTheDocument();
+ expect(screen.getByText(/More information: Counts use available history; coverage is incomplete\. Available history, up to 7 days Counts through 2026-10-0\d \d\d:00 UTC/)).toBeInTheDocument();
+ expect(screen.queryByText("Available history, up to 7 days")).not.toBeInTheDocument();
+});
+
+it("keeps the history notes in the info tip and the panel title off the phone layout", () => {
+ render(
);
+ expect(screen.getByText(/^More information: Available history, up to 7 days Counts through/)).toBeInTheDocument();
+ expect(screen.queryByText(/coverage is incomplete/)).not.toBeInTheDocument();
+ expect(screen.getByText("Observers")).toHaveClass("hidden", "md:block");
+ const controls = screen.getByRole("button", { name: "Refresh" }).parentElement!;
+ expect(controls).toContainElement(screen.getByRole("button", { name: "Name" }));
+ expect(controls).toHaveClass("ml-auto");
+ expect(controls.parentElement).toHaveClass("flex-wrap");
});
diff --git a/tests/features/stats/cards.test.tsx b/tests/features/stats/cards.test.tsx
index 831550c..cf5be95 100644
--- a/tests/features/stats/cards.test.tsx
+++ b/tests/features/stats/cards.test.tsx
@@ -11,6 +11,15 @@ it("keeps the header on one line unless a caller asks for the right slot to wrap
expect(screen.getByRole("button", { name: "Act" }).parentElement).toHaveClass("flex-wrap");
});
+it("keeps a compact tile's value small and its chart at the foot so stretched tiles stay uniform", () => {
+ const { container } = render(
);
+ const tile = container.firstElementChild!;
+ expect(tile).toHaveClass("flex", "flex-col");
+ expect(screen.getByText("-77.2 dBm")).toHaveClass("text-xl");
+ expect(screen.getByText("-77.2 dBm")).not.toHaveClass("sm:text-2xl");
+ expect(container.querySelector("svg")!.parentElement).toHaveClass("mt-auto");
+});
+
it("marks the peak hour on a sparkline when asked", () => {
const { container } = render(
);
const dot = container.querySelector("[data-peak]")!;