From a22877739c71267b2d4c84ecaae2ab5f71eefb25 Mon Sep 17 00:00:00 2001 From: Wilfred007 Date: Tue, 24 Feb 2026 17:56:23 +0100 Subject: [PATCH] fixed cicd pipeline failed --- .github/workflows/security.yml | 2 +- frontend/app/globals.css | 98 ++++--- frontend/app/incoming/page.tsx | 39 ++- frontend/components/IncomingStreams.tsx | 96 ++++--- .../components/dashboard/dashboard-view.tsx | 123 ++++++--- frontend/lib/api-types.ts | 33 +++ frontend/lib/dashboard.ts | 249 +++++++++--------- 7 files changed, 391 insertions(+), 249 deletions(-) create mode 100644 frontend/lib/api-types.ts diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 31a8b80..fffc1c7 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' cache: 'npm' - name: Install dependencies diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 904355d..9d33b5a 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -63,6 +63,7 @@ from { opacity: 0; } + to { opacity: 1; } @@ -73,6 +74,7 @@ opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); @@ -84,6 +86,7 @@ opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); @@ -91,20 +94,24 @@ } @keyframes pulse-slow { + 0%, 100% { opacity: 0.1; } + 50% { opacity: 0.3; } } @keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } @@ -206,11 +213,9 @@ body { .wallet-card { border-radius: 1rem; border: 1px solid rgba(19, 38, 61, 0.12); - background: linear-gradient( - 165deg, - rgba(255, 255, 255, 0.9), - rgba(239, 247, 255, 0.62) - ); + background: linear-gradient(165deg, + rgba(255, 255, 255, 0.9), + rgba(239, 247, 255, 0.62)); padding: 1rem; display: flex; flex-direction: column; @@ -258,11 +263,9 @@ body { border: 0; border-radius: 0.75rem; height: 2.65rem; - background: linear-gradient( - 90deg, - var(--accent-strong), - var(--wallet-accent) - ); + background: linear-gradient(90deg, + var(--accent-strong), + var(--wallet-accent)); color: #ffffff; font-size: 0.95rem; font-weight: 600; @@ -489,11 +492,9 @@ body { .dashboard-stat-card { border-radius: 0.9rem; border: 1px solid rgba(19, 38, 61, 0.12); - background: linear-gradient( - 160deg, - rgba(255, 255, 255, 0.88), - rgba(242, 250, 255, 0.76) - ); + background: linear-gradient(160deg, + rgba(255, 255, 255, 0.88), + rgba(242, 250, 255, 0.76)); padding: 0.9rem; } @@ -535,20 +536,16 @@ body { .dashboard-analytics-card { border-radius: 0.9rem; border: 1px solid rgba(19, 38, 61, 0.11); - background: linear-gradient( - 165deg, - rgba(250, 255, 255, 0.92), - rgba(236, 247, 252, 0.74) - ); + background: linear-gradient(165deg, + rgba(250, 255, 255, 0.92), + rgba(236, 247, 252, 0.74)); padding: 0.85rem; } .dashboard-analytics-card[data-unavailable="true"] { - background: linear-gradient( - 165deg, - rgba(247, 250, 253, 0.88), - rgba(236, 242, 249, 0.75) - ); + background: linear-gradient(165deg, + rgba(247, 250, 253, 0.88), + rgba(236, 242, 249, 0.75)); } .dashboard-analytics-card p { @@ -759,6 +756,7 @@ body { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); @@ -770,6 +768,7 @@ body { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); @@ -787,6 +786,7 @@ body { opacity: 0.84; transform: scale(0.88); } + to { opacity: 1; transform: scale(1.04); @@ -814,12 +814,10 @@ body { left: 0; right: 0; height: 1px; - background: linear-gradient( - 90deg, - transparent, - var(--glass-highlight), - transparent - ); + background: linear-gradient(90deg, + transparent, + var(--glass-highlight), + transparent); z-index: 1; } @@ -958,14 +956,48 @@ body { .stepper__label { font-size: 0.625rem; } - + .stepper__circle { width: 2rem; height: 2rem; font-size: 0.75rem; } - + .stepper__line { top: 1rem; } } + +.dashboard-loading-state, +.dashboard-error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 300px; + text-align: center; + border: 1px dashed rgba(19, 38, 61, 0.2); + border-radius: 1rem; + background: rgba(255, 255, 255, 0.55); + padding: 2rem; +} + +.dashboard-loading-state p, +.dashboard-error-state p { + margin-top: 1rem; + color: #4d6985; +} + +.spinner { + width: 2.5rem; + height: 2.5rem; + border-radius: 999px; + border: 3px solid rgba(13, 83, 120, 0.1); + border-top-color: var(--accent-strong); + animation: spin 1s linear infinite; +} + +.dashboard-error-state h3 { + color: #b12f3f; + margin: 0; +} \ No newline at end of file diff --git a/frontend/app/incoming/page.tsx b/frontend/app/incoming/page.tsx index f83357b..d00cc15 100644 --- a/frontend/app/incoming/page.tsx +++ b/frontend/app/incoming/page.tsx @@ -1,13 +1,50 @@ +"use client"; + import IncomingStreams from "../../components/IncomingStreams"; import { Navbar } from "@/components/Navbar"; +import { useWallet } from "@/context/wallet-context"; +import React, { useEffect, useState } from "react"; +import { fetchDashboardData, type Stream } from "@/lib/dashboard"; export default function IncomingPage() { + const { session, status } = useWallet(); + const [streams, setStreams] = useState([]); + const [loading, setLoading] = useState(true); + const [prevKey, setPrevKey] = useState(session?.publicKey); + + // Reset loading state if public key changes + if (session?.publicKey !== prevKey) { + setPrevKey(session?.publicKey); + setLoading(true); + } + + useEffect(() => { + if (session?.publicKey) { + fetchDashboardData(session.publicKey) + .then(data => setStreams(data.incomingStreams)) + .catch(err => console.error("Failed to fetch incoming streams:", err)) + .finally(() => setLoading(false)); + } + }, [session?.publicKey]); + return (
- + {status !== "connected" ? ( +
+

Wallet Not Connected

+

Please connect your wallet in the app to view your incoming streams.

+
+ ) : loading ? ( +
+
+

Loading incoming streams...

+
+ ) : ( + + )}
diff --git a/frontend/components/IncomingStreams.tsx b/frontend/components/IncomingStreams.tsx index 1258709..2b57e1b 100644 --- a/frontend/components/IncomingStreams.tsx +++ b/frontend/components/IncomingStreams.tsx @@ -2,76 +2,65 @@ import React, { useState } from 'react'; import toast from "react-hot-toast"; +import type { Stream } from '@/lib/dashboard'; -interface IncomingStreamData { - id: string; - sender: string; - token: string; - rate: string; - accrued: number; - status: 'Active' | 'Completed' | 'Paused'; +interface IncomingStreamsProps { + streams: Stream[]; } -const mockIncomingStreams: IncomingStreamData[] = [ - { id: '101', sender: 'G...56yA', token: 'USDC', rate: '500/mo', accrued: 125.50, status: 'Active' }, - { id: '102', sender: 'G...Klm9', token: 'XLM', rate: '1000/mo', accrued: 450.00, status: 'Active' }, - { id: '103', sender: 'G...22Pq', token: 'EURC', rate: '200/mo', accrued: 200.00, status: 'Completed' }, - { id: '104', sender: 'G...99Zx', token: 'USDC', rate: '1200/mo', accrued: 0.00, status: 'Paused' }, - { id: '105', sender: 'G...44Tb', token: 'XLM', rate: '300/mo', accrued: 300.00, status: 'Completed' }, -]; - -const IncomingStreams: React.FC = () => { +const IncomingStreams: React.FC = ({ streams }) => { const [filter, setFilter] = useState<'All' | 'Active' | 'Completed' | 'Paused'>('All'); const filteredStreams = filter === 'All' - ? mockIncomingStreams - : mockIncomingStreams.filter(s => s.status === filter); + ? streams + : streams.filter(s => s.status === filter); const handleWithdraw = async () => { - const toastId = toast.loading("Transaction pending..."); - - try { - // Simulate async transaction (replace with real blockchain call later) - await new Promise((resolve) => setTimeout(resolve, 2000)); + const toastId = toast.loading("Transaction pending..."); - toast.success("Withdrawal successful!", { id: toastId }); - } catch { - toast.error("Transaction failed.", { id: toastId }); - } -}; + try { + await new Promise((resolve) => setTimeout(resolve, 2000)); + toast.success("Withdrawal successful!", { id: toastId }); + } catch { + toast.error("Transaction failed.", { id: toastId }); + } + }; const handleFilterChange = (e: React.ChangeEvent) => { setFilter(e.target.value as 'All' | 'Active' | 'Completed' | 'Paused'); }; return ( -
-
-

Incoming Streams

-
- +
+
+
+

Incoming Payment Streams

+

Manage and withdraw from your active incoming streams

+
+
+
-
- - +
+
+ - - + + @@ -79,10 +68,10 @@ const IncomingStreams: React.FC = () => { {filteredStreams.map((stream) => ( - + - - + +
Sender TokenRateAccrued AmountDepositedWithdrawn Status Actions
{stream.sender}{stream.recipient} {stream.token}{stream.rate}{stream.accrued.toFixed(2)}{stream.deposited} {stream.token}{stream.withdrawn} {stream.token} { @@ -103,12 +96,13 @@ const IncomingStreams: React.FC = () => { ))}
- {filteredStreams.length === 0 && ( -
- No {filter !== 'All' ? filter.toLowerCase() : ''} streams found. -
- )}
+ + {filteredStreams.length === 0 && ( +
+

No incoming streams found matching the filter.

+
+ )}
); }; diff --git a/frontend/components/dashboard/dashboard-view.tsx b/frontend/components/dashboard/dashboard-view.tsx index fceae1b..f984db1 100644 --- a/frontend/components/dashboard/dashboard-view.tsx +++ b/frontend/components/dashboard/dashboard-view.tsx @@ -3,7 +3,7 @@ import React from "react"; import { getDashboardAnalytics, - getMockDashboardStats, + fetchDashboardData, type DashboardSnapshot, } from "@/lib/dashboard"; import { shortenPublicKey, type WalletSession } from "@/lib/wallet"; @@ -89,7 +89,7 @@ function renderAnalytics(snapshot: DashboardSnapshot | null) {

{isUnavailable ? "No data" - : formatAnalyticsValue(metric.value, metric.format)} + : formatAnalyticsValue(metric.value!, metric.format)}

{isUnavailable ? metric.unavailableText : metric.detail} @@ -149,7 +149,7 @@ function renderStreams(

My Active Streams

- {snapshot.streams.length} total + {snapshot.outgoingStreams.length} total
@@ -164,7 +164,7 @@ function renderStreams( - {snapshot.streams.map((stream) => ( + {snapshot.outgoingStreams.map((stream) => ( {stream.date} @@ -236,7 +236,33 @@ function renderRecentActivity(snapshot: DashboardSnapshot) { export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const [activeTab, setActiveTab] = React.useState("overview"); const [showWizard, setShowWizard] = React.useState(false); - const stats = getMockDashboardStats(session.walletId); + const [stats, setStats] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const [prevKey, setPrevKey] = React.useState(session.publicKey); + + // Reset loading state during render if key changes + if (session.publicKey !== prevKey) { + setPrevKey(session.publicKey); + setLoading(true); + } + + React.useEffect(() => { + async function loadData() { + try { + setError(null); + const data = await fetchDashboardData(session.publicKey); + setStats(data); + } catch (err) { + setError("Failed to load dashboard data. Please check your connection to the FlowFi backend."); + console.error(err); + } finally { + setLoading(false); + } + } + + loadData(); + }, [session.publicKey]); const handleTopUp = (streamId: string) => { const amount = prompt(`Enter amount to add to stream ${streamId}:`); @@ -255,7 +281,7 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { // 2. Calling the contract's create_stream function // 3. Handling the transaction signing // 4. Waiting for confirmation - + // For now, simulate success await new Promise((resolve) => setTimeout(resolve, 1500)); alert(`Stream created successfully!\n\nRecipient: ${data.recipient}\nToken: ${data.token}\nAmount: ${data.amount}\nDuration: ${data.duration} ${data.durationUnit}`); @@ -264,46 +290,67 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const renderContent = () => { if (activeTab === "incoming") { - return
; + return
; } if (activeTab === "overview") { - if (!stats) { - return ( -
-

No stream data yet

-

- Your account is connected, but there are no active or historical - stream records available yet. -

-
    -
  • Create your first payment stream
  • -
  • Invite a recipient to start receiving funds
  • -
  • Check back once transactions are confirmed
  • -
-
- -
-
- ); - } + if (loading) { + return ( +
+
+

Fetching your stream data...

+
+ ); + } + + if (error) { + return ( +
+

Oops! Something went wrong

+

{error}

+ +
+ ); + } + + if (!stats || (stats.outgoingStreams.length === 0 && stats.recentActivity.length === 0)) { return ( -
- {renderStats(stats)} - {renderAnalytics(stats)} - {renderStreams(stats, handleTopUp)} - {renderRecentActivity(stats)} +
+

No stream data yet

+

+ Your account is connected, but there are no active or historical + stream records available yet. +

+
    +
  • Create your first payment stream
  • +
  • Invite a recipient to start receiving funds
  • +
  • Check back once transactions are confirmed
  • +
+
+
+
); + } + return ( +
+ {renderStats(stats)} + {renderAnalytics(stats)} + {renderStreams(stats, handleTopUp)} + {renderRecentActivity(stats)} +
+ ); } - + return ( -
-

Under Construction

-

This tab is currently under development.

-
+
+

Under Construction

+

This tab is currently under development.

+
); }; diff --git a/frontend/lib/api-types.ts b/frontend/lib/api-types.ts new file mode 100644 index 0000000..d1fa2b8 --- /dev/null +++ b/frontend/lib/api-types.ts @@ -0,0 +1,33 @@ +export interface BackendUser { + id: string; + publicKey: string; + createdAt: string; + updatedAt: string; +} + +export interface BackendStreamEvent { + id: string; + streamId: string; + eventType: string; + data: string; + createdAt: string; +} + +export interface BackendStream { + id: string; + streamId: number; + sender: string; + recipient: string; + tokenAddress: string; + ratePerSecond: string; + depositedAmount: string; + withdrawnAmount: string; + startTime: number; + lastUpdateTime: number; + isActive: boolean; + createdAt: string; + updatedAt: string; + senderUser?: BackendUser; + recipientUser?: BackendUser; + events?: BackendStreamEvent[]; +} diff --git a/frontend/lib/dashboard.ts b/frontend/lib/dashboard.ts index 92abb87..b86c622 100644 --- a/frontend/lib/dashboard.ts +++ b/frontend/lib/dashboard.ts @@ -1,4 +1,5 @@ import type { WalletId } from "@/lib/wallet"; +import type { BackendStream } from "./api-types"; export interface ActivityItem { id: string; @@ -14,7 +15,7 @@ export interface Stream { recipient: string; amount: number; token: string; - status: "Active" | "Completed" | "Cancelled"; + status: "Active" | "Completed" | "Paused"; deposited: number; withdrawn: number; date: string; @@ -26,7 +27,8 @@ export interface DashboardSnapshot { totalValueLocked: number; activeStreamsCount: number; recentActivity: ActivityItem[]; - streams: Stream[]; + outgoingStreams: Stream[]; + incomingStreams: Stream[]; } export interface DashboardAnalyticsMetric { @@ -38,108 +40,103 @@ export interface DashboardAnalyticsMetric { unavailableText: string; } -const MOCK_STATS_BY_WALLET: Record = { - freighter: { - totalSent: 12850, - totalReceived: 4720, - totalValueLocked: 32140, - activeStreamsCount: 2, - streams: [ - { - id: "stream-1", - date: "2023-10-25", - recipient: "G...ABCD", - amount: 500, - token: "USDC", - status: "Active", - deposited: 500, - withdrawn: 100, - }, - { - id: "stream-2", - date: "2023-10-26", - recipient: "G...EFGH", - amount: 1200, - token: "XLM", - status: "Active", - deposited: 1200, - withdrawn: 300, - }, - ], - recentActivity: [ - { - id: "act-1", - title: "Design Retainer", - description: "Outgoing stream settled", - amount: 250, - direction: "sent", - timestamp: "2026-02-19T13:10:00.000Z", - }, - { - id: "act-2", - title: "Community Grant", - description: "Incoming stream payout", - amount: 420, - direction: "received", - timestamp: "2026-02-18T17:45:00.000Z", - }, - { - id: "act-3", - title: "Developer Subscription", - description: "Outgoing recurring payment", - amount: 85, - direction: "sent", - timestamp: "2026-02-18T09:15:00.000Z", - }, - ], - }, - albedo: null, - xbull: { - totalSent: 2130, - totalReceived: 3890, - totalValueLocked: 5400, - activeStreamsCount: 1, - streams: [ - { - id: "stream-3", - date: "2023-10-27", - recipient: "G...IJKL", - amount: 300, - token: "EURC", - status: "Active", - deposited: 300, - withdrawn: 50, - }, - ], - recentActivity: [ - { - id: "act-4", - title: "Ops Payroll", - description: "Incoming stream payout", - amount: 630, - direction: "received", - timestamp: "2026-02-19T08:05:00.000Z", - }, - ], - }, -}; +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/v1"; -export function getMockDashboardStats( - walletId: WalletId, -): DashboardSnapshot | null { - const source = MOCK_STATS_BY_WALLET[walletId]; - - if (!source) { - return null; - } +/** + * Maps a backend stream object to the frontend Stream interface. + */ +function mapBackendStreamToFrontend(s: BackendStream): Stream { + const deposited = parseFloat(s.depositedAmount) / 1e7; + const withdrawn = parseFloat(s.withdrawnAmount) / 1e7; return { - ...source, - recentActivity: source.recentActivity.map((activity) => ({ ...activity })), - streams: source.streams.map((stream) => ({ ...stream })), + id: s.streamId.toString(), + recipient: s.recipient.slice(0, 4) + "..." + s.recipient.slice(-4), + amount: deposited, + token: "TOKEN", // Placeholder + status: s.isActive ? "Active" : "Completed", + deposited, + withdrawn, + date: new Date(s.startTime * 1000).toISOString().split("T")[0], }; } +/** + * Fetches dashboard data for a given public key. + */ +export async function fetchDashboardData(publicKey: string): Promise { + try { + const [outgoingRes, incomingRes] = await Promise.all([ + fetch(`${API_BASE_URL}/streams?sender=${publicKey}`), + fetch(`${API_BASE_URL}/streams?recipient=${publicKey}`), + ]); + + if (!outgoingRes.ok || !incomingRes.ok) { + throw new Error("Failed to fetch streams from backend."); + } + + const outgoing: BackendStream[] = await outgoingRes.json(); + const incoming: BackendStream[] = await incomingRes.json(); + + const outgoingStreams = outgoing.map(mapBackendStreamToFrontend); + const incomingStreams = incoming.map(mapBackendStreamToFrontend); + + // Aggregation logic + let totalSent = 0; + let totalValueLocked = 0; + let activeStreamsCount = 0; + + outgoing.forEach(s => { + const dep = parseFloat(s.depositedAmount) / 1e7; + const withdr = parseFloat(s.withdrawnAmount) / 1e7; + totalSent += withdr; + if (s.isActive) { + totalValueLocked += (dep - withdr); + activeStreamsCount++; + } + }); + + let totalReceived = 0; + incoming.forEach(s => { + totalReceived += parseFloat(s.withdrawnAmount) / 1e7; + }); + + // Generate recent activity + const recentActivity: ActivityItem[] = [ + ...outgoing.map(s => ({ + id: `act-out-${s.id}`, + title: "Outgoing Stream", + description: `Stream to ${s.recipient.slice(0, 6)}...`, + amount: parseFloat(s.depositedAmount) / 1e7, + direction: "sent" as const, + timestamp: s.createdAt, + })), + ...incoming.map(s => ({ + id: `act-in-${s.id}`, + title: "Incoming Stream", + description: `Stream from ${s.sender.slice(0, 6)}...`, + amount: parseFloat(s.depositedAmount) / 1e7, + direction: "received" as const, + timestamp: s.createdAt, + })), + ].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + .slice(0, 10); + + return { + totalSent, + totalReceived, + totalValueLocked, + activeStreamsCount, + recentActivity, + outgoingStreams, + incomingStreams, + }; + } catch (error) { + console.error("Dashboard data fetch error:", error); + throw error; + } +} + const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; export function getDashboardAnalytics( @@ -150,34 +147,34 @@ export function getDashboardAnalytics( { id: "total-volume-30d", label: "Total Volume (30D)", - detail: "All incoming and outgoing activity in the last 30 days", + detail: "Total throughput in the last 30 days", format: "currency", value: null, - unavailableText: "No recent activity data", + unavailableText: "Insufficient historical data", }, { id: "net-flow-30d", label: "Net Flow (30D)", - detail: "Incoming minus outgoing activity over the same period", + detail: "Net capital flow over the last 30 days", format: "currency", value: null, - unavailableText: "No recent activity data", + unavailableText: "Insufficient historical data", }, { id: "avg-value-per-stream", - label: "Avg Locked Value / Active Stream", - detail: "Current TVL divided by active stream count", + label: "Avg Value / Stream", + detail: "Mean capital locked across active streams", format: "currency", value: null, - unavailableText: "No active stream data", + unavailableText: "No active streams", }, { id: "stream-utilization", label: "Stream Utilization", - detail: "Total withdrawn as a share of total deposited funds", + detail: "Share of total capital already withdrawn", format: "percent", value: null, - unavailableText: "No stream funding data", + unavailableText: "No withdrawal data", }, ]; } @@ -203,37 +200,39 @@ export function getDashboardAnalytics( ? snapshot.totalValueLocked / snapshot.activeStreamsCount : null; - const totalDeposited = snapshot.streams.reduce( - (sum, stream) => sum + stream.deposited, - 0, - ); - const totalWithdrawn = snapshot.streams.reduce( - (sum, stream) => sum + stream.withdrawn, - 0, - ); + const totalDeposited = [ + ...snapshot.outgoingStreams, + ...snapshot.incomingStreams, + ].reduce((sum, stream) => sum + stream.deposited, 0); + + const totalWithdrawn = [ + ...snapshot.outgoingStreams, + ...snapshot.incomingStreams, + ].reduce((sum, stream) => sum + stream.withdrawn, 0); + const utilization = totalDeposited > 0 ? totalWithdrawn / totalDeposited : null; return [ { id: "total-volume-30d", label: "Total Volume (30D)", - detail: "All incoming and outgoing activity in the last 30 days", + detail: "Total throughput in the last 30 days", format: "currency", - value: recentActivity.length > 0 ? totalVolume30d : null, - unavailableText: "No activity in the last 30 days", + value: totalVolume30d, + unavailableText: "Insufficient historical data", }, { id: "net-flow-30d", label: "Net Flow (30D)", - detail: "Incoming minus outgoing activity over the same period", + detail: "Net capital flow over the last 30 days", format: "currency", - value: recentActivity.length > 0 ? netFlow30d : null, - unavailableText: "No activity in the last 30 days", + value: netFlow30d, + unavailableText: "Insufficient historical data", }, { id: "avg-value-per-stream", - label: "Avg Locked Value / Active Stream", - detail: "Current TVL divided by active stream count", + label: "Avg Value / Stream", + detail: "Mean capital locked across active streams", format: "currency", value: avgValuePerStream, unavailableText: "No active streams", @@ -241,10 +240,10 @@ export function getDashboardAnalytics( { id: "stream-utilization", label: "Stream Utilization", - detail: "Total withdrawn as a share of total deposited funds", + detail: "Share of total capital already withdrawn", format: "percent", value: utilization, - unavailableText: "No deposited funds yet", + unavailableText: "No withdrawal data", }, ]; }