commit bc009847f578335db9ff972263a9a56a69c2a618
parent 633cebb5adfc15ad00b8e39b1eb787f979fc79f4
Author: Florian Dold <dold@taler.net>
Date: Mon, 31 Aug 2026 19:14:33 +0200
auditor web UI: link dashboard rows to findings
Diffstat:
11 files changed, 212 insertions(+), 48 deletions(-)
diff --git a/packages/taler-auditor-webui/src/InstanceRoutes.tsx b/packages/taler-auditor-webui/src/InstanceRoutes.tsx
@@ -19,45 +19,10 @@ import NotFoundPage from "./paths/notfound/index.js";
import OperationsDashboard from "./paths/operations/index.js";
import SecurityDashboard from "./paths/security/index.js";
import { Settings } from "./paths/settings/index.js";
+import { Paths } from "./routing/monitoringRoutes.js";
import type { HttpError } from "./utils/http.js";
import type { AuditorBackend } from "./declaration.js";
-export enum Paths {
- error = "/error",
- settings = "/settings",
- key_figures = "/key-figures",
- critical_errors = "/critical-errors",
- operating_status = "/operating-status",
- detail_view = "/detail-view",
- amount_arithmethic_inconsistency_list = "/amount-arithmetic-inconsistencies",
- bad_sig_losses_list = "/bad-sig-losses",
- balance_list = "/balance",
- closure_lag_list = "/closure-lags",
- coin_inconsistency_list = "/coin-inconsistencies",
- denomination_key_validity_withdraw_inconsistency_list = "/denomination-key-validity-withdraw-inconsistencies",
- denomination_without_sig_list = "/denominations-without-sig",
- deposit_confirmation_list = "/deposit-confirmations",
- deposit_confirmation_update = "/deposit-confirmation/:rowid/update",
- emergency_list = "/emergencies",
- emergency_by_count_list = "/emergencies-by-count",
- exchange_signkey_list = "/exchange-sign-keys",
- fee_time_inconsistency_list = "/fee-time-inconsistencies",
- historic_denomination_revenue_list = "/historic-denomination-revenues",
- misattribution_in_inconsistency_list = "/misattribution-in-inconsistencies",
- progress_list = "/progress",
- purse_not_closed_inconsistency_list = "/purse-not-closed-inconsistencies",
- purse_list = "/purses",
- reserve_balance_insufficient_inconsistency_list = "/reserve-balance-insufficient-inconsistencies",
- reserve_balance_summary_wrong_inconsistency_list = "/reserve-balance-summary-wrong-inconsistencies",
- reserve_in_inconsistency_list = "/reserve-in-inconsistencies",
- reserve_not_closed_inconsistency_list = "/reserve-not-closed-inconsistencies",
- reserves_list = "/reserves",
- row_inconsistency_list = "/row-inconsistencies",
- row_minor_inconsistency_list = "/row-minor-inconsistencies",
- wire_format_inconsistency_list = "/wire-format-inconsistencies",
- wire_out_inconsistency_list = "/wire-out-inconsistencies",
-}
-
interface EntityRoute {
title: string;
endpoint: string;
@@ -122,6 +87,11 @@ const entityRoutes: Partial<Record<Paths, EntityRoute>> = {
endpoint: "historic-denomination-revenues",
entity: null,
},
+ [Paths.historic_reserve_summary_list]: {
+ title: "Historic reserve summaries",
+ endpoint: "historic-reserve-summaries",
+ entity: null,
+ },
[Paths.misattribution_in_inconsistency_list]: {
title: "Misattribution in inconsistencies",
endpoint: "misattribution-in-inconsistencies",
diff --git a/packages/taler-auditor-webui/src/components/DashboardTableRow.tsx b/packages/taler-auditor-webui/src/components/DashboardTableRow.tsx
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2026 Taler Systems S.A.
+ */
+
+import type { ComponentChildren, JSX, VNode } from "preact";
+import { useLocation } from "wouter-preact";
+import { pathForMonitoringEndpoint } from "../routing/monitoringRoutes.js";
+
+interface Props {
+ endpoint: string;
+ label: string;
+ children: ComponentChildren;
+}
+
+export function DashboardTableRow({ endpoint, label, children }: Props): VNode {
+ const [, setLocation] = useLocation();
+ const path = pathForMonitoringEndpoint(endpoint);
+
+ if (!path) {
+ return <tr>{children}</tr>;
+ }
+
+ const onKeyDown = (event: JSX.TargetedKeyboardEvent<HTMLTableRowElement>): void => {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ event.preventDefault();
+ setLocation(path);
+ };
+
+ return (
+ <tr
+ aria-label={`View ${label}`}
+ class="cursor-pointer transition hover:bg-blue-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-taler-brand"
+ onClick={() => setLocation(path)}
+ onKeyDown={onKeyDown}
+ role="link"
+ tabIndex={0}
+ >
+ {children}
+ </tr>
+ );
+}
diff --git a/packages/taler-auditor-webui/src/components/menu/SideBar.tsx b/packages/taler-auditor-webui/src/components/menu/SideBar.tsx
@@ -7,7 +7,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import type { VNode } from "preact";
import { Link, useLocation } from "wouter-preact";
import { useConfigContext } from "../../context/config.js";
-import { Paths } from "../../InstanceRoutes.js";
+import { Paths } from "../../routing/monitoringRoutes.js";
interface Props {
mobile?: boolean;
diff --git a/packages/taler-auditor-webui/src/paths/default/index.tsx b/packages/taler-auditor-webui/src/paths/default/index.tsx
@@ -33,7 +33,7 @@ import {
} from "../../context/entity.js";
import { AuditorBackend } from "../../declaration.js";
import { useEntityList, useEntityAPI } from "../../hooks/entity.js";
-import { Paths } from "../../InstanceRoutes.js";
+import { Paths } from "../../routing/monitoringRoutes.js";
import { Notification } from "../../utils/types.js";
import { ErrorType, HttpError } from "../../utils/http.js";
import { CardTable } from "./Table.js";
diff --git a/packages/taler-auditor-webui/src/paths/details/ListPage.tsx b/packages/taler-auditor-webui/src/paths/details/ListPage.tsx
@@ -5,12 +5,13 @@
import type { VNode } from "preact";
import { Link } from "wouter-preact";
-import { Paths } from "../../InstanceRoutes.js";
+import { Paths } from "../../routing/monitoringRoutes.js";
export function ListPage(): VNode {
const entries = [
[Paths.amount_arithmethic_inconsistency_list, "Amount arithmetic inconsistencies"],
[Paths.bad_sig_losses_list, "Bad signature losses"],
+ [Paths.balance_list, "Balances"],
[Paths.closure_lag_list, "Closure lags"],
[Paths.coin_inconsistency_list, "Coin inconsistencies"],
[Paths.denomination_key_validity_withdraw_inconsistency_list, "Denomination key validity at withdrawal"],
@@ -19,6 +20,8 @@ export function ListPage(): VNode {
[Paths.emergency_list, "Emergencies"],
[Paths.emergency_by_count_list, "Emergencies by count"],
[Paths.fee_time_inconsistency_list, "Fee time inconsistencies"],
+ [Paths.historic_denomination_revenue_list, "Historic denomination revenues"],
+ [Paths.historic_reserve_summary_list, "Historic reserve summaries"],
[Paths.misattribution_in_inconsistency_list, "Misattribution-in inconsistencies"],
[Paths.purse_not_closed_inconsistency_list, "Purses not closed"],
[Paths.purse_list, "Purses"],
diff --git a/packages/taler-auditor-webui/src/paths/finance/ListPage.tsx b/packages/taler-auditor-webui/src/paths/finance/ListPage.tsx
@@ -4,6 +4,7 @@
*/
import type { VNode } from "preact";
+import { DashboardTableRow } from "../../components/DashboardTableRow.js";
const coinBalances = new Set([
"Total recoup loss",
@@ -50,12 +51,16 @@ function BalanceTable({
<thead><tr><th>Balance</th><th class="text-right">Value</th></tr></thead>
<tbody>
{balances.map((balance) => (
- <tr key={balance.balance_key}>
+ <DashboardTableRow
+ endpoint="balances"
+ key={balance.balance_key}
+ label={balanceName(balance.balance_key)}
+ >
<td>{balanceName(balance.balance_key)}</td>
<td class="whitespace-nowrap text-right font-mono">
{balance.balance_value.replace(":", " ")}
</td>
- </tr>
+ </DashboardTableRow>
))}
</tbody>
</table>
@@ -90,12 +95,16 @@ export function ListPage(data: any): VNode {
{findings.map((finding) => {
const count = finding.records.length;
return (
- <tr key={finding.endpoint}>
+ <DashboardTableRow
+ endpoint={finding.endpoint}
+ key={finding.endpoint}
+ label={finding.label}
+ >
<td>{finding.label}</td>
<td class={`text-right font-bold ${count === 0 ? "text-taler-success" : "text-taler-error"}`}>
{count}
</td>
- </tr>
+ </DashboardTableRow>
);
})}
</tbody>
diff --git a/packages/taler-auditor-webui/src/paths/login/index.tsx b/packages/taler-auditor-webui/src/paths/login/index.tsx
@@ -5,7 +5,7 @@
import type { VNode } from "preact";
import { Redirect } from "wouter-preact";
-import { Paths } from "../../InstanceRoutes.js";
+import { Paths } from "../../routing/monitoringRoutes.js";
export function LoginPage(): VNode {
return <Redirect to={Paths.key_figures} replace />;
diff --git a/packages/taler-auditor-webui/src/paths/operations/ListPage.tsx b/packages/taler-auditor-webui/src/paths/operations/ListPage.tsx
@@ -4,6 +4,7 @@
*/
import type { VNode } from "preact";
+import { DashboardTableRow } from "../../components/DashboardTableRow.js";
export function ListPage(data: any): VNode {
const summaries = data.data.data[0] as Array<{
@@ -24,12 +25,16 @@ export function ListPage(data: any): VNode {
{summaries.map((finding) => {
const count = finding.records.length;
return (
- <tr key={finding.endpoint}>
+ <DashboardTableRow
+ endpoint={finding.endpoint}
+ key={finding.endpoint}
+ label={finding.label}
+ >
<td>{finding.label}</td>
<td class={`text-right font-bold ${count === 0 ? "text-taler-success" : "text-taler-error"}`}>
{count}
</td>
- </tr>
+ </DashboardTableRow>
);
})}
</tbody>
diff --git a/packages/taler-auditor-webui/src/paths/security/ListPage.tsx b/packages/taler-auditor-webui/src/paths/security/ListPage.tsx
@@ -4,6 +4,7 @@
*/
import type { VNode } from "preact";
+import { DashboardTableRow } from "../../components/DashboardTableRow.js";
export function ListPage(data: any): VNode {
const summaries = data.data.data[0] as Array<{
@@ -24,12 +25,16 @@ export function ListPage(data: any): VNode {
{summaries.map((finding) => {
const count = finding.records.length;
return (
- <tr key={finding.endpoint}>
+ <DashboardTableRow
+ endpoint={finding.endpoint}
+ key={finding.endpoint}
+ label={finding.label}
+ >
<td>{finding.label}</td>
<td class={`text-right font-bold ${count === 0 ? "text-taler-success" : "text-taler-error"}`}>
{count}
</td>
- </tr>
+ </DashboardTableRow>
);
})}
</tbody>
diff --git a/packages/taler-auditor-webui/src/routing/monitoringRoutes.test.ts b/packages/taler-auditor-webui/src/routing/monitoringRoutes.test.ts
@@ -0,0 +1,52 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+ */
+
+import assert from "node:assert";
+import test from "node:test";
+import { pathForMonitoringEndpoint, Paths } from "./monitoringRoutes.js";
+
+test("dashboard monitoring endpoints have detail routes", () => {
+ const dashboardEndpoints = [
+ "misattribution-in-inconsistencies",
+ "coin-inconsistencies",
+ "reserve-in-inconsistencies",
+ "bad-sig-losses",
+ "balances",
+ "amount-arithmetic-inconsistencies",
+ "wire-format-inconsistencies",
+ "wire-out-inconsistencies",
+ "reserve-balance-summary-wrong-inconsistencies",
+ "fee-time-inconsistencies",
+ "emergencies",
+ "emergencies-by-count",
+ "reserve-balance-insufficient-inconsistencies",
+ "row-inconsistencies",
+ "purse-not-closed-inconsistencies",
+ "reserve-not-closed-inconsistencies",
+ "denominations-without-sigs",
+ "deposit-confirmations",
+ "denomination-key-validity-withdraw-inconsistencies",
+ "closure-lags",
+ "row-minor-inconsistencies",
+ "historic-denomination-revenues",
+ "historic-reserve-summaries",
+ ];
+
+ for (const endpoint of dashboardEndpoints) {
+ assert.ok(pathForMonitoringEndpoint(endpoint), `missing route for ${endpoint}`);
+ }
+});
+
+test("monitoring endpoints with non-matching paths resolve correctly", () => {
+ assert.equal(pathForMonitoringEndpoint("balances"), Paths.balance_list);
+ assert.equal(
+ pathForMonitoringEndpoint("denominations-without-sigs"),
+ Paths.denomination_without_sig_list,
+ );
+ assert.equal(
+ pathForMonitoringEndpoint("historic-reserve-summaries"),
+ Paths.historic_reserve_summary_list,
+ );
+});
diff --git a/packages/taler-auditor-webui/src/routing/monitoringRoutes.ts b/packages/taler-auditor-webui/src/routing/monitoringRoutes.ts
@@ -0,0 +1,78 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2026 Taler Systems S.A.
+ */
+
+export enum Paths {
+ error = "/error",
+ settings = "/settings",
+ key_figures = "/key-figures",
+ critical_errors = "/critical-errors",
+ operating_status = "/operating-status",
+ detail_view = "/detail-view",
+ amount_arithmethic_inconsistency_list = "/amount-arithmetic-inconsistencies",
+ bad_sig_losses_list = "/bad-sig-losses",
+ balance_list = "/balance",
+ closure_lag_list = "/closure-lags",
+ coin_inconsistency_list = "/coin-inconsistencies",
+ denomination_key_validity_withdraw_inconsistency_list = "/denomination-key-validity-withdraw-inconsistencies",
+ denomination_without_sig_list = "/denominations-without-sig",
+ deposit_confirmation_list = "/deposit-confirmations",
+ deposit_confirmation_update = "/deposit-confirmation/:rowid/update",
+ emergency_list = "/emergencies",
+ emergency_by_count_list = "/emergencies-by-count",
+ exchange_signkey_list = "/exchange-sign-keys",
+ fee_time_inconsistency_list = "/fee-time-inconsistencies",
+ historic_denomination_revenue_list = "/historic-denomination-revenues",
+ historic_reserve_summary_list = "/historic-reserve-summaries",
+ misattribution_in_inconsistency_list = "/misattribution-in-inconsistencies",
+ progress_list = "/progress",
+ purse_not_closed_inconsistency_list = "/purse-not-closed-inconsistencies",
+ purse_list = "/purses",
+ reserve_balance_insufficient_inconsistency_list = "/reserve-balance-insufficient-inconsistencies",
+ reserve_balance_summary_wrong_inconsistency_list = "/reserve-balance-summary-wrong-inconsistencies",
+ reserve_in_inconsistency_list = "/reserve-in-inconsistencies",
+ reserve_not_closed_inconsistency_list = "/reserve-not-closed-inconsistencies",
+ reserves_list = "/reserves",
+ row_inconsistency_list = "/row-inconsistencies",
+ row_minor_inconsistency_list = "/row-minor-inconsistencies",
+ wire_format_inconsistency_list = "/wire-format-inconsistencies",
+ wire_out_inconsistency_list = "/wire-out-inconsistencies",
+}
+
+const monitoringPathByEndpoint: Readonly<Record<string, Paths>> = {
+ "amount-arithmetic-inconsistencies": Paths.amount_arithmethic_inconsistency_list,
+ "bad-sig-losses": Paths.bad_sig_losses_list,
+ balances: Paths.balance_list,
+ "closure-lags": Paths.closure_lag_list,
+ "coin-inconsistencies": Paths.coin_inconsistency_list,
+ "denomination-key-validity-withdraw-inconsistencies":
+ Paths.denomination_key_validity_withdraw_inconsistency_list,
+ "denominations-without-sigs": Paths.denomination_without_sig_list,
+ "deposit-confirmations": Paths.deposit_confirmation_list,
+ emergencies: Paths.emergency_list,
+ "emergencies-by-count": Paths.emergency_by_count_list,
+ "exchange-sign-keys": Paths.exchange_signkey_list,
+ "fee-time-inconsistencies": Paths.fee_time_inconsistency_list,
+ "historic-denomination-revenues": Paths.historic_denomination_revenue_list,
+ "historic-reserve-summaries": Paths.historic_reserve_summary_list,
+ "misattribution-in-inconsistencies": Paths.misattribution_in_inconsistency_list,
+ progress: Paths.progress_list,
+ "purse-not-closed-inconsistencies": Paths.purse_not_closed_inconsistency_list,
+ purses: Paths.purse_list,
+ "reserve-balance-insufficient-inconsistencies":
+ Paths.reserve_balance_insufficient_inconsistency_list,
+ "reserve-balance-summary-wrong-inconsistencies":
+ Paths.reserve_balance_summary_wrong_inconsistency_list,
+ "reserve-in-inconsistencies": Paths.reserve_in_inconsistency_list,
+ "reserve-not-closed-inconsistencies": Paths.reserve_not_closed_inconsistency_list,
+ reserves: Paths.reserves_list,
+ "row-inconsistencies": Paths.row_inconsistency_list,
+ "row-minor-inconsistencies": Paths.row_minor_inconsistency_list,
+ "wire-format-inconsistencies": Paths.wire_format_inconsistency_list,
+ "wire-out-inconsistencies": Paths.wire_out_inconsistency_list,
+};
+
+export function pathForMonitoringEndpoint(endpoint: string): Paths | undefined {
+ return monitoringPathByEndpoint[endpoint];
+}