diff --git a/frontend/src/app/[locale]/network/page.tsx b/frontend/src/app/[locale]/network/page.tsx
index aa0c1886..4ca462cb 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,
+ fetchNetworkNewAccounts,
type NetworkPaymentVolumePoint,
+ type NetworkNewAccountsPoint,
} from "@/lib/network-api";
import { logger } from "@/lib/logger";
@@ -34,6 +36,23 @@ const PaymentVolumeChart = dynamic(
},
);
+const NewAccountsChart = dynamic(
+ () =>
+ import("@/components/charts/NewAccountsChart").then((m) => ({
+ default: m.NewAccountsChart,
+ })),
+ {
+ 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 [newAccountsPoints, setNewAccountsPoints] = useState<
+ NetworkNewAccountsPoint[]
+ >([]);
+ const [newAccountsLoading, setNewAccountsLoading] = useState(true);
useEffect(() => {
async function fetchGraphData() {
@@ -75,6 +98,22 @@ export default function NetworkPage() {
void loadPaymentVolume();
}, []);
+ useEffect(() => {
+ async function loadNewAccounts() {
+ setNewAccountsLoading(true);
+ try {
+ const result = await fetchNetworkNewAccounts(30);
+ setNewAccountsPoints(result.points);
+ } catch (err) {
+ logger.error("Failed to load new accounts panel:", err);
+ setNewAccountsPoints([]);
+ } finally {
+ setNewAccountsLoading(false);
+ }
+ }
+ void loadNewAccounts();
+ }, []);
+
return (
{/* Header Area */}
@@ -118,7 +157,13 @@ export default function NetworkPage() {
{/* Network metrics panels */}
-
+
{/* Topology */}
diff --git a/frontend/src/components/charts/NewAccountsChart.tsx b/frontend/src/components/charts/NewAccountsChart.tsx
new file mode 100644
index 00000000..ebfa2e11
--- /dev/null
+++ b/frontend/src/components/charts/NewAccountsChart.tsx
@@ -0,0 +1,192 @@
+"use client";
+
+import { useRef } from "react";
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { ChartExportButton } from "./ChartExportButton";
+import { getTooltipContentStyle } from "@/lib/chart-utils";
+import type { NetworkNewAccountsPoint } from "@/lib/network-api";
+
+interface NewAccountsChartProps {
+ data: NetworkNewAccountsPoint[];
+ loading?: boolean;
+}
+
+function formatCompact(value: number): string {
+ return new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ maximumFractionDigits: 1,
+ }).format(value);
+}
+
+export function NewAccountsChart({
+ data,
+ loading = false,
+}: NewAccountsChartProps) {
+ 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 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 // New Accounts
+
+
+ New Accounts
+
+
+ No new-account series yet. Data appears once{" "}
+ /api/v1/network/new-accounts{" "}
+ returns daily account-creation counts.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Network // New Accounts
+
+
+ New Accounts
+
+
+ Accounts created per day
+
+
+
+
+
+
+
+
+ Latest day
+
+
+ {formatCompact(latest)}
+
+
+
+
+ Period total
+
+
+ {formatCompact(total)}
+
+
+
+
+ Peak day
+
+
+ {formatCompact(peak)}
+
+
+
+
+
+
+
+
+
+
+ [
+ formatCompact(typeof value === "number" ? value : Number(value)),
+ "New accounts",
+ ]}
+ />
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/lib/network-api.ts b/frontend/src/lib/network-api.ts
index dc77807f..b3c121a4 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/new-accounts → {date, count} (backend#18)
* 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 NetworkNewAccountsPoint {
+ date: string;
+ /** New accounts created that day. */
+ count: number;
+}
+
+export interface NetworkNewAccountsResponse {
+ points: NetworkNewAccountsPoint[];
+}
+
async function fetchJson(url: string): Promise {
const response = await fetch(url, {
method: "GET",
@@ -89,3 +100,58 @@ export async function fetchNetworkPaymentVolume(
return { points: [], unit: "usd" };
}
}
+
+function normalizeNewAccountsPoints(raw: unknown): NetworkNewAccountsPoint[] {
+ 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;
+ new_accounts?: number;
+ created_accounts?: number;
+ };
+ const date = item.date ?? item.day ?? item.timestamp;
+ const count = item.count ?? item.new_accounts ?? item.created_accounts;
+ if (!date || typeof count !== "number" || Number.isNaN(count))
+ return null;
+ return { date, count };
+ })
+ .filter((point): point is NetworkNewAccountsPoint => point != null)
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
+}
+
+/**
+ * New-accounts-per-day time series (backend#18).
+ * 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 fetchNetworkNewAccounts(
+ days = 30,
+): Promise {
+ const url = `${API_BASE}/api/v1/network/new-accounts?days=${days}`;
+ try {
+ const data = await fetchJson(url);
+ return { points: normalizeNewAccountsPoints(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 new accounts:", error);
+ }
+ return { points: [] };
+ }
+}