Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -113,6 +112,3 @@ jobs:
- name: Run Tests
working-directory: ./frontend
run: npm test --if-present

# Workflow run retention settings
retention-days: 30
4 changes: 0 additions & 4 deletions .github/workflows/contract-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +26,3 @@ jobs:
package: "..."
secrets:
release_token: ${{ secrets.GITHUB_TOKEN }}

# Workflow run retention settings
retention-days: 90
4 changes: 0 additions & 4 deletions .github/workflows/dapp-ipfs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 0 additions & 4 deletions .github/workflows/secrets-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
9 changes: 9 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -105,6 +106,14 @@ function App() {
</ErrorBoundary>
}
/>
<Route
path="/cash-flow"
element={
<ErrorBoundary fallback={<ErrorFallback onReset={() => {}} />}>
<CashFlowForecast />
</ErrorBoundary>
}
/>
<Route
path="/debug"
element={
Expand Down
35 changes: 19 additions & 16 deletions frontend/src/pages/CashFlowForecast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback } from 'react';
import {
BarChart,
Bar,
Expand All @@ -24,11 +24,10 @@ import {
import {
getForecast,
getHistoricalData,
getAlerts,
type CashFlowForecast as ForecastType,
type BudgetAlert,
type HistoricalPayrollData,
} from '../services/cashFlowForecastApi';
} from '../services/cashFlowLiveApi';
import { useNotification } from '../hooks/useNotification';

interface ForecastParams {
Expand All @@ -49,23 +48,30 @@ 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),
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');
Expand All @@ -74,10 +80,6 @@ export default function CashFlowForecast() {
}
}, [params, notifyError, notifySuccess]);

useEffect(() => {
void loadForecast();
}, [loadForecast]);

const chartData = useMemo(() => {
if (!forecast) return [];

Expand Down Expand Up @@ -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,
});
});

Expand Down Expand Up @@ -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"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
Expand Down Expand Up @@ -415,4 +418,4 @@ export default function CashFlowForecast() {
)}
</div>
);
}
}
76 changes: 76 additions & 0 deletions frontend/src/services/cashFlowLiveApi.ts
Original file line number Diff line number Diff line change
@@ -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<CashFlowForecast> {
const { data } = await api.get<ForecastResponse>('/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<HistoricalDataResponse>('/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<UpcomingPayrollProjection[]> {
const { data } = await api.get<ProjectionsResponse>('/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<AlertsResponse>('/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;
}