Skip to content
Merged
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
34 changes: 32 additions & 2 deletions .github/workflows/frontend-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [develop]
paths:
- 'frontend/**'
pull_request:
branches: [develop]
paths:
- 'frontend/**'
workflow_dispatch:

env:
Expand All @@ -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
Expand Down
34 changes: 32 additions & 2 deletions .github/workflows/frontend-prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [main]
paths:
- 'frontend/**'
pull_request:
branches: [main]
paths:
- 'frontend/**'
workflow_dispatch:

env:
Expand All @@ -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
Expand Down
50 changes: 41 additions & 9 deletions .github/workflows/monitoring-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [develop]
paths-ignore:
- 'frontend/**'
pull_request:
branches: [develop]
paths-ignore:
- 'frontend/**'
workflow_dispatch:
inputs:
reset_database:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()
DATABASE_URL: ${{ secrets.DATABASE_URL_DEV }}
32 changes: 28 additions & 4 deletions .github/workflows/monitoring-prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [main]
paths-ignore:
- 'frontend/**'
pull_request:
branches: [main]
paths-ignore:
- 'frontend/**'
workflow_dispatch:
inputs:
reset_database:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
VITE_API_BASE_URL=http://localhost:3001
VITE_API_BASE_URL=http://localhost:3001
# or for remote backend
# VITE_API_BASE_URL=https://dev.monitoring.deuro.com/api
2 changes: 2 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/PositionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PositionResponse[]>;
}

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<PositionResponse>[] = [
{
header: { primary: 'CREATED', secondary: 'STATUS' },
Expand Down Expand Up @@ -95,6 +126,7 @@ export function PositionsTable({ data }: PositionsTableProps) {
getRowKey={(position) => position.address}
hidden={(position) => position.isClosed}
emptyMessage="No positions found"
onExport={handleExport}
/>
);
}
Expand Down
28 changes: 20 additions & 8 deletions frontend/src/components/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ interface TableProps<T> {
getRowKey: (row: T) => string;
hidden?: (row: T) => boolean;
emptyMessage?: string;
onExport?: () => void;
}

export function Table<T>({ title, data, sort, error, columns, getRowKey, hidden, emptyMessage = 'No data found' }: TableProps<T>) {
export function Table<T>({ title, data, sort, error, columns, getRowKey, hidden, emptyMessage = 'No data found', onExport }: TableProps<T>) {
const [showHidden, setShowHidden] = useState(false);

const sortedData = useMemo(() => (data && sort ? [...data].sort(sort) : data), [data, sort]);
Expand All @@ -55,13 +56,24 @@ export function Table<T>({ title, data, sort, error, columns, getRowKey, hidden,
<h2 className={`text-sm ${typography.tableHeader} ${colors.text.primary}`}>
{title} ({filteredData?.length})
</h2>
<button
hidden={!hiddenCount}
onClick={() => setShowHidden((prev) => !prev)}
className={`text-xs ${colors.text.secondary} hover:text-gray-300 transition-colors cursor-pointer`}
>
{showHidden ? `Hide ${hiddenCount}` : `Show ${hiddenCount} more`}
</button>
<div className="flex gap-3 items-center">
{onExport && (
<button
onClick={onExport}
className={`text-xs ${colors.text.secondary} hover:text-gray-300 transition-colors cursor-pointer`}
title="Export to CSV"
>
↓ CSV
</button>
)}
<button
hidden={!hiddenCount}
onClick={() => setShowHidden((prev) => !prev)}
className={`text-xs ${colors.text.secondary} hover:text-gray-300 transition-colors cursor-pointer`}
>
{showHidden ? `Hide ${hiddenCount}` : `Show ${hiddenCount} more`}
</button>
</div>
</div>

{/* table */}
Expand Down
38 changes: 38 additions & 0 deletions frontend/src/lib/csv-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export function exportToCSV<T>(
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);
}
10 changes: 7 additions & 3 deletions src/monitoring.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down