diff --git a/.github/workflows/frontend-dev.yaml b/.github/workflows/frontend-dev.yaml index ae0542f..f8d17df 100644 --- a/.github/workflows/frontend-dev.yaml +++ b/.github/workflows/frontend-dev.yaml @@ -5,6 +5,10 @@ on: branches: [develop] paths: - 'frontend/**' + pull_request: + branches: [develop] + paths: + - 'frontend/**' workflow_dispatch: env: @@ -14,9 +18,35 @@ env: AZURE_RESOURCE_GROUP: rg-dfx-api-dev jobs: - build-and-deploy: - name: Build and deploy Frontend to DEV + build: + name: Build and test Frontend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: frontend + + - name: Build frontend + run: npm run build + working-directory: frontend + env: + VITE_API_BASE_URL: https://dev.monitoring.deuro.com/api + + deploy: + name: Deploy Frontend to DEV runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/frontend-prd.yaml b/.github/workflows/frontend-prd.yaml index 83ea8ea..5ddced3 100644 --- a/.github/workflows/frontend-prd.yaml +++ b/.github/workflows/frontend-prd.yaml @@ -5,6 +5,10 @@ on: branches: [main] paths: - 'frontend/**' + pull_request: + branches: [main] + paths: + - 'frontend/**' workflow_dispatch: env: @@ -14,9 +18,35 @@ env: AZURE_RESOURCE_GROUP: rg-dfx-api-prd jobs: - build-and-deploy: - name: Build and deploy Frontend to PRD + build: + name: Build and test Frontend + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: frontend + + - name: Build frontend + run: npm run build + working-directory: frontend + env: + VITE_API_BASE_URL: https://monitoring.deuro.com/api + + deploy: + name: Deploy Frontend to PRD runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/monitoring-dev.yaml b/.github/workflows/monitoring-dev.yaml index 6d367af..c6a53f1 100644 --- a/.github/workflows/monitoring-dev.yaml +++ b/.github/workflows/monitoring-dev.yaml @@ -5,6 +5,10 @@ on: branches: [develop] paths-ignore: - 'frontend/**' + pull_request: + branches: [develop] + paths-ignore: + - 'frontend/**' workflow_dispatch: inputs: reset_database: @@ -20,9 +24,29 @@ env: DEPLOY_INFO: ${{ github.ref_name }}-${{ github.sha }} jobs: - build-and-deploy: - name: Build and deploy to DEV + build: + name: Build and test Monitoring runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: ${{ env.DOCKER_TAGS }} + + deploy: + name: Deploy Monitoring to DEV + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' steps: - name: Checkout uses: actions/checkout@v4 @@ -54,8 +78,20 @@ jobs: inlineScript: | az containerapp update --resource-group ${{ env.AZURE_RESOURCE_GROUP }} --name ${{ env.AZURE_CONTAINER_APP }} --image ${{ env.DOCKER_TAGS }} --set-env-vars DEPLOY_INFO=${{ env.DEPLOY_INFO }} - - name: Reset Database (if requested) - if: inputs.reset_database == true + - name: Logout from Azure + run: az logout + if: always() + + reset-database: + name: Reset Database + runs-on: ubuntu-latest + needs: deploy + if: github.event_name == 'workflow_dispatch' && inputs.reset_database == true + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Reset Database run: | set -euo pipefail sudo apt-get update && sudo apt-get install -y postgresql-client @@ -68,8 +104,4 @@ jobs: psql "$DATABASE_URL" -f database/schema.sql echo "Database reset complete" env: - DATABASE_URL: ${{ secrets.DATABASE_URL_DEV }} - - - name: Logout from Azure - run: az logout - if: always() \ No newline at end of file + DATABASE_URL: ${{ secrets.DATABASE_URL_DEV }} \ No newline at end of file diff --git a/.github/workflows/monitoring-prd.yaml b/.github/workflows/monitoring-prd.yaml index eda840f..3ec31dc 100644 --- a/.github/workflows/monitoring-prd.yaml +++ b/.github/workflows/monitoring-prd.yaml @@ -5,6 +5,10 @@ on: branches: [main] paths-ignore: - 'frontend/**' + pull_request: + branches: [main] + paths-ignore: + - 'frontend/**' workflow_dispatch: inputs: reset_database: @@ -20,9 +24,29 @@ env: DEPLOY_INFO: ${{ github.ref_name }}-${{ github.sha }} jobs: - build-and-deploy: - name: Build and deploy to PRD + build: + name: Build and test Monitoring + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: ${{ env.DOCKER_TAGS }} + + deploy: + name: Deploy Monitoring to PRD runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4 @@ -60,10 +84,10 @@ jobs: reset-db: name: Reset PRD Database - needs: build-and-deploy + needs: deploy runs-on: ubuntu-latest timeout-minutes: 15 - if: inputs.reset_database == true + if: github.event_name == 'workflow_dispatch' && inputs.reset_database == true steps: - name: Checkout uses: actions/checkout@v4 diff --git a/frontend/.env.example b/frontend/.env.example index 65f2f2c..922b2e1 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1,3 @@ -VITE_API_BASE_URL=http://localhost:3001 \ No newline at end of file +VITE_API_BASE_URL=http://localhost:3001 +# or for remote backend +# VITE_API_BASE_URL=https://dev.monitoring.deuro.com/api \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 1ee0267..bfc4479 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,6 +5,8 @@ Monitoring dashboard for the dEURO protocol. ## Run it ```bash npm install + +# adjust VITE_API_BASE_URL in .env as needed (see .env.example), then npm run dev ``` diff --git a/frontend/src/components/PositionsTable.tsx b/frontend/src/components/PositionsTable.tsx index 8c49b47..3e9014b 100644 --- a/frontend/src/components/PositionsTable.tsx +++ b/frontend/src/components/PositionsTable.tsx @@ -5,12 +5,43 @@ import { colors } from '../lib/theme'; import { formatNumber, formatPercent, formatDateTime, formatCountdown, getStatusColor } from '../lib/formatters'; import { AddressLink } from './AddressLink'; import type { DataState } from '../lib/api.hook'; +import { exportToCSV } from '../lib/csv-export'; interface PositionsTableProps { data?: DataState; } export function PositionsTable({ data }: PositionsTableProps) { + const handleExport = () => { + if (!data?.data) return; + + const activePositions = data.data.filter(p => !p.isClosed); + const timestamp = new Date().toISOString().split('T')[0]; + + exportToCSV( + activePositions, + [ + { header: 'Created', getValue: (p) => p.created ? formatDateTime(Number(p.created)) : '-' }, + { header: 'Status', getValue: (p) => p.status }, + { header: 'Position Address', getValue: (p) => p.address }, + { header: 'Owner Address', getValue: (p) => p.owner }, + { header: 'Collateral Symbol', getValue: (p) => p.collateralSymbol }, + { header: 'Collateral Address', getValue: (p) => p.collateral }, + { header: 'Collateral Balance', getValue: (p) => Number(p.collateralBalance) }, + { header: 'Liquidation Price', getValue: (p) => Number(p.virtualPrice) }, + { header: 'Market Price', getValue: (p) => p.marketPrice ? Number(p.marketPrice) : '-' }, + { header: 'Principal', getValue: (p) => Number(p.principal) }, + { header: 'Interest', getValue: (p) => Number(p.interest) }, + { header: 'Debt', getValue: (p) => Number(p.debt) }, + { header: 'Collateralization Ratio (%)', getValue: (p) => Number(p.collateralizationRatio || 0) }, + { header: 'Reserve Contribution (%)', getValue: (p) => (p.reserveContribution / 10000).toFixed(2) }, + { header: 'Expiry', getValue: (p) => p.expiration ? formatDateTime(Number(p.expiration)) : '-' }, + { header: 'Time Until Expiry', getValue: (p) => p.cooldown ? formatCountdown(p.expiration) : '-' }, + ], + `deuro-positions-${timestamp}.csv` + ); + }; + const columns: Column[] = [ { header: { primary: 'CREATED', secondary: 'STATUS' }, @@ -95,6 +126,7 @@ export function PositionsTable({ data }: PositionsTableProps) { getRowKey={(position) => position.address} hidden={(position) => position.isClosed} emptyMessage="No positions found" + onExport={handleExport} /> ); } diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx index 349a5f4..6241fd4 100644 --- a/frontend/src/components/Table.tsx +++ b/frontend/src/components/Table.tsx @@ -30,9 +30,10 @@ interface TableProps { getRowKey: (row: T) => string; hidden?: (row: T) => boolean; emptyMessage?: string; + onExport?: () => void; } -export function Table({ title, data, sort, error, columns, getRowKey, hidden, emptyMessage = 'No data found' }: TableProps) { +export function Table({ title, data, sort, error, columns, getRowKey, hidden, emptyMessage = 'No data found', onExport }: TableProps) { const [showHidden, setShowHidden] = useState(false); const sortedData = useMemo(() => (data && sort ? [...data].sort(sort) : data), [data, sort]); @@ -55,13 +56,24 @@ export function Table({ title, data, sort, error, columns, getRowKey, hidden,

