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