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 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() { } /> + {}} />}> + + + } + /> 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), + const [forecastData, historicalData] = await Promise.all([ + getForecast(requestParams), getHistoricalData(6), - getAlerts(params), ]); 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'); @@ -74,10 +80,6 @@ export default function CashFlowForecast() { } }, [params, notifyError, notifySuccess]); - useEffect(() => { - void loadForecast(); - }, [loadForecast]); - const chartData = useMemo(() => { if (!forecast) return []; @@ -115,12 +117,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, }); }); @@ -178,7 +181,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" > @@ -415,4 +418,4 @@ export default function CashFlowForecast() { )} ); -} +} \ No newline at end of file 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; +}