{title} ({filteredData?.length})

- +
+ {onExport && ( + + )} + +
{/* table */} diff --git a/frontend/src/lib/csv-export.ts b/frontend/src/lib/csv-export.ts new file mode 100644 index 0000000..2c2f183 --- /dev/null +++ b/frontend/src/lib/csv-export.ts @@ -0,0 +1,38 @@ +export function exportToCSV( + data: T[], + columns: { header: string; getValue: (row: T) => string | number }[], + filename: string +): void { + // Create CSV header + const headers = columns.map(col => col.header).join(','); + + // Create CSV rows + const rows = data.map(row => + columns.map(col => { + const value = col.getValue(row); + // Escape values that contain commas, quotes, or newlines + if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; + }).join(',') + ); + + // Combine header and rows + const csv = [headers, ...rows].join('\n'); + + // Create blob and download + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + + link.setAttribute('href', url); + link.setAttribute('download', filename); + link.style.visibility = 'hidden'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); +} diff --git a/src/monitoring.main.ts b/src/monitoring.main.ts index b20a365..3bb44cc 100644 --- a/src/monitoring.main.ts +++ b/src/monitoring.main.ts @@ -6,9 +6,13 @@ import { Logger } from '@nestjs/common'; async function bootstrap() { const logger = new Logger('Bootstrap'); - const allowedOrigins = process.env.ALLOWED_ORIGINS - ? process.env.ALLOWED_ORIGINS.split(',').map((origin) => origin.trim()) - : ['http://localhost:3000', 'http://localhost:5173', 'https://dev.monitoring.deuro.com', 'https://monitoring.deuro.com']; + const allowedOrigins = [ + 'http://localhost:3000', + 'http://localhost:5173', + ...(process.env.ALLOWED_ORIGINS + ? process.env.ALLOWED_ORIGINS.split(',').map((origin) => origin.trim()) + : ['https://dev.monitoring.deuro.com', 'https://monitoring.deuro.com']) + ]; const app = await NestFactory.create(AppModule, { cors: {