From f4cc28163576235bf1111ea67f64cc122dc8e96d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 4 Oct 2026 20:53:05 -0400 Subject: [PATCH 1/9] feat(frontend): route cash flow forecast page --- frontend/src/App.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index acc3dfc6..5a1dd015 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import Settings from './pages/Settings'; import WebhookSettings from './pages/WebhookSettings'; import TwoFactorSettings from './pages/TwoFactorSettings'; import CustomReportBuilder from './pages/CustomReportBuilder'; +import CashFlowForecast from './pages/CashFlowForecast'; import CrossAssetPayment from './pages/CrossAssetPayment'; import TransactionHistory from './pages/TransactionHistory'; import BulkPaymentTracker from './pages/BulkPaymentTracker'; @@ -105,6 +106,14 @@ function App() { } /> + {}} />}> + + + } + /> Date: Sun, 4 Oct 2026 20:53:18 -0400 Subject: [PATCH 2/9] fix(frontend): avoid empty forecast request on mount --- frontend/src/pages/CashFlowForecast.tsx | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/CashFlowForecast.tsx b/frontend/src/pages/CashFlowForecast.tsx index b3796322..738537c8 100644 --- a/frontend/src/pages/CashFlowForecast.tsx +++ b/frontend/src/pages/CashFlowForecast.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { BarChart, Bar, @@ -49,18 +49,26 @@ export default function CashFlowForecast() { forecastDays: 90, }); + const canLoadForecast = + params.distributionAccount.trim().length > 0 && params.assetIssuer.trim().length > 0; + const loadForecast = useCallback(async () => { - if (!params.distributionAccount || !params.assetIssuer) { - notifyError('Please provide distribution account and asset issuer'); + if (!params.distributionAccount.trim() || !params.assetIssuer.trim()) { return; } + const requestParams: ForecastParams = { + ...params, + distributionAccount: params.distributionAccount.trim(), + assetIssuer: params.assetIssuer.trim(), + }; + setIsLoading(true); try { const [forecastData, historicalData, alertsData] = await Promise.all([ - getForecast(params), + getForecast(requestParams), getHistoricalData(6), - getAlerts(params), + getAlerts(requestParams), ]); setForecast(forecastData); @@ -74,10 +82,6 @@ export default function CashFlowForecast() { } }, [params, notifyError, notifySuccess]); - useEffect(() => { - void loadForecast(); - }, [loadForecast]); - const chartData = useMemo(() => { if (!forecast) return []; @@ -178,7 +182,7 @@ export default function CashFlowForecast() { onClick={() => { void loadForecast(); }} - disabled={isLoading} + disabled={isLoading || !canLoadForecast} className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-(--text) rounded-lg font-semibold transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 min-h-[44px] touch-manipulation" > From 2dc8bd553a99ab1128ad54851a54e22e252edb49 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 4 Oct 2026 20:55:14 -0400 Subject: [PATCH 3/9] fix(frontend): add authenticated cash flow API client --- frontend/src/services/cashFlowLiveApi.ts | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 frontend/src/services/cashFlowLiveApi.ts diff --git a/frontend/src/services/cashFlowLiveApi.ts b/frontend/src/services/cashFlowLiveApi.ts new file mode 100644 index 00000000..4bdb9549 --- /dev/null +++ b/frontend/src/services/cashFlowLiveApi.ts @@ -0,0 +1,76 @@ +import api from '../utils/api'; +import type { + AlertsResponse, + BudgetAlert, + CashFlowForecast, + ForecastParams, + ForecastResponse, + HistoricalDataResponse, + HistoricalPayrollData, + ProjectionsResponse, + UpcomingPayrollProjection, +} from './cashFlowForecastApi'; + +export type { + BudgetAlert, + CashFlowForecast, + ForecastParams, + HistoricalPayrollData, + UpcomingPayrollProjection, +} from './cashFlowForecastApi'; + +export async function getForecast(params: ForecastParams): Promise { + const { data } = await api.get('/cash-flow/forecast', { + params: { + forecastDays: params.forecastDays || 90, + distributionAccount: params.distributionAccount, + assetIssuer: params.assetIssuer, + }, + }); + + if (!data.success) throw new Error('Failed to fetch forecast'); + return data.data; +} + +export async function getHistoricalData( + monthsBack: number = 6 +): Promise<{ + historical: HistoricalPayrollData[]; + averages: { weekly: number; biweekly: number; monthly: number }; +}> { + const { data } = await api.get('/cash-flow/historical', { + params: { monthsBack }, + }); + + if (!data.success) throw new Error('Failed to fetch historical data'); + return data.data; +} + +export async function getProjections( + forecastDays: number = 90 +): Promise { + const { data } = await api.get('/cash-flow/projections', { + params: { forecastDays }, + }); + + if (!data.success) throw new Error('Failed to fetch projections'); + return data.data; +} + +export async function getAlerts( + params: ForecastParams +): Promise<{ + alerts: BudgetAlert[]; + summary: { totalAlerts: number; criticalAlerts: number; warningAlerts: number }; +}> { + const { data } = await api.get('/cash-flow/alerts', { + params: { + forecastDays: params.forecastDays || 90, + distributionAccount: params.distributionAccount, + assetIssuer: params.assetIssuer, + }, + }); + + if (!data.success) throw new Error('Failed to fetch alerts'); + return data.data; +} From 5e7ecb63a7820b245a430fdb2b1eeb9aacebfe5e Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sun, 4 Oct 2026 20:55:22 -0400 Subject: [PATCH 4/9] fix(frontend): use live cash flow client --- frontend/src/pages/CashFlowForecast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/CashFlowForecast.tsx b/frontend/src/pages/CashFlowForecast.tsx index 738537c8..f339bbc8 100644 --- a/frontend/src/pages/CashFlowForecast.tsx +++ b/frontend/src/pages/CashFlowForecast.tsx @@ -28,7 +28,7 @@ import { type CashFlowForecast as ForecastType, type BudgetAlert, type HistoricalPayrollData, -} from '../services/cashFlowForecastApi'; +} from '../services/cashFlowLiveApi'; import { useNotification } from '../hooks/useNotification'; interface ForecastParams { From 36d927e64b7b44342cf7bc5648f5ff1efdc9f8a5 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 14:54:40 -0400 Subject: [PATCH 5/9] fix(cash-flow): preserve projected shortfalls and input order --- frontend/src/pages/CashFlowForecast.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/CashFlowForecast.tsx b/frontend/src/pages/CashFlowForecast.tsx index f339bbc8..c102cf88 100644 --- a/frontend/src/pages/CashFlowForecast.tsx +++ b/frontend/src/pages/CashFlowForecast.tsx @@ -119,12 +119,13 @@ export default function CashFlowForecast() { ]; forecast.projections + .slice() .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) .forEach((proj) => { runningBalance -= proj.projectedAmount; data.push({ date: new Date(proj.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), - balance: Math.max(0, runningBalance), + balance: runningBalance, }); }); From 66100c1c56e60a3d1b9f7e13379d67bdac4e689d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 15:08:28 -0400 Subject: [PATCH 6/9] perf(cash-flow): reuse alerts from the loaded forecast Avoid a second generateForecast call through the alerts endpoint and keep alerts aligned with the displayed forecast snapshot. Keep historical loading and the standalone alerts API unchanged. Source-callback check: 3 page API calls before, 2 after; forecast-generating endpoints 2 to 1; alert snapshot identity restored. Empty-input and error/loading behavior preserved in three focused cases. This is a request-count measurement with in-memory collaborators, not a live latency or backend load benchmark. --- frontend/src/pages/CashFlowForecast.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/CashFlowForecast.tsx b/frontend/src/pages/CashFlowForecast.tsx index c102cf88..073fa348 100644 --- a/frontend/src/pages/CashFlowForecast.tsx +++ b/frontend/src/pages/CashFlowForecast.tsx @@ -24,7 +24,6 @@ import { import { getForecast, getHistoricalData, - getAlerts, type CashFlowForecast as ForecastType, type BudgetAlert, type HistoricalPayrollData, @@ -65,15 +64,14 @@ export default function CashFlowForecast() { setIsLoading(true); try { - const [forecastData, historicalData, alertsData] = await Promise.all([ + const [forecastData, historicalData] = await Promise.all([ getForecast(requestParams), getHistoricalData(6), - getAlerts(requestParams), ]); setForecast(forecastData); setHistorical(historicalData.historical); - setAlerts(alertsData.alerts); + setAlerts(forecastData.alerts); notifySuccess('Cash flow forecast updated'); } catch (error) { notifyError(error instanceof Error ? error.message : 'Failed to load cash flow forecast'); @@ -420,4 +418,4 @@ export default function CashFlowForecast() { )} ); -} +} \ No newline at end of file From 7736cce69ada2434f54f23b3fef266519bcf64c1 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 15:10:57 -0400 Subject: [PATCH 7/9] ci: drop invalid workflow-root retention-days GitHub Actions rejects retention-days at the workflow root, so build, secrets-check, dapp-ipfs, and contract-release failed before any job started. Artifact retention stays on upload-artifact steps. Run retention is a repository setting, not a workflow key. --- .github/workflows/build.yml | 4 ---- .github/workflows/contract-release.yml | 4 ---- .github/workflows/dapp-ipfs.yml | 4 ---- .github/workflows/secrets-check.yml | 4 ---- 4 files changed, 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52029992..68212ca7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ on: branches: ["main"] types: [opened, synchronize, reopened, ready_for_review] -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days env: CARGO_TERM_COLOR: always PKG_CONFIG_PATH: /usr/lib/pkgconfig @@ -113,6 +112,3 @@ jobs: - name: Run Tests working-directory: ./frontend run: npm test --if-present - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/contract-release.yml b/.github/workflows/contract-release.yml index 2655fcbc..cca80486 100644 --- a/.github/workflows/contract-release.yml +++ b/.github/workflows/contract-release.yml @@ -5,7 +5,6 @@ on: tags: - "v*" -# Retention policy: Keep successful runs for 90 days, failed/cancelled for 14 days permissions: # required permissions for the workflow id-token: write contents: write # in order to create releases @@ -27,6 +26,3 @@ jobs: package: "..." secrets: release_token: ${{ secrets.GITHUB_TOKEN }} - -# Workflow run retention settings -retention-days: 90 \ No newline at end of file diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 29d16e55..5887cc7e 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -9,7 +9,6 @@ on: workflow_dispatch: -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -65,6 +64,3 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "- CID: ${{ steps.storacha.outputs.cid }}" >> "$GITHUB_STEP_SUMMARY" echo "- URL: ${{ steps.storacha.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/secrets-check.yml b/.github/workflows/secrets-check.yml index b8d3b7e8..5e989082 100644 --- a/.github/workflows/secrets-check.yml +++ b/.github/workflows/secrets-check.yml @@ -10,7 +10,6 @@ on: paths: - "k8s/**" -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days jobs: check-secrets-placeholders: name: Verify no real secrets in k8s manifests @@ -20,6 +19,3 @@ jobs: - name: Check backend-secret.yaml for non-placeholder values run: ./scripts/check-k8s-secrets.sh - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file From 35abb634cbd8c5f2b94b85701e0bba45d963a0fc Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 21:29:13 -0400 Subject: [PATCH 8/9] fix(frontend): expose error-boundary reset callback --- frontend/src/components/ErrorBoundary.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index c57a9ff7..e14371f5 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -1,8 +1,12 @@ import React from 'react'; import * as Sentry from '@sentry/react'; +type ErrorBoundaryFallback = + | React.ReactNode + | ((props: { onReset: () => void }) => React.ReactNode); + type ErrorBoundaryProps = { - fallback: React.ReactNode; + fallback: ErrorBoundaryFallback; children: React.ReactNode; }; @@ -27,9 +31,15 @@ export default class ErrorBoundary extends React.Component { + this.setState({ hasError: false }); + }; + render() { if (this.state.hasError) { - return this.props.fallback; + return typeof this.props.fallback === 'function' + ? this.props.fallback({ onReset: this.resetErrorBoundary }) + : this.props.fallback; } return this.props.children; From 2a60d38e06d5d88b3cd43645896afa04bd282099 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Mon, 5 Oct 2026 21:29:22 -0400 Subject: [PATCH 9/9] fix(frontend): wire cash-flow retry to boundary reset --- frontend/src/App.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5a1dd015..64328302 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -109,7 +109,9 @@ function App() { {}} />}> + } + > }