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