Add deployment scripts and integrate APIs for dashboard #1

Merged
nabarun merged 6 commits from ng-deploy-laconic into main 2025-09-02 11:53:55 +00:00
9 changed files with 488 additions and 163 deletions
Showing only changes of commit 38a007c3a2 - Show all commits
+1 -3
View File
@@ -1,3 +1 @@
CERC_TEST_WEBAPP_CONFIG1="this string"
CERC_TEST_WEBAPP_CONFIG2="this different string"
CERC_WEBAPP_DEBUG=0
NEXT_PUBLIC_MTM_SERVICE_URL=http://localhost:3000
+216 -3
View File
@@ -125,7 +125,6 @@ The app currently reads and displays three environment variables:
- Failed transaction analysis and retry functionality
- Account balance monitoring with ETH refill interface
- App download analytics with charts and version tracking
- Customer support ticket management system
- PWA configuration updated for admin dashboard
## Dashboard Pages
@@ -135,7 +134,6 @@ The app currently reads and displays three environment variables:
- `/dashboard/failed` - Failed transaction analysis
- `/dashboard/balances` - Account balance management
- `/dashboard/downloads` - App download analytics
- `/dashboard/support` - Customer support system
## Technical Implementation
- **Framework**: Next.js with TypeScript
@@ -159,4 +157,219 @@ The app currently reads and displays three environment variables:
- Bar charts using CSS flex and dynamic heights
- Line charts using SVG paths and CSS positioning
- Progress bars for platform distribution
- All charts are animated and responsive
- All charts are animated and responsive
## Design Requirements
**CRITICAL: These requirements must be followed in ALL future modifications:**
### Dashboard Stats Display
- **NEVER add percentage changes, change indicators, or trend arrows to dashboard stats**
- Dashboard stats should only show: `id`, `name`, `stat`, and `icon` properties
- Do NOT add `change`, `changeType`, percentage indicators, or trend arrows
- Keep stats display clean and simple without any change/trend visualization
### UI Components
- No support/customer service functionality
- Focus purely on transaction monitoring and analytics
- Android-only for app downloads (no iOS)
- No authentication flow (direct redirect to dashboard)
# MTM to NYM Service API Documentation
This document describes the API endpoints for the MTM to NYM conversion service that powers the dashboard.
## Base URLs
- Dashboard API: `/api/dashboard`
- Transactions API: `/api/transactions`
## Dashboard Endpoints
### GET /api/dashboard/stats
Returns overall dashboard statistics with monthly conversion data.
**Response:**
```json
{
"totalConversions": 1247,
"successfulConversions": 1189,
"failedConversions": 58,
"totalDownloads": 3229,
"monthlyData": [
{
"month": "Mar",
"totalConversions": 137,
"successfulConversions": 130,
"failedConversions": 7
},
{
"month": "Apr",
"totalConversions": 124,
"successfulConversions": 118,
"failedConversions": 6
},
{
"month": "May",
"totalConversions": 149,
"successfulConversions": 142,
"failedConversions": 7
},
{
"month": "Jun",
"totalConversions": 162,
"successfulConversions": 155,
"failedConversions": 7
},
{
"month": "Jul",
"totalConversions": 149,
"successfulConversions": 142,
"failedConversions": 7
},
{
"month": "Aug",
"totalConversions": 311,
"successfulConversions": 296,
"failedConversions": 15
}
]
}
```
## Transaction Endpoints
### GET /api/transactions/conversions
Returns paginated MTM to NYM conversion data with filtering options.
**Query Parameters:**
- `page` (optional): Page number (default: 1)
- `limit` (optional): Items per page (default: 10, max: 100)
- `status` (optional): Filter by status (`success`, `failed`, `all`)
**Response:**
```json
{
"transactions": [
{
"id": "uuid-string",
"transactionHash": "2AUxZpuQ...",
"fromAddress": "HmWkGTaL...",
"nymTransactionHash": "4E169C93...",
"error": null,
"createdAt": "2025-08-24T10:30:00.000Z"
}
],
"totalCount": 1247,
"pagination": {
"page": 1,
"limit": 10,
"totalPages": 125
}
}
```
## Other Transaction Endpoints
### POST /api/transactions/get-nym
Processes MTM to NYM conversion request.
**Request Body:**
```json
{
"transactionHash": "2AUxZpuQ...",
"nyxAddress": "n1sdnrq62m07gcwzpfmdgvqfpqqjvtnnllcypur8",
"signedTxHash": "429ameDUN..."
}
```
**Response:**
```json
{
"success": true,
"data": {
"transactionHash": "2AUxZpuQ...",
"fromAddress": "HmWkGTaL...",
"nymTransactionHash": "4E169C93..."
}
}
```
### GET /api/transactions/get-nym-balance
Returns current NYM balance of the service wallet.
**Response:**
```json
{
"success": true,
"data": {
"balanceInNym": 75000.00
}
}
```
## Error Responses
All endpoints return standard error responses:
```json
{
"error": "Error message description"
}
```
Common HTTP status codes:
- `200` - Success
- `400` - Bad Request
- `404` - Not Found
- `500` - Internal Server Error
## Database Integration
The API endpoints integrate directly with the existing database models:
- `Transaction` - MTM to NYM conversions with fields:
- `id` (UUID)
- `transactionHash` (Solana transaction hash)
- `fromAddress` (Solana wallet address)
- `nymTransactionHash` (NYM transaction hash, optional)
- `error` (Error message if failed, optional)
- `createdAt` (Timestamp)
## API Configuration
### Required Environment Variables
```env
# CORS Configuration (Required)
CORS_ALLOWED_ORIGINS="http://localhost:3001,https://dashboard.example.com"
# Database and Service Configuration
SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
PORT=3000
NYX_PRIVATE_KEY=your_private_key
NYX_RPC_ENDPOINT=https://rpc.nymtech.net
COSMOS_GAS_PRICE=0.025
```
## API Usage
Start the service:
```bash
yarn start
```
APIs will be available at:
- Dashboard: `http://localhost:3000/api/dashboard/`
- Transactions: `http://localhost:3000/api/transactions/`
## Frontend Integration
The MTM VPN Dashboard connects via:
```env
NEXT_PUBLIC_MTM_SERVICE_URL=http://localhost:3000
```
+1 -55
View File
@@ -1,4 +1,4 @@
import { Transaction, BridgeTransaction, SwapTransaction, AccountBalance, AppDownload, SupportTicket, DashboardStats } from '../types';
import { Transaction, BridgeTransaction, SwapTransaction, AccountBalance, AppDownload, DashboardStats } from '../types';
export const mockTransactions: Transaction[] = [
{
@@ -263,60 +263,6 @@ export const mockAppDownloads: AppDownload[] = [
}
];
export const mockSupportTickets: SupportTicket[] = [
{
id: '1',
userId: 'user_123',
email: 'john.doe@example.com',
subject: 'Unable to connect to VPN servers',
message: 'I\'ve been trying to connect to VPN servers for the past 2 hours but keep getting connection timeout errors. Please help.',
status: 'open',
priority: 'high',
createdAt: new Date('2025-08-24T14:30:00Z'),
updatedAt: new Date('2025-08-24T14:30:00Z'),
responses: []
},
{
id: '2',
userId: 'user_456',
email: 'jane.smith@example.com',
subject: 'Transaction failed but MTM tokens were deducted',
message: 'I sent 50 MTM tokens but the transaction shows as failed, however the tokens are missing from my wallet.',
status: 'in-progress',
priority: 'critical',
createdAt: new Date('2025-08-24T13:15:00Z'),
updatedAt: new Date('2025-08-24T13:45:00Z'),
responses: [
{
id: 'resp_1',
ticketId: '2',
message: 'We are investigating this issue. Can you please provide the transaction hash?',
isAdminResponse: true,
createdAt: new Date('2025-08-24T13:45:00Z')
}
]
},
{
id: '3',
userId: 'user_789',
email: 'mike.wilson@example.com',
subject: 'Feature request: Dark mode support',
message: 'Would love to see dark mode support in the VPN app. The current bright theme is hard on the eyes during night usage.',
status: 'resolved',
priority: 'low',
createdAt: new Date('2025-08-23T16:20:00Z'),
updatedAt: new Date('2025-08-24T10:00:00Z'),
responses: [
{
id: 'resp_2',
ticketId: '3',
message: 'Thank you for the suggestion! Dark mode is planned for the next major release.',
isAdminResponse: true,
createdAt: new Date('2025-08-24T10:00:00Z')
}
]
}
];
export const mockDashboardStats: DashboardStats = {
totalTransactions: 1247,
+7 -3
View File
@@ -2,8 +2,12 @@
## Setup
- Clone the repo and run the next set of steps inside cloned repo directory
<!-- TODO: Add command to copy -->
- Clone the repo and change to the [deploy](./) directory
```bash
git clone git@git.vdb.to:cerc-io/mtm-vpn-dashboard.git
cd mtm-vpn-dashboard/deploy
```
- Build registry CLI image:
@@ -16,7 +20,7 @@
- Configure `userKey` in the [registry CLI config](./config.yml):
- User key should be of the account that owns the `laconic-deploy` authority (owner account address: `laconic1kwx2jm6vscz38qlyujvq6msujmk8l3zangqahs`)
- Account should also own the bond `5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2`
```bash
+4 -3
View File
@@ -9,17 +9,18 @@ echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
echo "Using AUTHORITY: $AUTHORITY"
# Repository URL
REPO_URL="https://github.com/deep-stack/mtm-vpn-dashboard"
REPO_URL="https://git.vdb.to/cerc-io/mtm-vpn-dashboard"
# Get the latest commit hash for a branch
# TODO: Change to main branch when ready
BRANCH_NAME="ng-deploy-laconic"
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
# Gitea
# PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version)
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/branch/$BRANCH_NAME/package.json | jq -r .version)
# GitHub
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/refs/heads/$BRANCH_NAME/package.json | jq -r .version)
# PACKAGE_VERSION=$(curl -s $REPO_URL/raw/refs/heads/$BRANCH_NAME/package.json | jq -r .version)
APP_NAME=mtm-vpn-dashboard
+1 -1
View File
@@ -12,7 +12,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"
/>
<meta name="description" content="MTM VPN Admin Dashboard - Monitor transactions, balances, and support" />
<meta name="description" content="MTM VPN Admin Dashboard - Monitor transactions" />
<meta name="keywords" content="MTM, VPN, Admin, Dashboard, Blockchain, NYM" />
<title>MTM VPN Admin Dashboard</title>
+138 -70
View File
@@ -1,63 +1,122 @@
// Charts removed for compatibility
import { useState, useEffect } from 'react';
import {
ArrowsRightLeftIcon,
CheckCircleIcon,
XCircleIcon,
CurrencyDollarIcon,
UsersIcon,
CloudArrowDownIcon,
WalletIcon,
} from '@heroicons/react/24/outline';
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/20/solid';
import Layout from '../../components/Layout';
import { mockDashboardStats, mockTransactions, mockAppDownloads, mockAccountBalances } from '../../data/mockData';
const stats = [
{
id: 1,
name: 'Total MTM to NYM Conversions',
stat: mockDashboardStats.totalTransactions.toLocaleString(),
icon: ArrowsRightLeftIcon,
},
{
id: 2,
name: 'Successful Conversions',
stat: mockDashboardStats.successfulTransactions.toLocaleString(),
icon: CheckCircleIcon,
},
{
id: 3,
name: 'Failed Conversions',
stat: mockDashboardStats.failedTransactions.toLocaleString(),
icon: XCircleIcon,
},
{
id: 4,
name: 'Total Downloads',
stat: mockDashboardStats.totalDownloads.toLocaleString(),
icon: CloudArrowDownIcon,
},
];
// Generate mock chart data
const transactionChartData = [
{ name: 'Jan', conversions: 850 },
{ name: 'Feb', conversions: 920 },
{ name: 'Mar', conversions: 1150 },
{ name: 'Apr', conversions: 980 },
{ name: 'May', conversions: 1200 },
{ name: 'Jun', conversions: 1350 },
{ name: 'Jul', conversions: 1180 },
{ name: 'Aug', conversions: mockDashboardStats.totalTransactions },
];
const downloadChartData = mockAppDownloads.map(download => ({
version: download.version,
downloads: download.downloads,
platform: download.platform,
}));
import { mockAccountBalances } from '../../data/mockData';
import dashboardApi, { ApiError, DashboardStats, TransactionData, MonthlyData } from '../../utils/api';
export default function Dashboard() {
const [dashboardStats, setDashboardStats] = useState<DashboardStats | null>(null);
const [recentTransactions, setRecentTransactions] = useState<TransactionData[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
try {
setIsLoading(true);
// Fetch dashboard stats from MTM service
const statsData = await dashboardApi.getStats();
setDashboardStats(statsData);
// Fetch recent conversions from MTM service
const conversionsData = await dashboardApi.getConversions({
limit: 5,
status: 'all'
});
// Use conversion data directly
setRecentTransactions(conversionsData.transactions);
} catch (err) {
if (err instanceof ApiError) {
setError(`API Error: ${err.message}`);
} else {
setError(err instanceof Error ? err.message : 'An error occurred');
}
} finally {
setIsLoading(false);
}
};
const getStats = () => {
if (!dashboardStats) return [];
return [
{
id: 1,
name: 'Total MTM to NYM Conversions',
stat: dashboardStats.totalConversions.toLocaleString(),
icon: ArrowsRightLeftIcon,
},
{
id: 2,
name: 'Successful Conversions',
stat: dashboardStats.successfulConversions.toLocaleString(),
icon: CheckCircleIcon,
},
{
id: 3,
name: 'Failed Conversions',
stat: dashboardStats.failedConversions.toLocaleString(),
icon: XCircleIcon,
},
{
id: 4,
name: 'Total Downloads',
stat: dashboardStats.totalDownloads.toLocaleString(),
icon: CloudArrowDownIcon,
},
];
};
if (isLoading) {
return (
<Layout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
</Layout>
);
}
if (error) {
return (
<Layout>
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading dashboard</h3>
<div className="mt-2 text-sm text-red-700">
<p>{error}</p>
</div>
<div className="mt-4">
<button
onClick={fetchDashboardData}
className="bg-red-100 px-2 py-1 text-sm text-red-800 rounded hover:bg-red-200"
>
Retry
</button>
</div>
</div>
</div>
</div>
</Layout>
);
}
const stats = getStats();
return (
<Layout>
<div>
@@ -89,21 +148,27 @@ export default function Dashboard() {
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">MTM to NYM Conversions Trend</h3>
<div className="h-80 flex items-end justify-between space-x-2 border-b border-l border-gray-200 p-4">
{transactionChartData.map((data, index) => (
<div key={data.name} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-blue-500 rounded-t transition-all duration-500"
style={{
height: `${(data.conversions / Math.max(...transactionChartData.map(d => d.conversions))) * 100}%`,
minHeight: '4px'
}}
/>
<div className="mt-2 text-xs text-gray-500 font-medium">{data.name}</div>
<div className="text-xs text-gray-400">{data.conversions.toLocaleString()}</div>
</div>
))}
</div>
{dashboardStats && (
<div className="h-80 flex items-end justify-between space-x-2 border-b border-l border-gray-200 p-4">
{/* Monthly data from API */}
{dashboardStats.monthlyData && dashboardStats.monthlyData.map((data, index) => {
const maxConversions = Math.max(...dashboardStats.monthlyData.map(d => d.totalConversions));
return (
<div key={data.month} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-blue-500 rounded-t transition-all duration-500"
style={{
height: `${Math.max((data.totalConversions / Math.max(maxConversions, 1)) * 300, 8)}px`,
minHeight: '8px'
}}
/>
<div className="mt-2 text-xs text-gray-500 font-medium">{data.month}</div>
<div className="text-xs text-gray-400">{data.totalConversions.toLocaleString()}</div>
</div>
);
})}
</div>
)}
</div>
</div>
@@ -167,7 +232,7 @@ export default function Dashboard() {
</a>
</div>
<ul className="divide-y divide-gray-200">
{mockTransactions.slice(0, 5).map((transaction) => (
{recentTransactions.map((transaction) => (
<li key={transaction.id}>
<div className="px-4 py-4 flex items-center justify-between">
<div className="flex items-center">
@@ -180,16 +245,19 @@ export default function Dashboard() {
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{transaction.transactionHash.slice(0, 8)}...{transaction.transactionHash.slice(-8)}
{`${transaction.transactionHash.slice(0, 8)}...${transaction.transactionHash.slice(-8)}`}
</div>
<div className="text-sm text-gray-500">
From: {transaction.fromAddress.slice(0, 8)}...{transaction.fromAddress.slice(-8)}
{transaction.error ?
`Error: ${transaction.error.substring(0, 40)}...` :
`From: ${transaction.fromAddress.slice(0, 8)}...${transaction.fromAddress.slice(-8)}`
}
</div>
</div>
</div>
<div className="text-sm text-gray-900">
<span suppressHydrationWarning>
{transaction.createdAt.toLocaleDateString()}
{new Date(transaction.createdAt).toLocaleDateString()}
</span>
</div>
</div>
@@ -201,4 +269,4 @@ export default function Dashboard() {
</div>
</Layout>
);
}
}
+11 -25
View File
@@ -38,32 +38,18 @@ export interface AppDownload {
fileSize: string;
}
export interface SupportTicket {
id: string;
userId: string;
email: string;
subject: string;
message: string;
status: 'open' | 'in-progress' | 'resolved' | 'closed';
priority: 'low' | 'medium' | 'high' | 'critical';
createdAt: Date;
updatedAt: Date;
responses: SupportResponse[];
}
export interface SupportResponse {
id: string;
ticketId: string;
message: string;
isAdminResponse: boolean;
createdAt: Date;
}
export interface DashboardStats {
totalTransactions: number;
successfulTransactions: number;
failedTransactions: number;
totalVolume: string;
activeUsers: number;
totalConversions: number;
successfulConversions: number;
failedConversions: number;
totalDownloads: number;
monthlyData: MonthlyData[];
}
export interface MonthlyData {
month: string;
totalConversions: number;
successfulConversions: number;
failedConversions: number;
}
+109
View File
@@ -0,0 +1,109 @@
const MTM_SERVICE_URL = process.env.NEXT_PUBLIC_MTM_SERVICE_URL || 'http://localhost:3000';
export interface MonthlyData {
month: string;
totalConversions: number;
successfulConversions: number;
failedConversions: number;
}
export interface DashboardStats {
totalConversions: number;
successfulConversions: number;
failedConversions: number;
totalDownloads: number;
monthlyData: MonthlyData[];
}
export interface TransactionData {
id: string;
transactionHash: string;
fromAddress: string;
nymTransactionHash?: string;
error?: string | null;
createdAt: string;
}
export interface TransactionsResponse {
transactions: TransactionData[];
totalCount: number;
pagination: {
page: number;
limit: number;
totalPages: number;
};
}
export class ApiError extends Error {
constructor(
message: string,
public status: number,
public response?: any
) {
super(message);
this.name = 'ApiError';
}
}
async function apiRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${MTM_SERVICE_URL}${endpoint}`;
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP ${response.status}: ${response.statusText}`,
response.status,
errorData
);
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
// Network or parsing errors
throw new ApiError(
error instanceof Error ? error.message : 'Unknown error occurred',
0
);
}
}
export const dashboardApi = {
// Get dashboard statistics
getStats: (): Promise<DashboardStats> => apiRequest<DashboardStats>('/api/dashboard/stats'),
// Get conversions with pagination and filtering
getConversions: (params?: {
page?: number;
limit?: number;
status?: string;
type?: string;
}): Promise<TransactionsResponse> => {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', params.page.toString());
if (params?.limit) searchParams.set('limit', params.limit.toString());
if (params?.status) searchParams.set('status', params.status);
if (params?.type) searchParams.set('type', params.type);
const query = searchParams.toString();
const endpoint = `/api/transactions/conversions${query ? `?${query}` : ''}`;
return apiRequest<TransactionsResponse>(endpoint);
},
};
export default dashboardApi;