diff --git a/frontend/src/app/[locale]/network/page.tsx b/frontend/src/app/[locale]/network/page.tsx
index a5fe4ac1..0e547c35 100644
--- a/frontend/src/app/[locale]/network/page.tsx
+++ b/frontend/src/app/[locale]/network/page.tsx
@@ -5,9 +5,11 @@ import dynamic from "next/dynamic";
import { Activity, Share2, Info } from "lucide-react";
import {
fetchNetworkPaymentVolume,
+ fetchNetworkTransactionsPerDay,
fetchNetworkNewAccounts,
fetchNetworkFeeTrends,
type NetworkPaymentVolumePoint,
+ type NetworkTransactionsPerDayPoint,
type NetworkNewAccountsPoint,
type NetworkFeeTrendPoint,
} from "@/lib/network-api";
@@ -38,6 +40,23 @@ const PaymentVolumeChart = dynamic(
},
);
+const TransactionsPerDayChart = dynamic(
+ () =>
+ import("@/components/charts/TransactionsPerDayChart").then((m) => ({
+ default: m.TransactionsPerDayChart,
+ })),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+
const NewAccountsChart = dynamic(
() =>
import("@/components/charts/NewAccountsChart").then((m) => ({
@@ -80,6 +99,10 @@ export default function NetworkPage() {
[],
);
const [volumeLoading, setVolumeLoading] = useState(true);
+ const [txPerDayPoints, setTxPerDayPoints] = useState<
+ NetworkTransactionsPerDayPoint[]
+ >([]);
+ const [txPerDayLoading, setTxPerDayLoading] = useState(true);
const [newAccountsPoints, setNewAccountsPoints] = useState<
NetworkNewAccountsPoint[]
>([]);
@@ -121,6 +144,22 @@ export default function NetworkPage() {
void loadPaymentVolume();
}, []);
+ useEffect(() => {
+ async function loadTransactionsPerDay() {
+ setTxPerDayLoading(true);
+ try {
+ const result = await fetchNetworkTransactionsPerDay(30);
+ setTxPerDayPoints(result.points);
+ } catch (err) {
+ logger.error("Failed to load transactions-per-day panel:", err);
+ setTxPerDayPoints([]);
+ } finally {
+ setTxPerDayLoading(false);
+ }
+ }
+ void loadTransactionsPerDay();
+ }, []);
+
useEffect(() => {
async function loadNewAccounts() {
setNewAccountsLoading(true);
@@ -197,6 +236,10 @@ export default function NetworkPage() {
{/* Network metrics panels */}
+
(null);
+
+ const chartData = data.map((point) => ({
+ label: new Date(point.date).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ }),
+ date: point.date,
+ count: point.count,
+ }));
+
+ const total = chartData.reduce((sum, d) => sum + d.count, 0);
+ const latest = chartData[chartData.length - 1]?.count ?? 0;
+ const peak = chartData.length
+ ? Math.max(...chartData.map((d) => d.count))
+ : 0;
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (chartData.length === 0) {
+ return (
+
+
+ Network // Transactions
+
+
+ Transactions Per Day
+
+
+ No transaction series yet. Data appears once{" "}
+
+ /api/v1/network/transactions-per-day
+ {" "}
+ returns daily transaction counts.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Network // Transactions
+
+
+ Transactions Per Day
+
+
+ Total ledger transactions submitted per day
+
+
+
+
+
+
+
+
+ Latest day
+
+
+ {formatCompact(latest)}
+
+
+
+
+ Period total
+
+
+ {formatCompact(total)}
+
+
+
+
+ Peak day
+
+
+ {formatCompact(peak)}
+
+
+
+
+
+
+
+
+
+
+ [
+ formatCompact(typeof value === "number" ? value : Number(value)),
+ "Transactions",
+ ]}
+ />
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/lib/network-api.ts b/frontend/src/lib/network-api.ts
index 46aae38c..0506c781 100644
--- a/frontend/src/lib/network-api.ts
+++ b/frontend/src/lib/network-api.ts
@@ -1,6 +1,7 @@
/**
* Network dashboard API client.
* Backed by Stellar-Insightss/backend#15–#19:
+ * GET /api/v1/network/transactions-per-day → {date, count} (backend#16)
* GET /api/v1/network/new-accounts → {date, count} (backend#18)
* GET /api/v1/network/fee-trends → {date, avg_fee} (backend#19)
* GET /api/v1/network/payment-volume → {date, volume} (backend#17)
@@ -24,6 +25,16 @@ export interface NetworkPaymentVolumeResponse {
unit: "usd";
}
+export interface NetworkTransactionsPerDayPoint {
+ date: string;
+ /** Total transaction count for the day. */
+ count: number;
+}
+
+export interface NetworkTransactionsPerDayResponse {
+ points: NetworkTransactionsPerDayPoint[];
+}
+
export interface NetworkNewAccountsPoint {
date: string;
/** New accounts created that day. */
@@ -112,6 +123,39 @@ export async function fetchNetworkPaymentVolume(
}
}
+function normalizeTransactionsPerDayPoints(
+ raw: unknown,
+): NetworkTransactionsPerDayPoint[] {
+ const rows = Array.isArray(raw)
+ ? raw
+ : Array.isArray((raw as { points?: unknown })?.points)
+ ? (raw as { points: unknown[] }).points
+ : Array.isArray((raw as { series?: unknown })?.series)
+ ? (raw as { series: unknown[] }).series
+ : Array.isArray((raw as { data?: unknown })?.data)
+ ? (raw as { data: unknown[] }).data
+ : [];
+
+ return rows
+ .map((row) => {
+ const item = row as {
+ date?: string;
+ day?: string;
+ timestamp?: string;
+ count?: number;
+ transactions?: number;
+ tx_count?: number;
+ };
+ const date = item.date ?? item.day ?? item.timestamp;
+ const count = item.count ?? item.transactions ?? item.tx_count;
+ if (!date || typeof count !== "number" || Number.isNaN(count))
+ return null;
+ return { date, count };
+ })
+ .filter((point): point is NetworkTransactionsPerDayPoint => point != null)
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
+}
+
function normalizeNewAccountsPoints(raw: unknown): NetworkNewAccountsPoint[] {
const rows = Array.isArray(raw)
? raw
@@ -175,6 +219,30 @@ function normalizeFeeTrendPoints(raw: unknown): NetworkFeeTrendPoint[] {
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
}
+/**
+ * Daily transaction count time series (backend#16).
+ * Returns an empty series when the backend is unavailable so the panel
+ * can show an empty state without failing the whole /network page.
+ */
+export async function fetchNetworkTransactionsPerDay(
+ days = 30,
+): Promise {
+ const url = `${API_BASE}/api/v1/network/transactions-per-day?days=${days}`;
+ try {
+ const data = await fetchJson(url);
+ return { points: normalizeTransactionsPerDayPoints(data) };
+ } catch (error) {
+ const isNetworkError =
+ error instanceof TypeError &&
+ (error.message.includes("Failed to fetch") ||
+ error.message.includes("Network request failed"));
+ if (!isNetworkError) {
+ logger.error("Failed to fetch network transactions-per-day:", error);
+ }
+ return { points: [] };
+ }
+}
+
/**
* New-accounts-per-day time series (backend#18).
* Returns an empty series when the backend is unavailable so the panel