diff --git a/frontend/src/app/[locale]/network/page.tsx b/frontend/src/app/[locale]/network/page.tsx
index aa0c1886..abd7bb8f 100644
--- a/frontend/src/app/[locale]/network/page.tsx
+++ b/frontend/src/app/[locale]/network/page.tsx
@@ -5,7 +5,9 @@ import dynamic from "next/dynamic";
import { Activity, Share2, Info } from "lucide-react";
import {
fetchNetworkPaymentVolume,
+ fetchNetworkDailyActiveAccounts,
type NetworkPaymentVolumePoint,
+ type NetworkDailyActiveAccountsPoint,
} from "@/lib/network-api";
import { logger } from "@/lib/logger";
@@ -34,6 +36,23 @@ const PaymentVolumeChart = dynamic(
},
);
+const DailyActiveAccountsChart = dynamic(
+ () =>
+ import("@/components/charts/DailyActiveAccountsChart").then((m) => ({
+ default: m.DailyActiveAccountsChart,
+ })),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+
export default function NetworkPage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
@@ -42,6 +61,10 @@ export default function NetworkPage() {
[],
);
const [volumeLoading, setVolumeLoading] = useState(true);
+ const [daaPoints, setDaaPoints] = useState(
+ [],
+ );
+ const [daaLoading, setDaaLoading] = useState(true);
useEffect(() => {
async function fetchGraphData() {
@@ -75,6 +98,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();
+ }, []);
+
return (
{/* Header Area */}
@@ -118,7 +157,10 @@ export default function NetworkPage() {
{/* Network metrics panels */}
-
+
{/* Topology */}
diff --git a/frontend/src/components/charts/DailyActiveAccountsChart.tsx b/frontend/src/components/charts/DailyActiveAccountsChart.tsx
new file mode 100644
index 00000000..f1d25f7e
--- /dev/null
+++ b/frontend/src/components/charts/DailyActiveAccountsChart.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import { useRef } from "react";
+import {
+ Area,
+ AreaChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { ChartExportButton } from "./ChartExportButton";
+import { getTooltipContentStyle } from "@/lib/chart-utils";
+import type { NetworkDailyActiveAccountsPoint } from "@/lib/network-api";
+
+interface DailyActiveAccountsChartProps {
+ data: NetworkDailyActiveAccountsPoint[];
+ loading?: boolean;
+}
+
+function formatCompact(value: number): string {
+ return new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ maximumFractionDigits: 1,
+ }).format(value);
+}
+
+export function DailyActiveAccountsChart({
+ data,
+ loading = false,
+}: DailyActiveAccountsChartProps) {
+ const chartRef = useRef
(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 dc77807f..187ce7fb 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/payment-volume → {date, volume} (backend#17)
*
* Volume unit matches NetworkStats.volume_24h (USD-equivalent) —
@@ -22,6 +23,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[];
+}
+
async function fetchJson(url: string): Promise {
const response = await fetch(url, {
method: "GET",
@@ -89,3 +100,63 @@ export async function fetchNetworkPaymentVolume(
return { points: [], unit: "usd" };
}
}
+
+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());
+}
+
+/**
+ * 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: [] };
+ }
+}