diff --git a/frontend/src/app/[locale]/network/page.tsx b/frontend/src/app/[locale]/network/page.tsx index 0e547c35..cfcb9fba 100644 --- a/frontend/src/app/[locale]/network/page.tsx +++ b/frontend/src/app/[locale]/network/page.tsx @@ -5,10 +5,12 @@ import dynamic from "next/dynamic"; import { Activity, Share2, Info } from "lucide-react"; import { fetchNetworkPaymentVolume, + fetchNetworkDailyActiveAccounts, fetchNetworkTransactionsPerDay, fetchNetworkNewAccounts, fetchNetworkFeeTrends, type NetworkPaymentVolumePoint, + type NetworkDailyActiveAccountsPoint, type NetworkTransactionsPerDayPoint, type NetworkNewAccountsPoint, type NetworkFeeTrendPoint, @@ -40,6 +42,23 @@ const PaymentVolumeChart = dynamic( }, ); +const DailyActiveAccountsChart = dynamic( + () => + import("@/components/charts/DailyActiveAccountsChart").then((m) => ({ + default: m.DailyActiveAccountsChart, + })), + { + ssr: false, + loading: () => ( +
+
+
+
+
+ ), + }, +); + const TransactionsPerDayChart = dynamic( () => import("@/components/charts/TransactionsPerDayChart").then((m) => ({ @@ -99,6 +118,10 @@ export default function NetworkPage() { [], ); const [volumeLoading, setVolumeLoading] = useState(true); + const [daaPoints, setDaaPoints] = useState( + [], + ); + const [daaLoading, setDaaLoading] = useState(true); const [txPerDayPoints, setTxPerDayPoints] = useState< NetworkTransactionsPerDayPoint[] >([]); @@ -144,6 +167,22 @@ export default function NetworkPage() { void loadPaymentVolume(); }, []); + useEffect(() => { + async function loadDailyActiveAccounts() { + setDaaLoading(true); + try { + const result = await fetchNetworkDailyActiveAccounts(30); + setDaaPoints(result.points); + } catch (err) { + logger.error("Failed to load daily active accounts panel:", err); + setDaaPoints([]); + } finally { + setDaaLoading(false); + } + } + void loadDailyActiveAccounts(); + }, []); + useEffect(() => { async function loadTransactionsPerDay() { setTxPerDayLoading(true); @@ -236,6 +275,7 @@ 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 latest = chartData[chartData.length - 1]?.count ?? 0; + const peak = chartData.length + ? Math.max(...chartData.map((d) => d.count)) + : 0; + const average = chartData.length + ? Math.round(chartData.reduce((sum, d) => sum + d.count, 0) / chartData.length) + : 0; + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + if (chartData.length === 0) { + return ( +
+
+ Network // Daily Active Accounts +
+

+ Daily Active Accounts +

+

+ No active-account series yet. Data appears once{" "} + + /api/v1/network/daily-active-accounts + {" "} + returns daily unique account counts. +

+
+ ); + } + + return ( +
+
+
+
+ Network // Daily Active Accounts +
+

+ Daily Active Accounts +

+

+ Unique accounts transacting per day +

+
+ +
+ +
+
+

+ Latest day +

+

+ {formatCompact(latest)} +

+
+
+

+ Period average +

+

+ {formatCompact(average)} +

+
+
+

+ Peak day +

+

+ {formatCompact(peak)} +

+
+
+ +
+ + + + + + + + + + + + [ + formatCompact(typeof value === "number" ? value : Number(value)), + "Active accounts", + ]} + /> + + + +
+
+ ); +} diff --git a/frontend/src/lib/network-api.ts b/frontend/src/lib/network-api.ts index 0506c781..92b5c671 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/daily-active-accounts → {date, count} (backend#15) * 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) @@ -25,6 +26,16 @@ export interface NetworkPaymentVolumeResponse { unit: "usd"; } +export interface NetworkDailyActiveAccountsPoint { + date: string; + /** Count of unique accounts that transacted that day. */ + count: number; +} + +export interface NetworkDailyActiveAccountsResponse { + points: NetworkDailyActiveAccountsPoint[]; +} + export interface NetworkTransactionsPerDayPoint { date: string; /** Total transaction count for the day. */ @@ -123,6 +134,42 @@ export async function fetchNetworkPaymentVolume( } } +function normalizeDailyActiveAccountsPoints( + raw: unknown, +): NetworkDailyActiveAccountsPoint[] { + 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; + active_accounts?: number; + daily_active_accounts?: number; + }; + const date = item.date ?? item.day ?? item.timestamp; + const count = + item.count ?? item.active_accounts ?? item.daily_active_accounts; + if (!date || typeof count !== "number" || Number.isNaN(count)) + return null; + return { date, count }; + }) + .filter( + (point): point is NetworkDailyActiveAccountsPoint => point != null, + ) + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); +} + function normalizeTransactionsPerDayPoints( raw: unknown, ): NetworkTransactionsPerDayPoint[] { @@ -219,6 +266,30 @@ function normalizeFeeTrendPoints(raw: unknown): NetworkFeeTrendPoint[] { .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); } +/** + * Daily active accounts time series (backend#15). + * 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 fetchNetworkDailyActiveAccounts( + days = 30, +): Promise { + const url = `${API_BASE}/api/v1/network/daily-active-accounts?days=${days}`; + try { + const data = await fetchJson(url); + return { points: normalizeDailyActiveAccountsPoints(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 daily active accounts:", error); + } + return { points: [] }; + } +} + /** * Daily transaction count time series (backend#16). * Returns an empty series when the backend is unavailable so the panel