diff --git a/src/meshcore_hub/web/static/js/spa-react/pages/Routes.test.tsx b/src/meshcore_hub/web/static/js/spa-react/pages/Routes.test.tsx
index 0c171da..b97c844 100644
--- a/src/meshcore_hub/web/static/js/spa-react/pages/Routes.test.tsx
+++ b/src/meshcore_hub/web/static/js/spa-react/pages/Routes.test.tsx
@@ -74,6 +74,52 @@ describe("Routes", () => {
).not.toBeNull();
});
+ it("renders matched/threshold/degraded stat with dash separator", async () => {
+ const data = {
+ items: [
+ {
+ ...ROUTES.items[0],
+ route_result: {
+ quality: "clear",
+ state: "healthy",
+ matched_count: 3,
+ threshold: 5,
+ effective_clear: 2,
+ },
+ },
+ ],
+ };
+ vi.spyOn(api, "apiGet").mockImplementation(async (path) => {
+ if (path === "/api/v1/routes") return data;
+ if (path.match(/\/api\/v1\/routes\/[^/]+$/)) return ROUTE_DETAIL;
+ if (path.includes("/history")) return ROUTE_HISTORY;
+ throw new Error(`Unexpected: ${path}`);
+ });
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByText("3/5-2")).toBeInTheDocument();
+ });
+ });
+
+ it("shows Now label for the last history chart entry", async () => {
+ const historyWithData = {
+ data: [
+ { date: "2026-07-20", matched: 0, total: 0 },
+ { date: "2026-07-21", matched: 1, total: 1 },
+ ],
+ };
+ vi.spyOn(api, "apiGet").mockImplementation(async (path) => {
+ if (path === "/api/v1/routes") return ROUTES;
+ if (path.match(/\/api\/v1\/routes\/[^/]+$/)) return ROUTE_DETAIL;
+ if (path.includes("/history")) return historyWithData;
+ throw new Error(`Unexpected: ${path}`);
+ });
+ renderWithProviders();
+ await waitFor(() => {
+ expect(screen.getByText("common.now")).toBeInTheDocument();
+ });
+ });
+
it("shows an error on fetch failure", async () => {
vi.spyOn(api, "apiGet").mockRejectedValue(new Error("routes error"));
renderWithProviders();
diff --git a/src/meshcore_hub/web/static/js/spa-react/pages/Routes.tsx b/src/meshcore_hub/web/static/js/spa-react/pages/Routes.tsx
index e44187b..4a8fed4 100644
--- a/src/meshcore_hub/web/static/js/spa-react/pages/Routes.tsx
+++ b/src/meshcore_hub/web/static/js/spa-react/pages/Routes.tsx
@@ -310,7 +310,7 @@ function StatsRow({ route }: { route: RouteItem }) {
{matched}/{threshold}
- {"\u2192"}
+ {"-"}
{degraded}
@@ -473,7 +473,7 @@ function DetailContent({
{historyData.map((d, i) => (
{i === historyData.length - 1
- ? t("routes.last_n_hours", { n: route.window_hours })
+ ? t("common.now")
: new Date(`${d.date}T00:00:00`).toLocaleDateString(
undefined,
{ day: "2-digit", month: "2-digit" },
diff --git a/src/meshcore_hub/web/static/locales/en.json b/src/meshcore_hub/web/static/locales/en.json
index deb2d85..3a0aab6 100644
--- a/src/meshcore_hub/web/static/locales/en.json
+++ b/src/meshcore_hub/web/static/locales/en.json
@@ -117,7 +117,8 @@
"unnamed_node": "Unnamed Node",
"validation_invalid_number": "Value must be a valid number",
"validation_invalid_boolean": "Value must be true, false, yes, no, 1, or 0",
- "sort_by": "Sort by"
+ "sort_by": "Sort by",
+ "now": "Now"
},
"links": {
"website": "Website",
@@ -359,7 +360,6 @@
"quality_no_coverage": "No Coverage",
"quality_unknown": "Unknown",
"recent_packets": "Recent Packets",
- "last_n_hours": "Last {{n}}h",
"min_nodes_error": "At least 2 path nodes are required.",
"other_routes": "Other routes",
"filter_mine": "Show only my routes"
diff --git a/src/meshcore_hub/web/static/locales/nl.json b/src/meshcore_hub/web/static/locales/nl.json
index 014b915..a041a19 100644
--- a/src/meshcore_hub/web/static/locales/nl.json
+++ b/src/meshcore_hub/web/static/locales/nl.json
@@ -108,7 +108,8 @@
"filter_operator_label": "Operator",
"filter_observer_label": "Waarnemer",
"filter_observer_enable": "Klik om deze waarnemer te tonen",
- "filter_observer_disable": "Klik om deze waarnemer te verbergen"
+ "filter_observer_disable": "Klik om deze waarnemer te verbergen",
+ "now": "Nu"
},
"links": {
"website": "Website",
@@ -282,7 +283,6 @@
"quality_no_coverage": "Geen dekking",
"quality_unknown": "Onbekend",
"recent_packets": "Recente packets",
- "last_n_hours": "Laatste {{n}}u",
"min_nodes_error": "Minimaal 2 padknooppunten zijn vereist.",
"other_routes": "Overige routes",
"filter_mine": "Toon alleen mijn routes"