VE router dashboard frontend and backend code

This commit is contained in:
vipeesh.p 2024-11-16 15:06:44 +05:30
commit d9fd7edad5
80 changed files with 12813 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
.env
*.log

23
backend/.env Normal file
View File

@ -0,0 +1,23 @@
# Server Configuration
NODE_ENV=development
PORT=3000
CORS_ORIGIN=http://localhost:5173,http://localhost:3000
# Database Configuration
DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=rootpassword
DB_NAME=ve_router_db
DB_CONNECTION_LIMIT=10
# Authentication Configuration
JWT_SECRET=your-super-secure-jwt-secret-key
JWT_EXPIRES_IN=1d
SALT_ROUNDS=10
# Logging Configuration
LOG_LEVEL=info
LOG_FILENAME=app.log

24
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

45
backend/README.md Normal file
View File

@ -0,0 +1,45 @@
# VE Router Backend
## Setup
1. Install dependencies:
\\\ash
npm install
\\\
2. Configure environment variables:
\\\ash
cp .env.example .env
# Edit .env with your configuration
\\\
3. Start development server:
\\\ash
npm run dev
\\\
## Scripts
- \
pm start\: Start production server
- \
pm run dev\: Start development server
- \
pm run build\: Build the project
- \
pm test\: Run tests
- \
pm run lint\: Lint code
- \
pm run format\: Format code
## Project Structure
\\\
src/
├── config/ # Configuration files
├── controllers/ # Request handlers
├── middleware/ # Express middleware
├── repositories/ # Data access layer
├── routes/ # API routes
├── services/ # Business logic
├── types/ # TypeScript types
└── utils/ # Utility functions
\\\

267
backend/backend-setup.ps1 Normal file
View File

@ -0,0 +1,267 @@
# setup.ps1
$projectName = "ve-router-backend"
# Colors for output
$green = "`e[0;32m"
$blue = "`e[0;34m"
$nc = "`e[0m" # No Color
Write-Host "$blue Creating project structure for $projectName... $nc"
# Create project directory
New-Item -ItemType Directory -Force -Name $projectName
Set-Location -Path $projectName
# Create package.json
Write-Host "$blue Initializing package.json... $nc"
$packageJson = @"
{
"name": "ve-router-backend",
"version": "1.0.0",
"description": "Router Management System Backend",
"main": "dist/app.js",
"scripts": {
"start": "node dist/app.js",
"dev": "nodemon src/app.ts",
"build": "tsc",
"test": "jest",
"lint": "eslint src/**/*.ts",
"format": "prettier --write \"src/**/*.ts\""
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"mysql2": "^3.2.0",
"winston": "^3.8.2"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"eslint": "^8.37.0",
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"prettier": "^2.8.7",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.3"
}
}
"@
$packageJson | Out-File -FilePath "package.json" -Encoding UTF8
# Create directory structure
Write-Host "$blue Creating directory structure... $nc"
$dirs = @("src/config", "src/controllers", "src/middleware", "src/repositories", "src/routes", "src/services", "src/types", "src/utils", "tests/integration", "tests/unit/services")
$dirs | ForEach-Object { New-Item -ItemType Directory -Force -Path $_ }
# Create TypeScript configuration
Write-Host "$blue Creating TypeScript configuration... $nc"
$tsconfig = @"
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
"@
$tsconfig | Out-File -FilePath "tsconfig.json" -Encoding UTF8
# Create environment files
Write-Host "$blue Creating environment files... $nc"
$envExample = @"
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=ve_router_db
LOG_LEVEL=info
JWT_SECRET=your_jwt_secret
CORS_ORIGIN=http://localhost:3000
"
$envExample | Out-File -FilePath ".env.example" -Encoding UTF8
Copy-Item -Path ".env.example" -Destination ".env"
# Create .gitignore
Write-Host "$blue Creating .gitignore... $nc"
$gitignore = @"
node_modules/
dist/
.env
*.log
coverage/
.DS_Store
"
$gitignore | Out-File -FilePath ".gitignore" -Encoding UTF8
# Create source files
Write-Host "$blue Creating source files... $nc"
# Config files
$srcConfig = @"
import dotenv from 'dotenv';
dotenv.config();
export const config = {
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '3000', 10),
db: {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 've_router_db'
},
logLevel: process.env.LOG_LEVEL || 'info',
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000'
};
"@
$srcConfig | Out-File -FilePath "src/config/config.ts" -Encoding UTF8
$srcDb = @"
import mysql from 'mysql2/promise';
import { config } from './config';
const pool = mysql.createPool({
host: config.db.host,
user: config.db.user,
password: config.db.password,
database: config.db.database,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
export default pool;
"@
$srcDb | Out-File -FilePath "src/config/db.ts" -Encoding UTF8
# Types
$srcTypes = @"
export interface Study {
siuid: string;
patientId: string;
accessionNumber: string;
patientName: string;
studyDate: string;
modality: string;
studyDescription: string;
}
export interface VM {
id: number;
status: string;
}
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
export interface RouterData {
id: number;
slNo: number;
routerId: string;
facility: string;
routerAlias: string;
lastSeen: string;
diskStatus: string;
diskUsage: number;
freeDisk: number;
totalDisk: number;
routerActivity: {
studies: Study[];
};
systemStatus: {
vpnStatus: string;
appStatus: string;
vms: VM[];
};
}
"@
$srcTypes | Out-File -FilePath "src/types/index.ts" -Encoding UTF8
# Create barrel files for each directory
Write-Host "$blue Creating barrel files... $nc"
$directories = @("controllers", "middleware", "repositories", "routes", "services", "utils")
foreach ($dir in $directories) {
"export * from './$dir';" | Out-File -FilePath "src/$dir/index.ts" -Encoding UTF8
}
# Create README.md
Write-Host "$blue Creating README.md... $nc"
$readme = @"
# VE Router Backend
## Setup
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Configure environment variables:
\`\`\`bash
cp .env.example .env
# Edit .env with your configuration
\`\`\`
3. Start development server:
\`\`\`bash
npm run dev
\`\`\`
## Scripts
- \`npm start\`: Start production server
- \`npm run dev\`: Start development server
- \`npm run build\`: Build the project
- \`npm test\`: Run tests
- \`npm run lint\`: Lint code
- \`npm run format\`: Format code
## Project Structure
\`\`\`
src/
config/ # Configuration files
controllers/ # Request handlers
middleware/ # Express middleware
repositories/ # Data access layer
routes/ # API routes
services/ # Business logic
types/ # TypeScript types
utils/ # Utility functions
\`\`\`
"@
$readme | Out-File -FilePath "README.md" -Encoding UTF8
# Initialize git repository
Write-Host "$blue Initializing git repository... $nc"
git init
git add .
git commit -m "Initial commit"
# Install dependencies
Write-Host "$blue Installing dependencies... $nc"
npm install
Write-Host "$green Project setup complete! $nc"
Write-Host "$green To get started: $nc"
Write-Host "1. cd $projectName"
Write-Host "2. Edit .env with your configuration"
Write-Host "3. npm run dev"

49
backend/barrel-setup.ps1 Normal file
View File

@ -0,0 +1,49 @@
# For utils directory
$utilsContent = @"
export * from './logger';
export * from './validators';
export * from './formatters';
// Add more utility exports as needed
"@
$utilsContent | Out-File -FilePath "src/utils/index.ts" -Encoding UTF8
# For services directory
$servicesContent = @"
export * from './RouterService';
export * from './UserService';
// Add more service exports as needed
"@
$servicesContent | Out-File -FilePath "src/services/index.ts" -Encoding UTF8
# For controllers directory
$controllersContent = @"
export * from './RouterController';
export * from './UserController';
// Add more controller exports as needed
"@
$controllersContent | Out-File -FilePath "src/controllers/index.ts" -Encoding UTF8
# For repositories directory
$repositoriesContent = @"
export * from './RouterRepository';
export * from './UserRepository';
// Add more repository exports as needed
"@
$repositoriesContent | Out-File -FilePath "src/repositories/index.ts" -Encoding UTF8
# For middleware directory
$middlewareContent = @"
export * from './auth';
export * from './errorHandler';
export * from './validate';
// Add more middleware exports as needed
"@
$middlewareContent | Out-File -FilePath "src/middleware/index.ts" -Encoding UTF8
# For routes directory
$routesContent = @"
export * from './router.routes';
export * from './user.routes';
// Add more route exports as needed
"@
$routesContent | Out-File -FilePath "src/routes/index.ts" -Encoding UTF8

16
backend/dockerfile Normal file
View File

@ -0,0 +1,16 @@
# ve-router-backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]

4
backend/dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
.env
*.log

3607
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
backend/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "ve-router-backend",
"version": "1.0.0",
"description": "Router Management System Backend",
"main": "dist/app.js",
"scripts": {
"start": "node dist/app.js",
"dev": "nodemon --exec ts-node src/app.ts",
"build": "tsc",
"test": "jest",
"lint": "eslint src/**/*.ts",
"format": "prettier --write \"src/**/*.ts\""
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"mysql2": "^3.2.0",
"ve-router-backend": "file:",
"winston": "^3.16.0"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"eslint": "^8.37.0",
"nodemon": "^2.0.22",
"ts-node": "^10.9.2",
"typescript": "^5.0.3"
}
}

61
backend/src/app.ts Normal file
View File

@ -0,0 +1,61 @@
// src/app.ts
import express from 'express';
import cors from 'cors';
import config from './config/config';
import routes from './routes';
import { errorHandler } from './middleware';
import logger from './utils/logger';
const app = express();
// Debug middleware to log all incoming requests
app.use((req, res, next) => {
logger.debug(`Incoming request: ${req.method} ${req.originalUrl}`);
logger.debug('Request body:', req.body);
next();
});
// CORS configuration
app.use(cors({
origin: ['http://localhost:5173', 'http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
// Routes
logger.info(`Registering routes with prefix: ${config.server.apiPrefix}`);
app.use(config.server.apiPrefix, routes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// Error handling
app.use(errorHandler);
// 404 handler - Debug version
app.use((req, res) => {
logger.warn('404 Not Found:', {
method: req.method,
path: req.path,
originalUrl: req.originalUrl,
body: req.body
});
res.status(404).json({
error: 'Not Found',
path: req.path,
originalUrl: req.originalUrl
});
});
const port = config.server.port || 3000;
app.listen(port, () => {
logger.info(`Server running on port ${port} in ${config.env} mode`);
logger.info(`API endpoints available at http://localhost:${port}${config.server.apiPrefix}`);
});
export default app;

View File

@ -0,0 +1,38 @@
// src/config/config.ts
import dotenv from 'dotenv';
// Initialize dotenv at the very start
dotenv.config();
const config = {
env: process.env.NODE_ENV || 'development',
server: {
port: parseInt(process.env.PORT || '3000', 10),
// Update this to include your frontend URL
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:5173', 'http://localhost:3000'],
apiPrefix: '/api/v1'
},
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306', 10),
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '', // Make sure this is getting the password
database: process.env.DB_NAME || 've_router_db',
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10', 10)
},
logger: {
level: process.env.LOG_LEVEL || 'info',
filename: process.env.LOG_FILENAME || 'app.log'
}
};
// Add this for debugging
console.log('Database Config:', {
host: config.db.host,
user: config.db.user,
database: config.db.database,
// Don't log the password
});
export default config;

31
backend/src/config/db.ts Normal file
View File

@ -0,0 +1,31 @@
// src/config/db.ts
import mysql from 'mysql2/promise';
import config from './config';
import logger from '../utils/logger';
const pool = mysql.createPool({
host: config.db.host,
user: config.db.user,
password: config.db.password, // Make sure this is being passed
database: config.db.database,
port: config.db.port,
connectionLimit: config.db.connectionLimit,
waitForConnections: true,
queueLimit: 0
});
// Test connection
pool.getConnection()
.then(connection => {
logger.info('Database connected successfully');
connection.release();
})
.catch(error => {
logger.error('Database connection failed:', {
message: error.message,
code: error.code,
errno: error.errno
});
});
export default pool;

View File

@ -0,0 +1,2 @@
export { default as config } from './config';
export { default as db } from './db';

View File

@ -0,0 +1,210 @@
// src/controllers/DicomStudyController.ts
import { Request, Response, NextFunction } from 'express';
import { DicomStudyService } from '../services/DicomStudyService';
import logger from '../utils/logger';
interface ApiError {
message: string;
code?: string;
stack?: string;
}
export class DicomStudyController {
private service: DicomStudyService;
constructor() {
this.service = new DicomStudyService();
}
private handleError(error: unknown, message: string): ApiError {
const apiError: ApiError = {
message: message
};
if (error instanceof Error) {
logger.error(`${message}: ${error.message}`);
if (process.env.NODE_ENV === 'development') {
apiError.message = error.message;
apiError.stack = error.stack;
}
} else {
logger.error(`${message}: Unknown error type`, error);
}
return apiError;
}
getAllStudies = async (req: Request, res: Response, next: NextFunction) => {
try {
const studies = await this.service.getAllStudies();
res.json(studies);
} catch (error) {
const apiError = this.handleError(error, 'Failed to fetch studies');
res.status(500).json({ error: apiError });
}
};
getStudyById = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid study ID' });
}
const study = await this.service.getStudyById(id);
if (!study) {
return res.status(404).json({ error: 'Study not found' });
}
res.json(study);
} catch (error) {
const apiError = this.handleError(error, `Failed to fetch study ${req.params.id}`);
res.status(500).json({ error: apiError });
}
};
getStudiesByRouterId = async (req: Request, res: Response, next: NextFunction) => {
try {
const routerId = req.params.routerId;
if (!routerId || typeof routerId !== 'string') {
return res.status(400).json({ error: 'Invalid router ID' });
}
const studies = await this.service.getStudiesByRouterId(routerId);
res.json(studies);
} catch (error) {
const apiError = this.handleError(error, `Failed to fetch studies for router ${req.params.routerId}`);
// If router not found, return 404
if (error instanceof Error && error.message.includes('Invalid router_id')) {
return res.status(404).json({ error: 'Router not found' });
}
res.status(500).json({ error: apiError });
}
};
createStudy = async (req: Request, res: Response, next: NextFunction) => {
try {
const requiredFields = [
'router_id',
'study_instance_uid',
'patient_id',
'patient_name',
'accession_number',
'study_date',
'modality',
'series_instance_uid',
'study_status_code',
'association_id'
];
const missingFields = requiredFields.filter(field => !req.body[field]);
if (missingFields.length > 0) {
return res.status(400).json({
error: 'Missing required fields',
missingFields
});
}
// Validate router_id format if needed
if (typeof req.body.router_id !== 'string') {
return res.status(400).json({
error: 'Invalid router_id format',
details: 'router_id must be a string'
});
}
const study = await this.service.createStudy(req.body);
res.status(201).json(study);
} catch (error) {
const apiError = this.handleError(error, 'Failed to create study');
// Handle specific error cases
if (error instanceof Error) {
if (error.message.includes('Missing required field')) {
return res.status(400).json({ error: apiError });
}
if (error.message.includes('Invalid router_id')) {
return res.status(404).json({ error: 'Router not found' });
}
if (error.message.includes('Invalid study status code')) {
return res.status(400).json({ error: apiError });
}
if (error.message.includes('Invalid study date format')) {
return res.status(400).json({ error: apiError });
}
}
res.status(500).json({ error: apiError });
}
};
updateStudy = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid study ID' });
}
const study = await this.service.updateStudy(id, req.body);
if (!study) {
return res.status(404).json({ error: 'Study not found' });
}
res.json(study);
} catch (error) {
const apiError = this.handleError(error, `Failed to update study ${req.params.id}`);
res.status(500).json({ error: apiError });
}
};
deleteStudy = async (req: Request, res: Response, next: NextFunction) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid study ID' });
}
const success = await this.service.deleteStudy(id);
if (!success) {
return res.status(404).json({ error: 'Study not found' });
}
res.status(204).send();
} catch (error) {
const apiError = this.handleError(error, `Failed to delete study ${req.params.id}`);
res.status(500).json({ error: apiError });
}
};
searchStudies = async (req: Request, res: Response, next: NextFunction) => {
try {
const { startDate, endDate, modality, patientName } = req.query;
if (startDate && typeof startDate !== 'string') {
return res.status(400).json({
error: 'Invalid startDate format',
expected: 'YYYY-MM-DD'
});
}
if (endDate && typeof endDate !== 'string') {
return res.status(400).json({
error: 'Invalid endDate format',
expected: 'YYYY-MM-DD'
});
}
const studies = await this.service.searchStudies({
startDate: startDate as string,
endDate: endDate as string,
modality: modality as string,
patientName: patientName as string
});
res.json(studies);
} catch (error) {
const apiError = this.handleError(error, 'Failed to search studies');
res.status(500).json({ error: apiError });
}
};
}

View File

@ -0,0 +1,135 @@
// src/controllers/RouterController.ts
import { Request, Response } from 'express';
import { RouterService } from '../services/RouterService';
import { Pool } from 'mysql2/promise';
import { RouterData, VMUpdate, VMUpdateRequest } from '../types';
export class RouterController {
private service: RouterService;
constructor(pool: Pool) {
this.service = new RouterService(pool);
}
// src/controllers/RouterController.ts
// src/controllers/RouterController.ts
updateRouterVMs = async (req: Request, res: Response) => {
try {
const routerId = req.query.router_id as string;
const { vms } = req.body;
// Add debugging logs
console.log('Received request:');
console.log('Router ID:', routerId);
console.log('VMs data:', vms);
if (!routerId) {
return res.status(400).json({ error: 'router_id is required' });
}
if (!Array.isArray(vms)) {
return res.status(400).json({ error: 'VMs must be an array' });
}
const updatedVMs = await this.service.updateRouterVMs(routerId, vms);
res.json(updatedVMs);
} catch (err) {
// Type cast the error
const error = err as Error;
// Enhanced error logging
console.error('Error in updateRouterVMs:', {
message: error?.message || 'Unknown error',
stack: error?.stack,
type: error?.constructor.name
});
res.status(500).json({
error: 'Failed to update VMs',
details: error?.message || 'Unknown error'
});
}
};
getAllRouters = async (req: Request, res: Response) => {
try {
const routers = await this.service.getAllRouters();
res.json(routers);
} catch (error: unknown) {
if (error && typeof error === 'object' && 'message' in error) {
const e = error as { message: string }; // Type assertion here
res.status(500).json({ error: e.message });
} else {
res.status(500).json({ error: 'An unexpected error occurred' });
}
}
};
getRouterById = async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const router = await this.service.getRouterById(id);
if (!router) {
return res.status(404).json({ error: 'Router not found' });
}
res.json(router);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch router' });
}
};
createRouter = async (req: Request, res: Response) => {
try {
const router = await this.service.createRouter(req.body);
res.status(201).json(router);
} catch (error) {
res.status(500).json({ error: 'Failed to create router' });
}
};
updateRouter = async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const router = await this.service.updateRouter(id, req.body);
if (!router) {
return res.status(404).json({ error: 'Router not found' });
}
res.json(router);
} catch (error) {
res.status(500).json({ error: 'Failed to update router' });
}
};
deleteRouter = async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const success = await this.service.deleteRouter(id);
if (!success) {
return res.status(404).json({ error: 'Router not found' });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Failed to delete router' });
}
};
getRoutersByFacility = async (req: Request, res: Response) => {
try {
const facility = req.params.facility;
const routers = await this.service.getRoutersByFacility(facility);
res.json(routers);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch routers by facility' });
}
};
}

View File

@ -0,0 +1,3 @@
export * from './RouterController';
export * from './DicomStudyController';
// Add more controller exports as needed

View File

@ -0,0 +1,22 @@
import { Request, Response, NextFunction } from 'express';
import config from '../config/config';
import logger from '../utils/logger';
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
logger.error('Error:', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
res.status(500).json({
error: 'Internal Server Error',
message: config.env === 'development' ? err.message : undefined
});
};

View File

@ -0,0 +1,2 @@
export * from './errorHandler';
// Add more middleware exports as needed

View File

@ -0,0 +1,249 @@
// src/repositories/DicomStudyRepository.ts
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DBDicomStudy, DicomStudySearchParams } from '../types/dicom';
import pool from '../config/db';
import logger from '../utils/logger';
export class DicomStudyRepository {
private async getRouterStringId(numericId: number): Promise<string> {
try {
const [result] = await pool.query(
'SELECT router_id FROM routers WHERE id = ?',
[numericId]
);
const rows = result as any[];
if (rows.length === 0) {
throw new Error(`Router not found with id: ${numericId}`);
}
return rows[0].router_id;
} catch (error) {
logger.error('Error getting router string ID:', error);
throw error;
}
}
private async getRouterNumericId(routerId: string): Promise<number> {
try {
const [result] = await pool.query(
'SELECT id FROM routers WHERE router_id = ?',
[routerId]
);
const rows = result as any[];
if (rows.length === 0) {
throw new Error(`Router not found with router_id: ${routerId}`);
}
return rows[0].id;
} catch (error) {
logger.error('Error getting router numeric ID:', error);
throw error;
}
}
private async mapDBStudyToDicomStudy(dbStudy: DBDicomStudy): Promise<DicomStudy> {
const routerStringId = await this.getRouterStringId(dbStudy.router_id);
return {
id: dbStudy.id,
router_id: routerStringId,
study_instance_uid: dbStudy.study_instance_uid,
patient_id: dbStudy.patient_id,
patient_name: dbStudy.patient_name,
accession_number: dbStudy.accession_number,
study_date: dbStudy.study_date,
modality: dbStudy.modality,
study_description: dbStudy.study_description,
series_instance_uid: dbStudy.series_instance_uid,
procedure_code: dbStudy.procedure_code,
referring_physician_name: dbStudy.referring_physician_name,
study_status_code: dbStudy.study_status_code,
association_id: dbStudy.association_id,
created_at: dbStudy.created_at,
updated_at: dbStudy.updated_at
};
}
async create(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
try {
// Convert string router_id to numeric id for database
const numericRouterId = await this.getRouterNumericId(studyData.router_id);
const [result] = await pool.query(
`INSERT INTO dicom_study_overview (
router_id, study_instance_uid, patient_id, patient_name,
accession_number, study_date, modality, study_description,
series_instance_uid, procedure_code, referring_physician_name,
study_status_code, association_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
numericRouterId,
studyData.study_instance_uid,
studyData.patient_id,
studyData.patient_name,
studyData.accession_number,
studyData.study_date,
studyData.modality,
studyData.study_description,
studyData.series_instance_uid,
studyData.procedure_code,
studyData.referring_physician_name,
studyData.study_status_code,
studyData.association_id
]
);
// Get the created study with the correct router_id format
const insertId = (result as any).insertId;
return await this.findById(insertId) as DicomStudy;
} catch (error) {
logger.error('Error creating DICOM study:', error);
throw new Error('Failed to create DICOM study');
}
}
async findAll(): Promise<DicomStudy[]> {
try {
const [rows] = await pool.query(`
SELECT d.*, r.router_id as router_string_id
FROM dicom_study_overview d
JOIN routers r ON d.router_id = r.id
ORDER BY d.created_at DESC
`);
return Promise.all((rows as DBDicomStudy[]).map(row => this.mapDBStudyToDicomStudy(row)));
} catch (error) {
logger.error('Error fetching all DICOM studies:', error);
throw new Error('Failed to fetch DICOM studies');
}
}
async findById(id: number): Promise<DicomStudy | null> {
try {
const [rows] = await pool.query(`
SELECT d.*, r.router_id as router_string_id
FROM dicom_study_overview d
JOIN routers r ON d.router_id = r.id
WHERE d.id = ?
`, [id]);
const results = rows as DBDicomStudy[];
if (results.length === 0) {
return null;
}
return await this.mapDBStudyToDicomStudy(results[0]);
} catch (error) {
logger.error('Error fetching DICOM study by ID:', error);
throw new Error('Failed to fetch DICOM study');
}
}
async findByRouterId(routerId: string): Promise<DicomStudy[]> {
try {
const numericRouterId = await this.getRouterNumericId(routerId);
const [rows] = await pool.query(`
SELECT d.*, r.router_id as router_string_id
FROM dicom_study_overview d
JOIN routers r ON d.router_id = r.id
WHERE d.router_id = ?
ORDER BY d.created_at DESC
`, [numericRouterId]);
return Promise.all((rows as DBDicomStudy[]).map(row => this.mapDBStudyToDicomStudy(row)));
} catch (error) {
logger.error('Error fetching DICOM studies by router ID:', error);
throw new Error('Failed to fetch DICOM studies');
}
}
async update(id: number, studyData: UpdateDicomStudyDTO): Promise<DicomStudy | null> {
try {
// First check if study exists
const existingStudy = await this.findById(id);
if (!existingStudy) {
return null;
}
// Build update query dynamically based on provided fields
const updateFields: string[] = [];
const updateValues: any[] = [];
Object.entries(studyData).forEach(([key, value]) => {
if (value !== undefined) {
updateFields.push(`${key} = ?`);
updateValues.push(value);
}
});
if (updateFields.length > 0) {
// Add updated_at timestamp
updateFields.push('updated_at = CURRENT_TIMESTAMP');
// Add id for WHERE clause
updateValues.push(id);
await pool.query(`
UPDATE dicom_study_overview
SET ${updateFields.join(', ')}
WHERE id = ?
`, updateValues);
}
// Return updated study
return await this.findById(id);
} catch (error) {
logger.error('Error updating DICOM study:', error);
throw new Error('Failed to update DICOM study');
}
}
async delete(id: number): Promise<boolean> {
try {
const [result] = await pool.query(
'DELETE FROM dicom_study_overview WHERE id = ?',
[id]
);
return (result as any).affectedRows > 0;
} catch (error) {
logger.error('Error deleting DICOM study:', error);
throw new Error('Failed to delete DICOM study');
}
}
async search(params: DicomStudySearchParams): Promise<DicomStudy[]> {
try {
let query = `
SELECT d.*, r.router_id as router_string_id
FROM dicom_study_overview d
JOIN routers r ON d.router_id = r.id
WHERE 1=1
`;
const values: any[] = [];
if (params.startDate) {
query += ' AND d.study_date >= ?';
values.push(params.startDate);
}
if (params.endDate) {
query += ' AND d.study_date <= ?';
values.push(params.endDate);
}
if (params.modality) {
query += ' AND d.modality = ?';
values.push(params.modality);
}
if (params.patientName) {
query += ' AND d.patient_name LIKE ?';
values.push(`%${params.patientName}%`);
}
// Add sorting
query += ' ORDER BY d.created_at DESC';
const [rows] = await pool.query(query, values);
return Promise.all((rows as DBDicomStudy[]).map(row => this.mapDBStudyToDicomStudy(row)));
} catch (error) {
logger.error('Error searching DICOM studies:', error);
throw new Error('Failed to search DICOM studies');
}
}
}

View File

@ -0,0 +1,260 @@
// src/repositories/RouterRepository.ts
import { RouterData, Study, VM,VMUpdate } from '../types';
import pool from '../config/db';
import { RowDataPacket, ResultSetHeader } from 'mysql2';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
export class RouterRepository {
constructor(private pool: Pool) {} // Modified constructor
// src/repositories/RouterRepository.ts
// src/repositories/RouterRepository.ts
async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
console.log('Repository: Starting VM update for router:', routerId);
const connection = await this.pool.getConnection();
try {
await connection.beginTransaction();
console.log('Started transaction');
// First verify router exists
const [routers] = await connection.query(
'SELECT id FROM routers WHERE router_id = ?',
[routerId]
);
console.log('Router query result:', routers);
if (!Array.isArray(routers) || routers.length === 0) {
throw new Error(`Router ${routerId} not found`);
}
// Log each VM update
for (const vm of vms) {
console.log(`Processing VM ${vm.vm_number}`);
const [result]: any = await connection.query(
`UPDATE vm_details
SET status_code = ?,
updated_at = CURRENT_TIMESTAMP
WHERE router_id = ? AND vm_number = ?`,
[vm.status_code, routerId, vm.vm_number]
);
console.log('Update result:', result);
if (!result.affectedRows) {
console.log('No rows affected, attempting insert');
await connection.query(
`INSERT INTO vm_details (router_id, vm_number, status_code)
VALUES (?, ?, ?)`,
[routerId, vm.vm_number, vm.status_code]
);
}
}
await connection.commit();
console.log('Transaction committed');
const [updatedVMs] = await connection.query(
`SELECT * FROM vm_details WHERE router_id = ? ORDER BY vm_number`,
[routerId]
);
return updatedVMs;
} catch (err) {
console.error('Repository error:', err);
await connection.rollback();
throw err;
} finally {
connection.release();
console.log('Connection released');
}
}
private async getRouterStudies(routerId: number): Promise<Study[]> {
try {
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT
study_instance_uid as siuid,
patient_id as patientId,
accession_number as accessionNumber,
patient_name as patientName,
DATE_FORMAT(study_date, '%Y-%m-%d') as studyDate,
modality,
study_description as studyDescription
FROM dicom_study_overview
WHERE router_id = ?
ORDER BY study_date DESC`,
[routerId]
);
return rows as Study[];
} catch (error) {
logger.error(`Error fetching studies for router ${routerId}:`, error);
return [];
}
}
private async getRouterVMs(routerId: number): Promise<VM[]> {
try {
// First get the router_id string from routers table
const [routerRow] = await pool.query<RowDataPacket[]>(
'SELECT router_id FROM routers WHERE id = ?',
[routerId]
);
if (!routerRow || !routerRow[0]) {
return [];
}
const routerIdString = routerRow[0].router_id;
// Then use this to query vm_details
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT
id,
status_code as status
FROM vm_details
WHERE router_id = ?`,
[routerIdString]
);
console.log(`VMs for router ${routerId} (${routerIdString}):`, rows);
return rows as VM[];
} catch (error) {
logger.error(`Error fetching VMs for router ${routerId}:`, error);
return [];
}
}
private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> {
try {
const studies = await this.getRouterStudies(dbRouter.id);
const vms = await this.getRouterVMs(dbRouter.id);
return {
id: dbRouter.id,
slNo: index + 1,
routerId: dbRouter.router_id,
facility: dbRouter.facility,
routerAlias: dbRouter.router_alias,
lastSeen: new Date(dbRouter.last_seen).toISOString(),
diskStatus: dbRouter.disk_status_code,
diskUsage: parseFloat(dbRouter.disk_usage),
freeDisk: parseInt(dbRouter.free_disk),
totalDisk: parseInt(dbRouter.total_disk),
routerActivity: {
studies
},
systemStatus: {
vpnStatus: dbRouter.vpn_status_code,
appStatus: dbRouter.disk_status_code,
vms
}
};
} catch (error) {
logger.error(`Error transforming router ${dbRouter.id}:`, error);
throw error;
}
}
async findAll(): Promise<RouterData[]> {
try {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM routers ORDER BY created_at DESC'
);
const routers = await Promise.all(
rows.map((row, index) => this.transformDatabaseRouter(row, index))
);
return routers;
} catch (error) {
logger.error('Error in RouterRepository.findAll:', error);
throw error;
}
}
async findById(id: number): Promise<RouterData | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM routers WHERE id = ?',
[id]
);
if (!rows.length) return null;
return this.transformDatabaseRouter(rows[0], 0);
}
async findByFacility(facility: string): Promise<RouterData[]> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM routers WHERE facility = ? ORDER BY created_at DESC',
[facility]
);
const routers = await Promise.all(
rows.map((row, index) => this.transformDatabaseRouter(row, index))
);
return routers;
}
async create(router: Partial<RouterData>): Promise<RouterData> {
const [result] = await pool.query<ResultSetHeader>(
`INSERT INTO routers (
router_id, facility, router_alias, last_seen,
vpn_status_code, disk_status_code, license_status,
free_disk, total_disk, disk_usage
) VALUES (?, ?, ?, NOW(), ?, ?, 'inactive', ?, ?, ?)`,
[
router.routerId,
router.facility,
router.routerAlias,
'unknown',
router.diskStatus || 'unknown',
router.freeDisk,
router.totalDisk,
router.diskUsage || 0
]
);
return this.findById(result.insertId) as Promise<RouterData>;
}
async update(id: number, router: Partial<RouterData>): Promise<RouterData | null> {
const updates: Record<string, any> = {};
if (router.routerId) updates.router_id = router.routerId;
if (router.facility) updates.facility = router.facility;
if (router.routerAlias) updates.router_alias = router.routerAlias;
if (router.diskStatus) updates.disk_status_code = router.diskStatus;
if (router.freeDisk !== undefined || router.totalDisk !== undefined) {
const existingRouter = await this.findById(id);
if (!existingRouter) return null;
updates.free_disk = router.freeDisk ?? existingRouter.freeDisk;
updates.total_disk = router.totalDisk ?? existingRouter.totalDisk;
updates.disk_usage = ((updates.total_disk - updates.free_disk) / updates.total_disk) * 100;
}
if (Object.keys(updates).length > 0) {
const setClauses = Object.keys(updates)
.map(key => `${key} = ?`)
.join(', ');
await pool.query(
`UPDATE routers SET ${setClauses}, updated_at = NOW() WHERE id = ?`,
[...Object.values(updates), id]
);
}
return this.findById(id);
}
async delete(id: number): Promise<boolean> {
const [result] = await pool.query<ResultSetHeader>(
'DELETE FROM routers WHERE id = ?',
[id]
);
return result.affectedRows > 0;
}
}

View File

@ -0,0 +1,3 @@
export * from './RouterRepository';
export * from './DicomStudyRepository';
// Add more repository exports as needed

View File

@ -0,0 +1,36 @@
// src/routes/dicom.routes.ts
import express from 'express';
import { DicomStudyController } from '../controllers/DicomStudyController';
import logger from '../utils/logger';
const router = express.Router();
const dicomStudyController = new DicomStudyController();
// Debug logging
logger.info('Initializing DICOM routes');
// Add debug middleware for DICOM routes
router.use((req, res, next) => {
logger.debug(`DICOM route hit: ${req.method} ${req.originalUrl}`);
next();
});
// Test route to verify DICOM routing is working
router.get('/test', (req, res) => {
res.json({ message: 'DICOM routes are working' });
});
router.post('/', (req, res, next) => {
logger.debug('POST / route hit with body:', req.body);
dicomStudyController.createStudy(req, res, next);
});
router.get('/', dicomStudyController.getAllStudies);
router.get('/search', dicomStudyController.searchStudies);
router.get('/router/:routerId', dicomStudyController.getStudiesByRouterId);
router.get('/:id', dicomStudyController.getStudyById);
router.put('/:id', dicomStudyController.updateStudy);
router.delete('/:id', dicomStudyController.deleteStudy);
export default router;

View File

@ -0,0 +1,23 @@
// src/routes/index.ts
import { Router } from 'express';
import routerRoutes from './router.routes';
import dicomRoutes from './dicom.routes';
import logger from '../utils/logger';
const router = Router();
// Add debug logging
logger.info('Registering routes:');
logger.info('- /routers -> router routes');
logger.info('- /studies -> dicom routes');
router.use('/routers', routerRoutes);
router.use('/studies', dicomRoutes);
// Debug middleware to log incoming requests
router.use((req, res, next) => {
logger.debug(`Route hit: ${req.method} ${req.originalUrl}`);
next();
});
export default router;

View File

@ -0,0 +1,34 @@
// src/routes/router.routes.ts
import { Router } from 'express';
import pool from '../config/db'; // If using default export
import { RouterController } from '../controllers/RouterController';
const router = Router();
const controller = new RouterController(pool);
router.put('/vms', async (req, res, next) => {
console.log('Route handler: /vms endpoint hit');
console.log('Query params:', req.query);
console.log('Request body:', req.body);
try {
await controller.updateRouterVMs(req, res);
} catch (err) {
console.error('Route error:', err);
next(err);
}
});
router.get('/', controller.getAllRouters);
router.get('/:id', controller.getRouterById);
router.post('/', controller.createRouter);
router.put('/:id', controller.updateRouter);
router.delete('/:id', controller.deleteRouter);
router.get('/facility/:facility', controller.getRoutersByFacility);
// Add this new route
router.put('/vms', controller.updateRouterVMs); // New route for VM updates
export default router;

View File

@ -0,0 +1,154 @@
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DicomStudySearchParams } from '../types/dicom';
import { DicomStudyRepository } from '../repositories/DicomStudyRepository';
import pool from '../config/db';
import logger from '../utils/logger';
export class DicomStudyService {
private repository: DicomStudyRepository;
constructor() {
this.repository = new DicomStudyRepository();
}
private async isValidStatusCode(statusCode: string): Promise<boolean> {
try {
const [rows] = await pool.query(
'SELECT 1 FROM status_type WHERE code = ? AND category_id = (SELECT id FROM status_category WHERE name = ?)',
[statusCode, 'DICOM_STUDY']
);
return (rows as any[]).length > 0;
} catch (error) {
logger.error('Error checking status code:', error);
return false;
}
}
async createStudy(studyData: CreateDicomStudyDTO): Promise<DicomStudy> {
// Validate required fields
const requiredFields = [
'router_id',
'study_instance_uid',
'patient_id',
'patient_name',
'accession_number',
'study_date',
'modality',
'series_instance_uid',
'study_status_code',
'association_id'
];
for (const field of requiredFields) {
if (!studyData[field as keyof CreateDicomStudyDTO]) {
throw new Error(`Missing required field: ${field}`);
}
}
// Validate status code
const isValidStatus = await this.isValidStatusCode(studyData.study_status_code);
if (!isValidStatus) {
throw new Error(`Invalid study status code: ${studyData.study_status_code}. Must be one of: NEW, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, ON_HOLD`);
}
// Validate date format
if (!this.isValidDate(studyData.study_date)) {
throw new Error('Invalid study date format. Use YYYY-MM-DD');
}
logger.info('Creating new study', { studyData });
return await this.repository.create(studyData);
}
private isValidDate(dateString: string): boolean {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateString)) return false;
const date = new Date(dateString);
return date instanceof Date && !isNaN(date.getTime());
}
async getAllStudies(): Promise<DicomStudy[]> {
try {
logger.info('Fetching all studies');
return await this.repository.findAll();
} catch (error) {
logger.error('Error in getAllStudies:', error);
throw new Error('Failed to fetch studies');
}
}
async getStudyById(id: number): Promise<DicomStudy | null> {
try {
logger.info(`Fetching study by id: ${id}`);
const study = await this.repository.findById(id);
if (!study) {
logger.warn(`Study not found with id: ${id}`);
}
return study;
} catch (error) {
logger.error(`Error in getStudyById: ${id}`, error);
throw new Error('Failed to fetch study');
}
}
async getStudiesByRouterId(routerId: string): Promise<DicomStudy[]> {
try {
logger.info(`Fetching studies for router: ${routerId}`);
return await this.repository.findByRouterId(routerId);
} catch (error) {
logger.error(`Error in getStudiesByRouterId: ${routerId}`, error);
throw new Error('Failed to fetch studies for router');
}
}
async updateStudy(id: number, studyData: UpdateDicomStudyDTO): Promise<DicomStudy | null> {
try {
const existingStudy = await this.repository.findById(id);
if (!existingStudy) {
logger.warn(`Study not found for update: ${id}`);
return null;
}
logger.info(`Updating study: ${id}`, { studyData });
return await this.repository.update(id, studyData);
} catch (error) {
logger.error(`Error in updateStudy: ${id}`, error);
throw new Error('Failed to update study');
}
}
async deleteStudy(id: number): Promise<boolean> {
try {
const existingStudy = await this.repository.findById(id);
if (!existingStudy) {
logger.warn(`Study not found for deletion: ${id}`);
return false;
}
logger.info(`Deleting study: ${id}`);
return await this.repository.delete(id);
} catch (error) {
logger.error(`Error in deleteStudy: ${id}`, error);
throw new Error('Failed to delete study');
}
}
async searchStudies(params: DicomStudySearchParams): Promise<DicomStudy[]> {
try {
if (params.startDate && !this.isValidDate(params.startDate)) {
throw new Error('Invalid start date format. Use YYYY-MM-DD');
}
if (params.endDate && !this.isValidDate(params.endDate)) {
throw new Error('Invalid end date format. Use YYYY-MM-DD');
}
logger.info('Searching studies with params:', params);
return await this.repository.search(params);
} catch (error) {
logger.error('Error in searchStudies:', error);
throw new Error('Failed to search studies');
}
}
}

View File

@ -0,0 +1,49 @@
// src/services/RouterService.ts
import { RouterRepository } from '../repositories/RouterRepository';
import { RouterData,VMUpdate} from '../types';
import { Pool } from 'mysql2/promise';
export class RouterService {
private repository: RouterRepository;
constructor(pool: Pool) {
this.repository = new RouterRepository(pool);
}
async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
console.log('Service: Updating VMs for router:', routerId);
try {
return await this.repository.updateVMs(routerId, vms);
} catch (err) {
console.error('Service error:', err);
throw err;
}
}
async getAllRouters(): Promise<RouterData[]> {
return this.repository.findAll();
}
async getRouterById(id: number): Promise<RouterData | null> {
return this.repository.findById(id);
}
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
return this.repository.findByFacility(facility);
}
async createRouter(router: Partial<RouterData>): Promise<RouterData> {
return this.repository.create(router);
}
async updateRouter(id: number, router: Partial<RouterData>): Promise<RouterData | null> {
return this.repository.update(id, router);
}
async deleteRouter(id: number): Promise<boolean> {
return this.repository.delete(id);
}
}

View File

@ -0,0 +1,3 @@
export * from './RouterService';
export * from './DicomStudyService';
// Add more service exports as needed

View File

@ -0,0 +1,74 @@
export interface DicomStudy {
id: number;
router_id: string; // External interface - always string
study_instance_uid: string;
patient_id: string;
patient_name: string;
accession_number: string;
study_date: string;
modality: string;
study_description?: string;
series_instance_uid: string;
procedure_code?: string;
referring_physician_name?: string;
study_status_code: string;
association_id: string;
created_at?: Date;
updated_at?: Date;
}
export interface CreateDicomStudyDTO {
router_id: string; // External interface - always string
study_instance_uid: string;
patient_id: string;
patient_name: string;
accession_number: string;
study_date: string;
modality: string;
study_description?: string;
series_instance_uid: string;
procedure_code?: string;
referring_physician_name?: string;
study_status_code: string;
association_id: string;
}
export interface DBDicomStudy {
id: number;
router_id: number; // Database uses numeric ID
study_instance_uid: string;
patient_id: string;
patient_name: string;
accession_number: string;
study_date: string;
modality: string;
study_description?: string;
series_instance_uid: string;
procedure_code?: string;
referring_physician_name?: string;
study_status_code: string;
association_id: string;
created_at?: Date;
updated_at?: Date;
}
export interface UpdateDicomStudyDTO {
study_instance_uid?: string;
patient_id?: string;
patient_name?: string;
accession_number?: string;
study_date?: string;
modality?: string;
study_description?: string;
series_instance_uid?: string;
procedure_code?: string;
referring_physician_name?: string;
study_status_code?: string;
}
export interface DicomStudySearchParams {
startDate?: string;
endDate?: string;
modality?: string;
patientName?: string;
}

View File

@ -0,0 +1,46 @@
// src/types/index.ts
export interface RouterData {
id: number; // maps to backend 'id'
slNo: number; // maps to backend 'slNo'
routerId: string; // maps to backend 'router_id'
facility: string; // maps to backend 'facility'
routerAlias: string; // maps to backend 'router_alias'
lastSeen: string; // maps to backend 'last_seen'
diskStatus: string; // maps to backend 'disk_status_code'
diskUsage: number; // maps to backend 'disk_usage'
freeDisk: number; // maps to backend 'free_disk'
totalDisk: number; // maps to backend 'total_disk'
routerActivity: {
studies: Study[];
};
systemStatus: {
vpnStatus: string; // maps to backend 'vpn_status_code'
appStatus: string; // maps to backend 'disk_status_code'
vms: VM[];
};
}
export interface Study {
siuid: string;
patientId: string;
accessionNumber: string;
patientName: string;
studyDate: string;
modality: string;
studyDescription: string;
}
export interface VM {
id: number;
status: string;
}
// Add these new interfaces
export interface VMUpdate {
vm_number: number;
status_code: string;
}
export interface VMUpdateRequest {
vms: VMUpdate[];
}

View File

@ -0,0 +1,2 @@
export * from './logger';
// Add more utility exports as needed

View File

@ -0,0 +1,27 @@
import winston from 'winston';
import config from '../config/config';
const logger = winston.createLogger({
level: config.logger.level || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
})
]
});
export default logger;

26
backend/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"lib": ["es2018", "esnext.asynciterable"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
},
"allowJs": true,
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}

60
docker-compose.yml Normal file
View File

@ -0,0 +1,60 @@
version: '3.8'
services:
frontend:
build:
context: ./router-dashboard
dockerfile: Dockerfile
ports:
- "5173:5173"
environment:
- VITE_API_URL=http://localhost:3001/api/v1
depends_on:
- backend
volumes:
- ./router-dashboard:/app
- /app/node_modules
backend:
build:
context: ./ve-router-backend
dockerfile: Dockerfile
ports:
- "3001:3000"
environment:
- NODE_ENV=development
- DB_HOST=host.docker.internal
- DB_PORT=3307
- DB_USER=ve_router_user
- DB_PASSWORD=ve_router_password
- DB_NAME=ve_router_db
depends_on:
- mysql
volumes:
- ./ve-router-backend:/app
- /app/node_modules
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: ve_router_db
MYSQL_USER: ve_router_user
MYSQL_PASSWORD: ve_router_password
volumes:
- mysql_data:/var/lib/mysql
# Correct paths for init scripts
- ./router-dashboard/sql/init.sql:/docker-entrypoint-initdb.d/01-init.sql
- ./router-dashboard/sql/seed_data.sql:/docker-entrypoint-initdb.d/02-seed_data.sql
ports:
- "3307:3306"
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ve_router_user", "-pve_router_password"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
mysql_data:

34
dockerfile Normal file
View File

@ -0,0 +1,34 @@
# Dockerfile
# Build stage
FROM node:18-alpine as build
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
# Copy built assets from build stage
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package*.json ./
# Expose port
EXPOSE 3000
# Start the application
CMD ["npm", "start"]

2
frontend/.env Normal file
View File

@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:3001/api/v1
VITE_NODE_ENV=development

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

50
frontend/README.md Normal file
View File

@ -0,0 +1,50 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

15
frontend/dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV VITE_API_URL=http://localhost:3001/api/v1
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host"]

28
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4238
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
frontend/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "router-dashboard",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.294.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"router-dashboard": "file:"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,198 @@
{
"dicom": {
"local": {
"aet": "ABEL_3",
"port": 104,
"file_directory": "/dicom_images",
"wait_time": 2,
"receiver_wait_time": 5000
},
"association": {
"acse_timeout": 5,
"dimse_timeout": 1000,
"network_timeout": 1000,
"retry": {
"attempts": 3,
"interval": 10
}
},
"query_retrieve": {
"cfind_level": "STUDY",
"cmove_level": "STUDY"
}
},
"rabbitmq": {
"local": {
"hostname": "router-rabbitmq",
"port": 5672,
"credentials": {
"username": "vitalengine",
"password": "vitalengine"
},
"settings": {
"durable": true,
"auto_delete": false,
"exchange_type": "direct",
"heartbeat": 50
},
"exchanges": {
"image_uploader": {
"name": "dicom.image.uploader",
"queue": "dicom.image.uploader.queue",
"routing_key": "image_uploader_routing_key"
},
"image_sender": {
"name": "dicom.image.sender",
"queue": "dicom.image.sender.queue"
},
"rds_status": {
"name": "dicom.image.status.rds",
"queue": "dicom.image.status.rds.queue",
"routing_key": "rds_status_routing_key"
},
"presigned_s3": {
"name": "presigned.s3.client",
"queue": "presigned.s3.client.queue",
"routing_key": "presigned_s3_client_routing_key"
},
"query_retrieve": {
"request": {
"name": "dicom.qr.request",
"queue": "dicom.qr.request.queue"
},
"response": {
"name": "dicom.qr.response",
"queue": "dicom.qr.response.queue",
"routing_key": "dicom.qr.response.routingkey"
}
}
}
},
"server": {
"hostname": "10.8.0.1",
"port": 5672,
"credentials": {
"username": "vitalengine",
"password": "vitalengine"
},
"settings": {
"durable": true,
"auto_delete": false,
"exchange_type": "direct",
"heartbeat": 50,
"retry": {
"attempts": 3,
"interval": 10
},
"consumer_threads": 5
},
"exchanges": {
"dicom_incoming": {
"name": "dicom.incoming.file",
"queue": "dicom.incoming.file.queue",
"routing_key": "dicom.incoming.file.routingkey"
}
}
}
},
"network": {
"vpn": {
"ip": "10.8.0.254"
}
},
"aws": {
"presigned_url_endpoint": "https://qa-develop.dev.vitalengine.com/dicom-cstore/rest/aws-pre-signed-url?aet="
},
"scp_connections": {
"pacs_nodes": [
{
"node_id": "PACS_01",
"active": true,
"aet_config": {
"local_aet": "ABEL_3",
"remote_aet": "REMOTE_PACS",
"remote_host": "192.168.1.100",
"remote_port": 104
},
"connection_settings": {
"max_pdu_length": 16384,
"timeout": 30,
"max_associations": 5,
"implicit_vr_big_endian": false,
"implicit_vr_little_endian": true,
"explicit_vr_little_endian": true
},
"capabilities": {
"storage_commitment": true,
"query_retrieve": true,
"verification": true
}
}
]
},
"peer_connections": {
"peer_nodes": [
{
"node_id": "PEER_01",
"active": true,
"aet_config": {
"local_aet": "ABEL_3",
"remote_aet": "PEER_ROUTER_1",
"remote_host": "10.8.0.15",
"remote_port": 104
},
"connection_settings": {
"max_pdu_length": 16384,
"timeout": 30,
"max_associations": 2
},
"transfer_settings": {
"auto_forward": true,
"compression": "none",
"priority": 1
}
},
{
"node_id": "PEER_02",
"active": false,
"aet_config": {
"local_aet": "ABEL_3",
"remote_aet": "PEER_ROUTER_2",
"remote_host": "10.8.0.16",
"remote_port": 104
},
"connection_settings": {
"max_pdu_length": 16384,
"timeout": 30,
"max_associations": 2
},
"transfer_settings": {
"auto_forward": false,
"compression": "none",
"priority": 2
}
}
],
"global_peer_settings": {
"max_total_associations": 10,
"retry_interval": 300,
"max_retry_attempts": 3,
"keep_alive_interval": 60
}
},
"network_settings": {
"vpn": {
"enabled": true,
"network": "10.8.0.0/24",
"gateway": "10.8.0.1"
},
"allowed_subnets": [
"192.168.1.0/24",
"10.8.0.0/24"
]
}
}

View File

@ -0,0 +1,135 @@
{
"aws": {
"environment": {
"production": false,
"regions": {
"prod": "us-west-2",
"nonprod": "us-east-1"
}
},
"sqs": {
"queue_settings": {
"max_messages": 10,
"visibility_timeout": 5,
"wait_time_seconds": 1
},
"queues": {
"dicom_router": {
"prod": {
"name": "prod-ve-dcmsend-dicom-router-sqs",
"url": "https://queue.amazonaws.com/677720207704/prod-ve-dcmsend-dicom-router-sqs"
},
"nonprod": {
"name": "dev-dcmsend-dicom-router-queue",
"url": "https://sqs.us-east-1.amazonaws.com/63377856061"
}
},
"client_settings": {
"prod": {
"name": "router-client-settings-queue",
"url": "https://queue.amazonaws.com/677720207704/router-client-settings-queue"
},
"nonprod": {
"name": "dev-router-client-settings-queue",
"url": "https://sqs.us-east-1.amazonaws.com/633778560614/dev-router-client-settings-queue"
}
},
"incoming_file_list": {
"prod": {
"url": "https://sqs.us-west-2.amazonaws.com/67"
},
"nonprod": {
"url": "https://sqs.us-east-1.amazonaws.com/6"
}
},
"s3_upload_process": {
"prod": {
"url": "https://sqs.us-west-2.amazonaws.com/67"
},
"nonprod": {
"url": "https://sqs.us-east-1.amazonaws.com/633778560614/dicom-"
}
}
}
},
"s3": {
"credentials": {
"temporary_duration": 21600
},
"buckets": {
"dicom_router": {
"prod": {
"name": "prod-ve-dcmsend-dicom-router-metadata",
"role_arn": "arn:aws:iam::677720207704:role/s3uploaderrole"
},
"nonprod": {
"name": "dev-dicom-router-metadata",
"role_arn": "arn:aws:iam::633778560614:role/s3uploaderrole"
}
},
"client_settings": {
"prod": {
"name": "router-client-settings"
},
"nonprod": {
"name": "dev-router-client-settings"
}
}
}
},
"edge_endpoints": {
"prod": "https://app.fffs.com",
"nonprod": "https://qa-develop.dev.VA.com"
}
},
"rabbitmq": {
"connection": {
"hostname": "localhost",
"port": 5672,
"credentials": {
"username": "va",
"password": "aa"
},
"settings": {
"durable": true,
"auto_delete": false,
"exchange_type": "direct",
"heartbeat": 50
}
},
"exchanges": {
"image_sender": {
"name": "dicom.image.sender",
"queue": "dicom.image.sender.queue"
},
"rds_status": {
"name": "dicom.image.status.rds",
"queue": "dicom.image.status.rds.queue",
"routing_key": "rds_status_routing_key"
},
"presigned_s3": {
"name": "presigned.s3.client",
"queue": "presigned.s3.client.queue",
"routing_key": "presigned_s3_client_routing_key"
},
"client_settings": {
"name": "client.settings.response",
"queue": "client.settings.response.queue",
"routing_key": "client_settings_response_routing_key"
},
"dicom_incoming": {
"name": "dicom.incoming.file",
"queue": "dicom.incoming.file.queue",
"routing_key": "dicom.incoming.file.routingkey"
},
"s3_upload": {
"name": "s3.upload.sqs.publish",
"queue": "s3.upload.sqs.publish.queue",
"routing_key": "s3.upload.sqs.publish.routingkey"
}
}
},
"api": {
"base_url": "https://router-server.dev-api.fd.com"
}
}

10
frontend/src/App.css Normal file
View File

@ -0,0 +1,10 @@
import React from 'react';
import DashboardLayout from './components/dashboard/DashboardLayout';
function App() {
return (
<DashboardLayout />
);
}
export default App;

7
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,7 @@
import DashboardLayout from './components/dashboard/DashboardLayout';
function App() {
return <DashboardLayout />;
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,117 @@
// src/components/dashboard/Dashboard.tsx
import { useState, useEffect } from 'react';
import RouterTable from './RouterTable';
import { RouterData } from '../../types';
import { apiService } from '../../services/api.service';
interface DashboardSummary {
totalRouters: number;
activeRouters: number;
criticalRouters: number;
diskWarnings: number;
}
const Dashboard = () => {
const [routers, setRouters] = useState<RouterData[]>([]);
const [summary, setSummary] = useState<DashboardSummary>({
totalRouters: 0,
activeRouters: 0,
criticalRouters: 0,
diskWarnings: 0
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchRouters = async () => {
try {
setLoading(true);
const data = await apiService.getAllRouters();
setRouters(data);
// Calculate summary
const summaryData = {
totalRouters: data.length,
activeRouters: data.filter(router =>
router.systemStatus.vpnStatus === 'VPN_CONNECTED'
).length,
criticalRouters: data.filter(router =>
router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
router.diskStatus === 'DISK_CRITICAL'
).length,
diskWarnings: data.filter(router =>
router.diskUsage >= 80
).length
};
setSummary(summaryData);
} catch (error) {
console.error('Error fetching router data:', error);
setError('Failed to load router data');
} finally {
setLoading(false);
}
};
fetchRouters();
// Optional: Set up polling for real-time updates
const interval = setInterval(fetchRouters, 30000); // Update every 30 seconds
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<div className="p-6 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
</div>
);
}
return (
<div className="p-6">
<div className="grid gap-6">
{/* Summary Cards */}
<div className="grid grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total Routers</h3>
<p className="text-2xl font-semibold mt-1">{summary.totalRouters}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Routers</h3>
<p className="text-2xl font-semibold text-green-600 mt-1">{summary.activeRouters}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Critical Status</h3>
<p className="text-2xl font-semibold text-red-600 mt-1">{summary.criticalRouters}</p>
</div>
<div className="bg-white p-4 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Disk Usage Alert</h3>
<p className="text-2xl font-semibold text-yellow-600 mt-1">{summary.diskWarnings}</p>
</div>
</div>
{/* Router Table */}
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200">
<h2 className="text-lg font-semibold">Router Status Overview</h2>
</div>
<RouterTable routers={routers} onRefresh={() => {}} />
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,229 @@
// src/components/dashboard/DashboardLayout.tsx
import React, { useState, useEffect } from 'react';
import Navbar from './Navbar';
import Header from './Header';
import RouterTable from './RouterTable';
import RouterManagement from './pages/RouterManagement';
import { RouterData } from '../../types';
import { apiService } from '../../services/api.service';
interface User {
name: string;
role: string;
}
type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
const DashboardLayout: React.FC = () => {
const [activeTab, setActiveTab] = useState('dashboard');
const [activeFilter, setActiveFilter] = useState<FilterType>('all');
const [routers, setRouters] = useState<RouterData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [user] = useState<User>({
name: 'John Doe',
role: 'Administrator'
});
useEffect(() => {
const fetchRouters = async () => {
try {
setLoading(true);
let data = await apiService.getAllRouters();
// Apply filter based on activeFilter
if (activeFilter !== 'all') {
data = data.filter(router => {
switch (activeFilter) {
case 'active':
return router.routerActivity?.studies &&
router.routerActivity.studies.length > 0;
case 'critical':
return router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
router.systemStatus.appStatus === 'DISK_CRITICAL' ||
router.diskStatus === 'DISK_CRITICAL';
case 'diskAlert':
return router.diskUsage > 80;
default:
return true;
}
});
}
setRouters(data);
setError(null);
} catch (err) {
console.error('Error fetching routers:', err);
setError('Failed to fetch router data');
} finally {
setLoading(false);
}
};
fetchRouters();
const interval = setInterval(fetchRouters, 30000);
return () => clearInterval(interval);
}, [activeFilter]);
const handleLogout = () => {
console.log('Logging out...');
// Add your logout logic here
};
const getSummary = (routerData: RouterData[]) => {
return {
total: routerData.length,
active: routerData.filter(r =>
r.routerActivity?.studies &&
r.routerActivity.studies.length > 0
).length,
critical: routerData.filter(r =>
r.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
r.systemStatus.appStatus === 'DISK_CRITICAL' ||
r.diskStatus === 'DISK_CRITICAL'
).length,
diskAlert: routerData.filter(r => r.diskUsage > 80).length
};
};
const renderContent = () => {
if (loading) {
return (
<div className="p-6 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
</div>
);
}
switch (activeTab) {
case 'dashboard': {
const summary = getSummary(routers);
return (
<div className="p-6">
<div className="grid gap-6">
{/* Summary Cards */}
<div className="grid grid-cols-4 gap-4">
<div
className={`bg-white p-4 rounded-lg shadow ${
activeFilter === 'all' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveFilter('all')}
>
<h3 className="text-sm font-medium text-gray-500">Total Routers</h3>
<p className="text-2xl font-semibold mt-1">{summary.total}</p>
</div>
<div
className={`bg-white p-4 rounded-lg shadow cursor-pointer hover:bg-gray-50 ${
activeFilter === 'active' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveFilter('active')}
>
<h3 className="text-sm font-medium text-gray-500">Active Routers</h3>
<p className="text-2xl font-semibold text-green-600 mt-1">{summary.active}</p>
</div>
<div
className={`bg-white p-4 rounded-lg shadow cursor-pointer hover:bg-gray-50 ${
activeFilter === 'critical' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveFilter('critical')}
>
<h3 className="text-sm font-medium text-gray-500">Critical Status</h3>
<p className="text-2xl font-semibold text-red-600 mt-1">{summary.critical}</p>
</div>
<div
className={`bg-white p-4 rounded-lg shadow cursor-pointer hover:bg-gray-50 ${
activeFilter === 'diskAlert' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveFilter('diskAlert')}
>
<h3 className="text-sm font-medium text-gray-500">Disk Usage Alert</h3>
<p className="text-2xl font-semibold text-yellow-600 mt-1">
{summary.diskAlert}
</p>
</div>
</div>
{/* Filter Label */}
{activeFilter !== 'all' && (
<div className="flex items-center gap-2 text-sm text-gray-600">
<span>Showing:</span>
<span className="font-medium">
{activeFilter === 'active' && 'Active Routers (with Studies)'}
{activeFilter === 'critical' && 'Routers with Critical Status'}
{activeFilter === 'diskAlert' && 'Routers with High Disk Usage'}
</span>
<button
onClick={() => setActiveFilter('all')}
className="text-blue-500 hover:text-blue-700 ml-2"
>
Clear Filter
</button>
</div>
)}
{/* Router Table */}
<div className="bg-white rounded-lg shadow">
<RouterTable
routers={routers} // Make sure routers is initialized as an empty array if undefined
filter={activeFilter}
onFilterChange={setActiveFilter}
/>
</div>
</div>
</div>
);
}
case 'routers':
return <RouterManagement />;
case 'users':
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">User Management</h2>
<p>User management content goes here</p>
</div>
</div>
);
case 'settings':
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Settings</h2>
<p>Settings content goes here</p>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gray-100">
<Header user={user} onLogout={handleLogout} />
<div className="flex h-[calc(100vh-4rem)]">
<Navbar activeTab={activeTab} onTabChange={setActiveTab} />
<main className="flex-1 overflow-auto">
{renderContent()}
</main>
</div>
</div>
);
};
export default DashboardLayout;

View File

@ -0,0 +1,49 @@
// src/components/dashboard/Header.tsx
import React from 'react';
import { LogOut } from 'lucide-react';
import companyLogo from '../../assets/images/ve-logo-tm.svg';
interface User {
name: string;
role: string;
}
interface HeaderProps {
user: User;
onLogout: () => void;
}
const Header: React.FC<HeaderProps> = ({ user, onLogout }) => {
return (
<header className="bg-white shadow-sm">
<div className="h-16 px-4 flex items-center justify-between">
<div className="flex items-center gap-4">
{/* Replace the gray circle with the logo */}
<img
src={companyLogo}
alt="Company Logo"
className="h-10 w-auto object-contain"
// If you want the logo to maintain its aspect ratio but fit within certain dimensions:
// style={{ maxWidth: '40px', maxHeight: '40px' }}
/>
<h1 className="text-xl font-bold">Router Management and Reporting System</h1>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="text-sm font-medium">{user.name}</div>
<div className="text-xs text-gray-500">{user.role}</div>
</div>
<button
onClick={onLogout}
className="px-3 py-1 text-sm flex items-center gap-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded"
>
<LogOut size={16} />
Logout
</button>
</div>
</div>
</header>
);
};
export default Header;

View File

@ -0,0 +1,173 @@
import React, { useState, useEffect } from 'react';
import {
LayoutDashboard,
Router as RouterIcon,
Users,
Settings,
Pin,
ChevronLeft,
ChevronRight,
type LucideIcon
} from 'lucide-react';
interface NavbarProps {
activeTab: string;
onTabChange: (tab: string) => void;
}
interface Tab {
id: string;
label: string;
icon: LucideIcon;
}
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const [isPinned, setIsPinned] = useState(true);
const [isHovered, setIsHovered] = useState(false);
const tabs: Tab[] = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'routers', label: 'Router Management', icon: RouterIcon },
{ id: 'users', label: 'User Management', icon: Users },
{ id: 'settings', label: 'Settings', icon: Settings }
];
const handleMouseEnter = () => {
if (!isPinned) {
setIsCollapsed(false);
}
setIsHovered(true);
};
const handleMouseLeave = () => {
if (!isPinned) {
setIsCollapsed(true);
}
setIsHovered(false);
};
const togglePin = (e: React.MouseEvent) => {
e.stopPropagation();
setIsPinned(!isPinned);
if (!isPinned) {
setIsCollapsed(false);
}
};
const toggleCollapse = (e: React.MouseEvent) => {
e.stopPropagation();
setIsCollapsed(!isCollapsed);
if (!isCollapsed) {
setIsPinned(false);
}
};
return (
<div
className={`relative bg-gray-900 text-white h-full flex flex-col transition-all duration-300 ease-in-out ${
isCollapsed ? 'w-16' : 'w-64'
}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Header with title and controls */}
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
{!isCollapsed && (
<div>
<h2 className="text-sm font-semibold">Router Management</h2>
<h2 className="text-sm font-semibold">System</h2>
</div>
)}
<div className={`flex items-center gap-2 ${isCollapsed ? 'w-full justify-center' : 'justify-end'}`}>
{(isHovered || !isCollapsed) && (
<button
onClick={togglePin}
className={`p-1 rounded hover:bg-gray-700 transition-colors ${
isPinned ? 'text-blue-400' : 'text-gray-400'
}`}
title={isPinned ? 'Unpin sidebar' : 'Pin sidebar'}
>
<Pin
size={16}
className={`transform transition-transform ${isPinned ? 'rotate-45' : ''}`}
/>
</button>
)}
<button
onClick={toggleCollapse}
className="p-1 rounded hover:bg-gray-700 transition-colors"
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{isCollapsed ? (
<ChevronRight size={16} />
) : (
<ChevronLeft size={16} />
)}
</button>
</div>
</div>
{/* Navigation Items */}
<nav className="flex-1 p-2">
{tabs.map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
transition-colors duration-200 group
${activeTab === tab.id
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-800'}
`}
title={isCollapsed ? tab.label : undefined}
>
<Icon size={20} />
{!isCollapsed && <span>{tab.label}</span>}
</button>
);
})}
</nav>
{/* Expanded overlay when collapsed and hovered */}
{isHovered && isCollapsed && !isPinned && (
<div
className="absolute left-16 top-0 bg-gray-900 text-white h-full w-64 shadow-lg z-50 overflow-hidden"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="p-4 border-b border-gray-700">
<h2 className="text-sm font-semibold">Router Management</h2>
<h2 className="text-sm font-semibold">System</h2>
</div>
<nav className="p-2">
{tabs.map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`
w-full flex items-center gap-3 px-3 py-2 mb-1 rounded-lg
transition-colors duration-200
${activeTab === tab.id
? 'bg-blue-600 text-white'
: 'text-gray-300 hover:bg-gray-800'}
`}
>
<Icon size={20} />
<span>{tab.label}</span>
</button>
);
})}
</nav>
</div>
)}
</div>
);
};
export default Navbar;

View File

@ -0,0 +1,40 @@
// src/components/dashboard/pages/RouterManagement.tsx
import React, { useState } from 'react';
import { RouterData, FilterType } from '../../../types';
import { RouterTable } from '../RouterTable';
export const RouterManagement: React.FC = () => {
const [filter, setFilter] = useState<FilterType>('all');
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow">
<div className="p-6">
<h2 className="text-xl font-semibold mb-6">Router Management</h2>
{/* Filter Controls */}
<div className="mb-4">
<select
value={filter}
onChange={(e) => setFilter(e.target.value as FilterType)}
className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Routers</option>
<option value="active">Active</option>
<option value="critical">Critical</option>
<option value="diskAlert">Disk Alert</option>
</select>
</div>
{/* Router Table Component */}
<RouterTable
filter={filter}
onFilterChange={setFilter}
/>
</div>
</div>
</div>
);
};
export default RouterManagement;

View File

@ -0,0 +1,145 @@
// src/components/dashboard/RouterTable.tsx
import React from 'react';
import { Search, ArrowUpDown, GripHorizontal, X } from 'lucide-react';
import { RouterData, FilterType } from '../../types';
import { useRouterTableLogic } from './RouterTableLogic';
import { RouterTableRow } from './RouterTableRow';
interface RouterTableProps {
routers?: RouterData[];
filter: FilterType;
onFilterChange: (filter: FilterType) => void;
}
export const RouterTable: React.FC<RouterTableProps> = ({
routers = [],
filter,
onFilterChange
}) => {
const {
routers: filteredRouters,
searchTerm,
setSearchTerm,
expandedRows,
timeLeft,
handleSort,
toggleRowExpansion,
handleExpandedContentHover,
totalRouters
} = useRouterTableLogic(routers);
const getLocalTimeZone = () => {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone
.split('/')
.pop()
?.replace('_', ' ') || '';
} catch {
return new Date()
.toLocaleTimeString('en-us', { timeZoneName: 'short' })
.split(' ')[2];
}
};
const columnHeaders = [
{ key: 'slNo', label: 'Sl#', sortable: true },
{ key: 'routerId', label: 'Router ID', sortable: true },
{ key: 'routerAlias', label: 'Router Alias', sortable: true },
{ key: 'facility', label: 'Facility', sortable: true },
{ key: 'activity', label: 'Activity', sortable: false },
{ key: 'systemStatus', label: 'System Status', sortable: false },
{ key: 'lastSeen', label: `Last Seen (${getLocalTimeZone()})`, sortable: true },
{ key: 'diskUsage', label: 'Disk Usage', sortable: true }
] as const;
return (
<div className="p-6">
{/* Search Bar */}
<div className="mb-4 relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search by Router ID, Alias, or Facility..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-10 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
>
<X size={16} />
</button>
)}
</div>
{/* Table */}
<div className="overflow-x-auto rounded-lg border border-gray-200">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
{columnHeaders.map(column => (
<th
key={column.key}
className="px-4 py-2 text-left"
onClick={() => column.sortable && handleSort(column.key as keyof RouterData)}
>
<div className={`flex items-center gap-2 ${column.sortable ? 'cursor-pointer hover:text-gray-700' : ''}`}>
<GripHorizontal size={16} className="text-gray-400" />
<span>{column.label}</span>
{column.sortable && <ArrowUpDown size={14} className="text-gray-400" />}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{filteredRouters.length > 0 ? (
filteredRouters.map(router => (
<RouterTableRow
key={router.id}
router={router}
expandedRows={expandedRows}
timeLeft={timeLeft}
onToggleExpansion={toggleRowExpansion}
onExpandedContentHover={handleExpandedContentHover}
/>
))
) : (
<tr>
<td colSpan={columnHeaders.length} className="px-4 py-8 text-center border-b">
<div className="flex flex-col items-center justify-center space-y-2">
{searchTerm ? (
<>
<p className="text-gray-600">No results found for "{searchTerm}"</p>
<button
onClick={() => setSearchTerm('')}
className="text-blue-500 hover:text-blue-700 text-sm flex items-center gap-1"
>
<X size={14} />
Clear search
</button>
</>
) : (
<p className="text-gray-600">No routers available</p>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Results Summary */}
{filteredRouters.length > 0 && (
<div className="mt-4 text-sm text-gray-500">
Showing {filteredRouters.length} of {totalRouters} routers
</div>
)}
</div>
);
};
export default RouterTable;

View File

@ -0,0 +1,185 @@
// src/components/dashboard/RouterTableLogic.tsx
import { useState, useMemo, useEffect, useCallback } from 'react';
import { RouterData, FilterType } from '../../types';
interface TimeoutInfo {
timeoutId: NodeJS.Timeout;
startTime: number;
}
interface AutoHideTimeouts {
[key: string]: TimeoutInfo;
}
export const useRouterTableLogic = (initialRouters: RouterData[] = []) => {
const [searchTerm, setSearchTerm] = useState('');
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [sortConfig, setSortConfig] = useState<{ key: keyof RouterData; direction: 'asc' | 'desc' } | null>(null);
const [timeLeft, setTimeLeft] = useState<{ [key: string]: number }>({});
const [autoHideTimeouts, setAutoHideTimeouts] = useState<AutoHideTimeouts>({});
const AUTO_HIDE_DELAY = 10000;
// Update countdown timer
useEffect(() => {
const interval = setInterval(() => {
setTimeLeft(prev => {
const now = Date.now();
const updated = { ...prev };
Object.entries(autoHideTimeouts).forEach(([key, info]) => {
if (info) {
const remaining = Math.max(0, AUTO_HIDE_DELAY - (now - info.startTime));
updated[key] = (remaining / AUTO_HIDE_DELAY) * 100;
}
});
return updated;
});
}, 100);
// Cleanup on unmount
return () => {
clearInterval(interval);
Object.values(autoHideTimeouts).forEach(info => {
if (info?.timeoutId) {
clearTimeout(info.timeoutId);
}
});
};
}, [autoHideTimeouts]);
// Filter and sort data
const filteredRouters = useMemo(() => {
const routers = Array.isArray(initialRouters) ? initialRouters : [];
let processed = [...routers];
if (searchTerm.trim()) {
const lowerSearchTerm = searchTerm.toLowerCase().trim();
processed = processed.filter(router => {
const searchableFields = ['routerId', 'routerAlias', 'facility'];
return searchableFields.some(field =>
String(router[field as keyof RouterData] || '')
.toLowerCase()
.includes(lowerSearchTerm)
);
});
}
if (sortConfig) {
processed.sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return processed;
}, [initialRouters, searchTerm, sortConfig]);
const handleSort = useCallback((key: keyof RouterData) => {
setSortConfig(current => ({
key,
direction: current?.key === key && current.direction === 'asc' ? 'desc' : 'asc'
}));
}, []);
const toggleRowExpansion = useCallback((rowId: number, section: 'activity' | 'status' | 'disk') => {
const key = `${rowId}-${section}`;
setExpandedRows(prev => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
const existingTimeout = autoHideTimeouts[key]?.timeoutId;
if (existingTimeout) {
clearTimeout(existingTimeout);
setAutoHideTimeouts(prev => {
const next = { ...prev };
delete next[key];
return next;
});
setTimeLeft(prev => {
const next = { ...prev };
delete next[key];
return next;
});
}
} else {
next.add(key);
const timeoutId = setTimeout(() => {
setExpandedRows(prev => {
const next = new Set(prev);
next.delete(key);
return next;
});
setAutoHideTimeouts(prev => {
const next = { ...prev };
delete next[key];
return next;
});
setTimeLeft(prev => {
const next = { ...prev };
delete next[key];
return next;
});
}, AUTO_HIDE_DELAY);
setAutoHideTimeouts(prev => ({
...prev,
[key]: { timeoutId, startTime: Date.now() }
}));
setTimeLeft(prev => ({ ...prev, [key]: 100 }));
}
return next;
});
}, [autoHideTimeouts]);
const handleExpandedContentHover = useCallback((rowId: number, section: 'activity' | 'status' | 'disk') => {
const key = `${rowId}-${section}`;
const existingTimeout = autoHideTimeouts[key]?.timeoutId;
if (existingTimeout) {
clearTimeout(existingTimeout);
const timeoutId = setTimeout(() => {
setExpandedRows(prev => {
const next = new Set(prev);
next.delete(key);
return next;
});
setAutoHideTimeouts(prev => {
const next = { ...prev };
delete next[key];
return next;
});
setTimeLeft(prev => {
const next = { ...prev };
delete next[key];
return next;
});
}, AUTO_HIDE_DELAY);
setAutoHideTimeouts(prev => ({
...prev,
[key]: { timeoutId, startTime: Date.now() }
}));
setTimeLeft(prev => ({ ...prev, [key]: 100 }));
}
}, [autoHideTimeouts]);
return {
routers: filteredRouters,
totalRouters: initialRouters.length,
searchTerm,
setSearchTerm,
expandedRows,
timeLeft,
handleSort,
toggleRowExpansion,
handleExpandedContentHover
};
};

View File

@ -0,0 +1,234 @@
// src/components/dashboard/RouterTableRow.tsx
import React from 'react';
import { ChevronRight, ChevronDown } from 'lucide-react';
import { RouterData } from '../../types';
import { STATUS_COLORS, formatStatus, getStatusColor } from '../../utils/statusHelpers';
interface RouterTableRowProps {
router: RouterData;
expandedRows: Set<string>;
timeLeft: { [key: string]: number };
onToggleExpansion: (id: number, section: 'activity' | 'status' | 'disk') => void;
onExpandedContentHover: (id: number, section: 'activity' | 'status' | 'disk') => void;
}
export const RouterTableRow: React.FC<RouterTableRowProps> = ({
router,
expandedRows,
timeLeft,
onToggleExpansion,
onExpandedContentHover
}) => {
const formatLocalTime = (isoString: string) => {
return new Date(isoString).toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
});
};
const formatDiskSpace = (bytes: number) => {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
};
const getUsageColor = (usage: number) => {
if (usage >= 90) return 'bg-red-500';
if (usage >= 70) return 'bg-yellow-500';
return 'bg-green-500';
};
const renderActivityPanel = () => (
<div
className="bg-gray-50 p-4 relative"
onMouseEnter={() => onExpandedContentHover(router.id, 'activity')}
>
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
<div
className="h-1 bg-blue-500 transition-all duration-200"
style={{ width: `${timeLeft[`${router.id}-activity`] || 0}%` }}
/>
</div>
<h3 className="font-semibold mb-3">Recent Studies</h3>
<div className="grid grid-cols-3 gap-4">
{router.routerActivity.studies.length > 0 ? (
router.routerActivity.studies.map((study, idx) => (
<div key={idx} className="bg-white p-3 rounded shadow-sm">
<div className="grid gap-2">
<div><span className="font-medium">SIUID:</span> {study.siuid}</div>
<div><span className="font-medium">Patient:</span> {study.patientName}</div>
<div><span className="font-medium">Accession:</span> {study.accessionNumber}</div>
<div><span className="font-medium">Date:</span> {study.studyDate}</div>
<div><span className="font-medium">Modality:</span> {study.modality}</div>
{study.studyDescription && (
<div><span className="font-medium">Description:</span> {study.studyDescription}</div>
)}
</div>
</div>
))
) : (
<div className="col-span-3 text-center text-gray-500">No recent studies</div>
)}
</div>
</div>
);
const renderStatusPanel = () => (
<div
className="bg-gray-50 p-4 relative"
onMouseEnter={() => onExpandedContentHover(router.id, 'status')}
>
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
<div
className="h-1 bg-blue-500 transition-all duration-200"
style={{ width: `${timeLeft[`${router.id}-status`] || 0}%` }}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<h4 className="font-semibold mb-2">VPN Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
{formatStatus(router.systemStatus.vpnStatus)}
</span>
</div>
<div>
<h4 className="font-semibold mb-2">App Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.diskStatus)}`}>
{formatStatus(router.diskStatus)}
</span>
</div>
<div>
<h4 className="font-semibold mb-2">VM Status</h4>
{router.systemStatus.vms.length > 0 ? (
router.systemStatus.vms.map((vm, idx) => (
<div key={idx} className="mb-1">
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(vm.status)}`}>
VM {vm.id}: {formatStatus(vm.status)}
</span>
</div>
))
) : (
<span className="text-gray-500">No VMs configured</span>
)}
</div>
</div>
</div>
);
const renderDiskPanel = () => (
<div
className="bg-gray-50 p-4 relative"
onMouseEnter={() => onExpandedContentHover(router.id, 'disk')}
>
<div className="h-1 bg-gray-200 absolute top-0 left-0 right-0">
<div
className="h-1 bg-blue-500 transition-all duration-200"
style={{ width: `${timeLeft[`${router.id}-disk`] || 0}%` }}
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<h4 className="font-semibold mb-2">Free Disk Space</h4>
<span className="text-lg">{formatDiskSpace(router.freeDisk)}</span>
</div>
<div>
<h4 className="font-semibold mb-2">Total Disk Space</h4>
<span className="text-lg">{formatDiskSpace(router.totalDisk)}</span>
</div>
<div>
<h4 className="font-semibold mb-2">Used Disk Space</h4>
<span className="text-lg">{formatDiskSpace(router.totalDisk - router.freeDisk)}</span>
</div>
</div>
</div>
);
return (
<React.Fragment>
<tr className="border-b hover:bg-gray-50">
<td className="px-4 py-2">{router.slNo}</td>
<td className="px-4 py-2">{router.routerId}</td>
<td className="px-4 py-2">{router.routerAlias}</td>
<td className="px-4 py-2">{router.facility}</td>
<td className="px-4 py-2">
<button
onClick={() => onToggleExpansion(router.id, 'activity')}
className="flex items-center gap-1 text-blue-500 hover:text-blue-700"
>
{expandedRows.has(`${router.id}-activity`) ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
View Activity ({router.routerActivity.studies.length} studies)
</button>
</td>
<td className="px-4 py-2">
<button
onClick={() => onToggleExpansion(router.id, 'status')}
className="flex items-center gap-1 text-blue-500 hover:text-blue-700"
>
{expandedRows.has(`${router.id}-status`) ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
{formatStatus(router.systemStatus.vpnStatus)}
</span>
</button>
</td>
<td className="px-4 py-2">{formatLocalTime(router.lastSeen)}</td>
<td className="px-4 py-2">
<button
onClick={() => onToggleExpansion(router.id, 'disk')}
className="w-full flex items-center gap-2"
>
<div className="flex-1 flex items-center gap-2">
<div className="w-24 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getUsageColor(router.diskUsage)}`}
style={{ width: `${router.diskUsage}%` }}
/>
</div>
<span>{router.diskUsage}%</span>
</div>
{expandedRows.has(`${router.id}-disk`) ? (
<ChevronDown size={16} className="text-gray-400" />
) : (
<ChevronRight size={16} className="text-gray-400" />
)}
</button>
</td>
</tr>
{/* Expandable Panels */}
{expandedRows.has(`${router.id}-activity`) && (
<tr>
<td colSpan={8}>
{renderActivityPanel()}
</td>
</tr>
)}
{expandedRows.has(`${router.id}-status`) && (
<tr>
<td colSpan={8}>
{renderStatusPanel()}
</td>
</tr>
)}
{expandedRows.has(`${router.id}-disk`) && (
<tr>
<td colSpan={8}>
{renderDiskPanel()}
</td>
</tr>
)}
</React.Fragment>
);
};
export default RouterTableRow;

View File

@ -0,0 +1,200 @@
import React, { useState } from 'react';
import {
Plus,
Settings,
RefreshCw,
Upload,
Download,
FileSpreadsheet
} from 'lucide-react';
const RouterManagement: React.FC = () => {
const [selectedRouter, setSelectedRouter] = useState<string | null>(null);
return (
<div className="p-6">
<div className="bg-white rounded-lg shadow">
{/* Header */}
<div className="p-6 border-b border-gray-200">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">Router Management</h2>
<div className="flex gap-3">
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2">
<Plus size={20} />
Add New Router
</button>
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
<RefreshCw size={20} />
Refresh
</button>
</div>
</div>
</div>
{/* Main Content */}
<div className="grid grid-cols-4 gap-6 p-6">
{/* Configuration Section */}
<div className="col-span-3">
<div className="grid gap-6">
{/* Router Configuration */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-medium mb-4">Router Configuration</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Router ID
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter Router ID"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Router Alias
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter Router Alias"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Facility
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter Facility Name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
License Key
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter License Key"
/>
</div>
</div>
</div>
{/* Network Settings */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-medium mb-4">Network Settings</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
IP Address
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter IP Address"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Subnet Mask
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter Subnet Mask"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gateway
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter Gateway"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
DNS Server
</label>
<input
type="text"
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter DNS Server"
/>
</div>
</div>
</div>
</div>
</div>
{/* Actions Panel */}
<div className="col-span-1">
<div className="border rounded-lg p-4">
<h3 className="text-lg font-medium mb-4">Actions</h3>
<div className="space-y-3">
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
<Settings size={20} />
Configure Router
</button>
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
<Upload size={20} />
Upload Config
</button>
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
<Download size={20} />
Download Config
</button>
<button className="w-full px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2">
<FileSpreadsheet size={20} />
Export Settings
</button>
</div>
{/* Status Section */}
<div className="mt-6">
<h4 className="font-medium mb-2">Current Status</h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Connection:</span>
<span className="px-2 py-1 rounded-full text-sm bg-green-100 text-green-800">
Connected
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">License:</span>
<span className="px-2 py-1 rounded-full text-sm bg-blue-100 text-blue-800">
Active
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600">Last Updated:</span>
<span className="text-sm">2 mins ago</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="border-t border-gray-200 p-6">
<div className="flex justify-end gap-4">
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50">
Cancel
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Save Changes
</button>
</div>
</div>
</div>
</div>
);
};
export default RouterManagement;

View File

@ -0,0 +1,12 @@
// src/config/env.ts
interface Config {
apiUrl: string;
environment: string;
}
const config: Config = {
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
environment: import.meta.env.VITE_NODE_ENV || 'development',
};
export default config;

View File

@ -0,0 +1,136 @@
import { RouterData } from '../types';
export const STATUS_COLORS = {
Normal: 'bg-green-100 text-green-800',
Warning: 'bg-yellow-100 text-yellow-800',
Critical: 'bg-red-100 text-red-800',
Connected: 'bg-green-100 text-green-800',
Running: 'bg-green-100 text-green-800'
};
export const getUsageColor = (usage: number): string => {
if (usage > 90) return 'bg-red-500';
if (usage > 70) return 'bg-yellow-500';
return 'bg-green-500';
};
export const MOCK_ROUTERS: RouterData[] = [
{
id: 1,
slNo: 1,
routerId: 'RTR001',
facility: 'City Hospital',
routerAlias: 'Main-Router-1',
lastSeen: '2024-03-07T14:30:00Z',
diskStatus: 'Normal',
diskUsage: 45,
// ... existing fields ...
freeDisk: 107374182400, // 100 GB in bytes
totalDisk: 1099511627776, // 1 TB in bytes
routerActivity: {
studies: [
{
siuid: 'SI123456',
patientId: 'P001',
accessionNumber: 'ACC001',
patientName: 'John Smith',
studyDate: '2024-03-07',
modality: 'CT',
studyDescription: 'Chest CT'
},
{
siuid: 'SI123457',
patientId: 'P002',
accessionNumber: 'ACC002',
patientName: 'Sarah Johnson',
studyDate: '2024-03-07',
modality: 'MRI',
studyDescription: 'Brain MRI'
}
]
},
systemStatus: {
vpnStatus: 'Connected',
appStatus: 'Running',
vms: [
{ id: 1, status: 'Running' },
{ id: 2, status: 'Running' }
]
}
},
{
id: 2,
slNo: 2,
routerId: 'RTR002',
facility: 'Medical Center',
routerAlias: 'Emergency-Router',
lastSeen: '2024-03-07T14:25:00Z',
diskStatus: 'Critical',
diskUsage: 92,
freeDisk: 107374182400, // 100 GB in bytes
totalDisk: 1099511627776, // 1 TB in bytes
routerActivity: {
studies: [
{
siuid: 'SI123458',
patientId: 'P003',
accessionNumber: 'ACC003',
patientName: 'Michael Brown',
studyDate: '2024-03-07',
modality: 'XR',
studyDescription: 'Chest X-Ray'
}
]
},
systemStatus: {
vpnStatus: 'Critical',
appStatus: 'Critical',
vms: [
{ id: 1, status: 'Critical' },
{ id: 2, status: 'Running' }
]
}
},
{
id: 3,
slNo: 3,
routerId: 'RTR003',
facility: 'Imaging Center',
routerAlias: 'Radiology-Router',
lastSeen: '2024-03-07T14:20:00Z',
diskStatus: 'Warning',
diskUsage: 78,
freeDisk: 107374182400, // 100 GB in bytes
totalDisk: 1099511627776, // 1 TB in bytes
routerActivity: {
studies: [
{
siuid: 'SI123459',
patientId: 'P004',
accessionNumber: 'ACC004',
patientName: 'Emily Davis',
studyDate: '2024-03-07',
modality: 'US',
studyDescription: 'Abdominal Ultrasound'
},
{
siuid: 'SI123460',
patientId: 'P005',
accessionNumber: 'ACC005',
patientName: 'Robert Wilson',
studyDate: '2024-03-07',
modality: 'MG',
studyDescription: 'Mammogram'
}
]
},
systemStatus: {
vpnStatus: 'Connected',
appStatus: 'Warning',
vms: [
{ id: 1, status: 'Running' },
{ id: 2, status: 'Warning' }
]
}
}
];

0
frontend/src/hooks Normal file
View File

4
frontend/src/index.css Normal file
View File

@ -0,0 +1,4 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

11
frontend/src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
// File: src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -0,0 +1,228 @@
// router-dashboard/src/services/api.service.ts
import { RouterData, FilterType, BackendRouter } from '../types';
const API_BASE_URL = 'http://localhost:3000/api/v1';
// Default request options for all API calls
const DEFAULT_OPTIONS = {
credentials: 'include' as const,
headers: {
'Content-Type': 'application/json',
}
};
// Helper function to log API responses in development
const logResponse = (prefix: string, data: any) => {
if (process.env.NODE_ENV === 'development') {
console.log(`${prefix}:`, data);
}
};
class ApiService {
async getAllRouters(filter: FilterType = 'all'): Promise<RouterData[]> {
try {
const response = await fetch(`${API_BASE_URL}/routers?filter=${filter}`, {
method: 'GET',
...DEFAULT_OPTIONS
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
logResponse('Raw API response', data);
const transformed = this.transformData(data);
logResponse('Transformed routers', transformed);
return transformed;
} catch (error) {
console.error('Error fetching routers:', error);
return [];
}
}
private transformData(data: any[]): RouterData[] {
try {
if (!Array.isArray(data)) {
console.error('Expected array of routers, got:', typeof data);
return [];
}
return data.map(router => {
console.log('Processing router:', router); // Debug log raw data
const transformed: RouterData = {
id: router.id,
slNo: router.slNo,
routerId: router.routerId, // Changed from router.router_id
facility: router.facility,
routerAlias: router.routerAlias, // Changed from router.router_alias
lastSeen: router.lastSeen, // Changed from router.last_seen
diskStatus: router.diskStatus, // Changed from router.disk_status_code
diskUsage: router.diskUsage || 0, // Changed from router.disk_usage
freeDisk: router.freeDisk || 0, // Changed from router.free_disk
totalDisk: router.totalDisk || 0, // Changed from router.total_disk
routerActivity: {
studies: Array.isArray(router.routerActivity?.studies)
? router.routerActivity.studies.map((study: any) => ({
siuid: study.siuid,
patientId: study.patientId,
accessionNumber: study.accessionNumber,
patientName: study.patientName,
studyDate: study.studyDate,
modality: study.modality,
studyDescription: study.studyDescription
}))
: []
},
systemStatus: {
vpnStatus: router.systemStatus?.vpnStatus || 'unknown',
appStatus: router.systemStatus?.appStatus || 'unknown',
vms: Array.isArray(router.systemStatus?.vms)
? router.systemStatus.vms.map((vm: any) => ({
id: vm.id,
status: vm.status
}))
: []
}
};
console.log('Transformed router:', transformed); // Debug transformed data
return transformed;
});
} catch (error) {
console.error('Error transforming data:', error);
return [];
}
}
async getRouterById(id: number): Promise<RouterData | null> {
try {
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
...DEFAULT_OPTIONS
});
if (!response.ok) {
throw new Error('Failed to fetch router');
}
const data = await response.json();
const transformed = this.transformData([data]);
return transformed[0] || null;
} catch (error) {
console.error('Error fetching router:', error);
return null;
}
}
async createRouter(routerData: Partial<RouterData>): Promise<RouterData | null> {
try {
// Transform frontend model to backend model
const backendData = {
router_id: routerData.routerId,
facility: routerData.facility,
router_alias: routerData.routerAlias,
disk_status_code: routerData.diskStatus,
disk_usage: routerData.diskUsage,
free_disk: routerData.freeDisk,
total_disk: routerData.totalDisk,
vpn_status_code: routerData.systemStatus?.vpnStatus,
last_seen: new Date().toISOString(), // Set current timestamp
};
const response = await fetch(`${API_BASE_URL}/routers`, {
method: 'POST',
...DEFAULT_OPTIONS,
body: JSON.stringify(backendData),
});
if (!response.ok) {
throw new Error('Failed to create router');
}
const data = await response.json();
const transformed = this.transformData([data]);
return transformed[0] || null;
} catch (error) {
console.error('Error creating router:', error);
return null;
}
}
async updateRouter(id: number, routerData: Partial<RouterData>): Promise<RouterData | null> {
try {
// Transform frontend model to backend model
const backendData = {
...(routerData.routerId && { router_id: routerData.routerId }),
...(routerData.facility && { facility: routerData.facility }),
...(routerData.routerAlias && { router_alias: routerData.routerAlias }),
...(routerData.diskStatus && { disk_status_code: routerData.diskStatus }),
...(routerData.diskUsage !== undefined && { disk_usage: routerData.diskUsage }),
...(routerData.freeDisk !== undefined && { free_disk: routerData.freeDisk }),
...(routerData.totalDisk !== undefined && { total_disk: routerData.totalDisk }),
...(routerData.systemStatus?.vpnStatus && { vpn_status_code: routerData.systemStatus.vpnStatus }),
last_seen: new Date().toISOString() // Update timestamp on changes
};
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
method: 'PUT',
...DEFAULT_OPTIONS,
body: JSON.stringify(backendData),
});
if (!response.ok) {
throw new Error('Failed to update router');
}
const data = await response.json();
const transformed = this.transformData([data]);
return transformed[0] || null;
} catch (error) {
console.error('Error updating router:', error);
return null;
}
}
async deleteRouter(id: number): Promise<boolean> {
try {
const response = await fetch(`${API_BASE_URL}/routers/${id}`, {
method: 'DELETE',
...DEFAULT_OPTIONS
});
if (!response.ok) {
throw new Error('Failed to delete router');
}
return true;
} catch (error) {
console.error('Error deleting router:', error);
return false;
}
}
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
try {
const response = await fetch(
`${API_BASE_URL}/routers/facility/${encodeURIComponent(facility)}`,
{ ...DEFAULT_OPTIONS }
);
if (!response.ok) {
throw new Error('Failed to fetch routers by facility');
}
const data = await response.json();
return this.transformData(data);
} catch (error) {
console.error('Error fetching routers by facility:', error);
return [];
}
}
async checkApiStatus(): Promise<boolean> {
try {
const response = await fetch(`${API_BASE_URL}/routers`, {
method: 'GET',
...DEFAULT_OPTIONS
});
return response.ok;
} catch (error) {
console.error('API Status Check Failed:', error);
return false;
}
}
}
export const apiService = new ApiService();

View File

@ -0,0 +1,2 @@
// router-dashboard/src/services/index.ts
export * from './api.service';

View File

@ -0,0 +1,36 @@
// router-dashboard/src/types/backend.ts
export interface BackendStudy {
siuid: string;
patient_id: string;
accession_number: string;
patient_name: string;
study_date: string;
modality: string;
study_description: string;
}
export interface BackendVM {
id: number;
status: string;
status_code?: string;
}
export interface BackendRouter {
id: number;
slNo: number;
router_id: string;
facility: string;
router_alias: string;
last_seen: string;
disk_status_code: string;
disk_usage: number;
free_disk: number;
total_disk: number;
routerActivity?: {
studies: BackendStudy[];
};
vpn_status_code: string;
system_status?: {
vms: BackendVM[];
};
}

View File

@ -0,0 +1,41 @@
// router-dashboard/src/types/index.ts
export interface Study {
siuid: string;
patientId: string;
accessionNumber: string;
patientName: string;
studyDate: string;
modality: string;
studyDescription: string;
}
export interface VM {
id: number;
status: string;
}
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
export interface RouterData {
id: number;
slNo: number;
routerId: string;
facility: string;
routerAlias: string;
lastSeen: string;
diskStatus: string;
diskUsage: number;
freeDisk: number;
totalDisk: number;
routerActivity: {
studies: Study[];
};
systemStatus: {
vpnStatus: string;
appStatus: string;
vms: VM[];
};
}
// Export everything from backend types
export * from './backend';

View File

@ -0,0 +1,30 @@
// src/utils/statusHelpers.ts
// Define all possible status values
export type StatusType =
| 'RUNNING'
| 'STOPPED'
| 'WARNING'
| 'CONNECTED'
| 'DISCONNECTED'
| 'ERROR'
| 'UNKNOWN';
export const STATUS_COLORS: Record<StatusType | string, string> = {
'RUNNING': 'bg-green-100 text-green-700',
'CONNECTED': 'bg-green-100 text-green-700',
'STOPPED': 'bg-red-100 text-red-700',
'DISCONNECTED': 'bg-red-100 text-red-700',
'WARNING': 'bg-yellow-100 text-yellow-700',
'ERROR': 'bg-red-100 text-red-700',
'UNKNOWN': 'bg-gray-100 text-gray-700'
};
export const formatStatus = (status: string): string => {
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
};
// Add this helper function
export const getStatusColor = (status: string): string => {
return STATUS_COLORS[status] || STATUS_COLORS['UNKNOWN'];
};

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,12 @@
// File: tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

21
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

7
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})

135
sql/init.sql Normal file
View File

@ -0,0 +1,135 @@
-- Create and use database
CREATE DATABASE IF NOT EXISTS ve_router_db;
USE ve_router_db;
-- Router table
CREATE TABLE IF NOT EXISTS routers (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id VARCHAR(10) UNIQUE NOT NULL, -- Unique router identifier
facility VARCHAR(50) NOT NULL,
router_alias VARCHAR(50) NOT NULL,
last_seen TIMESTAMP NOT NULL,
vpn_status_code VARCHAR(50) NOT NULL,
disk_status_code VARCHAR(50) NOT NULL,
license_status ENUM('active', 'inactive', 'suspended') NOT NULL DEFAULT 'inactive',
free_disk BIGINT NOT NULL CHECK (free_disk >= 0),
total_disk BIGINT NOT NULL CHECK (total_disk > 0),
disk_usage DECIMAL(5,2) NOT NULL CHECK (disk_usage BETWEEN 0 AND 100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Users and Authentication tables
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'operator', 'viewer') NOT NULL DEFAULT 'viewer',
status ENUM('active', 'locked', 'disabled') NOT NULL DEFAULT 'active',
failed_login_attempts INT NOT NULL DEFAULT 0,
last_login TIMESTAMP NULL,
password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT valid_email CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
CONSTRAINT valid_username CHECK (username REGEXP '^[A-Za-z0-9_-]{3,50}$')
);
-- User-Router access permissions
CREATE TABLE IF NOT EXISTS user_router_access (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
router_id INT NOT NULL,
can_view BOOLEAN NOT NULL DEFAULT TRUE,
can_manage BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_user_router_access (user_id, router_id)
);
-- Session management
CREATE TABLE IF NOT EXISTS user_sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
session_token VARCHAR(255) UNIQUE NOT NULL,
refresh_token VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
expires_at TIMESTAMP NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL 24 HOUR),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT unique_session_token UNIQUE(session_token),
CONSTRAINT unique_refresh_token UNIQUE(refresh_token)
);
-- System status table
CREATE TABLE IF NOT EXISTS system_status (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Container status table
CREATE TABLE IF NOT EXISTS container_status (
id INT AUTO_INCREMENT PRIMARY KEY,
system_status_id INT NOT NULL,
container_number INT NOT NULL CHECK (container_number BETWEEN 1 AND 10),
status_code VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- VM details table
CREATE TABLE IF NOT EXISTS vm_details (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id INT NOT NULL,
vm_number INT NOT NULL CHECK (vm_number > 0),
status_code VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT unique_vm_per_router UNIQUE(router_id, vm_number)
);
-- DICOM study overview table with router_id as a string reference
CREATE TABLE IF NOT EXISTS dicom_study_overview (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id VARCHAR(10) NOT NULL, -- Matching VARCHAR(10) with the routers table
study_instance_uid VARCHAR(100) UNIQUE NOT NULL,
patient_id VARCHAR(50) NOT NULL,
patient_name VARCHAR(100) NOT NULL,
accession_number VARCHAR(50) NOT NULL,
study_date DATE NOT NULL,
modality VARCHAR(20) NOT NULL,
study_description VARCHAR(255),
series_instance_uid VARCHAR(100) NOT NULL,
procedure_code VARCHAR(50),
referring_physician_name VARCHAR(100),
study_status_code VARCHAR(50) NOT NULL DEFAULT 'NEW', -- Default value, ensure 'NEW' exists in status_type
association_id VARCHAR(50) NOT NULL DEFAULT 'NEW',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- History and Audit Tables (No changes required here)
CREATE TABLE IF NOT EXISTS router_status_history (
id INT AUTO_INCREMENT PRIMARY KEY,
router_id INT NOT NULL,
vpn_status_code VARCHAR(50) NOT NULL,
disk_status_code VARCHAR(50) NOT NULL,
license_status ENUM('active', 'inactive', 'suspended') NOT NULL,
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Status category table
CREATE TABLE IF NOT EXISTS status_category (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Additional history and settings tables as needed...

267
sql/seed_data.sql Normal file
View File

@ -0,0 +1,267 @@
DELIMITER //
CREATE PROCEDURE seed_complete_router_system()
BEGIN
-- Disable foreign key checks and start fresh
SET FOREIGN_KEY_CHECKS=0;
-- Conditionally clear existing data, only if the table exists
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'auth_log') THEN
TRUNCATE TABLE auth_log;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_sessions') THEN
TRUNCATE TABLE user_sessions;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'user_router_access') THEN
TRUNCATE TABLE user_router_access;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'users') THEN
TRUNCATE TABLE users;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'container_status_history') THEN
TRUNCATE TABLE container_status_history;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_status_history') THEN
TRUNCATE TABLE router_status_history;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'container_status') THEN
TRUNCATE TABLE container_status;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vm_details') THEN
TRUNCATE TABLE vm_details;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'dicom_study_overview') THEN
TRUNCATE TABLE dicom_study_overview;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'system_status') THEN
TRUNCATE TABLE system_status;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_settings_history') THEN
TRUNCATE TABLE router_settings_history;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'router_settings') THEN
TRUNCATE TABLE router_settings;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'routers') THEN
TRUNCATE TABLE routers;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'status_type') THEN
TRUNCATE TABLE status_type;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'status_category') THEN
TRUNCATE TABLE status_category;
END IF;
-- Re-enable foreign key checks
SET FOREIGN_KEY_CHECKS=1;
-- Insert status categories
INSERT INTO status_category (name, description)
VALUES
('Network', 'Network related statuses'),
('Disk', 'Disk related statuses'),
('VPN', 'VPN connection statuses'),
('License', 'License statuses'),
('Container', 'Container related statuses')
ON DUPLICATE KEY UPDATE id = id;
-- Insert status types
INSERT INTO status_type (category_id, name, code, description, severity)
VALUES
(1, 'Online', 'NET_ONLINE', 'System is online', 1),
(1, 'Offline', 'NET_OFFLINE', 'System is offline', 5),
(2, 'Normal', 'DISK_NORMAL', 'Disk usage is normal', 1),
(2, 'Warning', 'DISK_WARNING', 'Disk usage is high', 3),
(2, 'Critical', 'DISK_CRITICAL', 'Disk usage is critical', 5),
(3, 'Connected', 'VPN_CONNECTED', 'VPN is connected', 1),
(3, 'Disconnected', 'VPN_DISCONNECTED', 'VPN is disconnected', 5),
(5, 'Running', 'CONTAINER_RUNNING', 'Container is running', 1),
(5, 'Stopped', 'CONTAINER_STOPPED', 'Container is stopped', 5)
ON DUPLICATE KEY UPDATE id = id;
-- Insert routers
INSERT INTO routers (router_id, facility, router_alias, last_seen, vpn_status_code, disk_status_code, license_status, free_disk, total_disk, disk_usage)
VALUES
('RTR001', 'Main Hospital', 'MAIN_RAD', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'active', 500000000000, 1000000000000, 50.00),
('RTR002', 'Emergency Center', 'ER_RAD', NOW(), 'VPN_CONNECTED', 'DISK_WARNING', 'active', 400000000000, 1000000000000, 60.00),
('RTR003', 'Imaging Center', 'IMG_CENTER', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'active', 600000000000, 1000000000000, 40.00)
ON DUPLICATE KEY UPDATE id = id;
-- Store router IDs for later use
SET @router1_id = (SELECT id FROM routers WHERE router_id = 'RTR001');
SET @router2_id = (SELECT id FROM routers WHERE router_id = 'RTR002');
SET @router3_id = (SELECT id FROM routers WHERE router_id = 'RTR003');
-- Insert system status
INSERT INTO system_status (router_id)
VALUES
(@router1_id),
(@router2_id),
(@router3_id)
ON DUPLICATE KEY UPDATE id = id;
-- Insert container status
INSERT INTO container_status (system_status_id, container_number, status_code)
VALUES
(1, 1, 'CONTAINER_RUNNING'),
(1, 2, 'CONTAINER_RUNNING'),
(2, 1, 'CONTAINER_RUNNING'),
(2, 2, 'CONTAINER_STOPPED'),
(3, 1, 'CONTAINER_RUNNING')
ON DUPLICATE KEY UPDATE id = id;
-- Insert VM details
INSERT INTO vm_details (router_id, vm_number, status_code)
VALUES
(@router1_id, 1, 'NET_ONLINE'),
(@router2_id, 1, 'NET_ONLINE'),
(@router3_id, 1, 'NET_ONLINE')
ON DUPLICATE KEY UPDATE id = id;
-- Insert DICOM studies
INSERT INTO dicom_study_overview (
router_id,
study_instance_uid,
patient_id,
patient_name,
accession_number,
study_date,
modality,
study_description,
series_instance_uid,
procedure_code,
referring_physician_name
)
VALUES
(@router1_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.134', 'P1', 'John Doe', 'ACC1234', '2024-03-15', 'CT', 'Chest CT', '1.2.840.113619.2.55.3.283116435.276.1543707219.135', 'CT001', 'Dr. Smith'),
(@router2_id, '1.2.840.113619.2.55.3.283116435.276.1543707218.136', 'P2', 'Jane Doe', 'ACC1235', '2024-03-15', 'MR', 'Brain MRI', '1.2.840.113619.2.55.3.283116435.276.1543707219.137', 'MR001', 'Dr. Johnson')
ON DUPLICATE KEY UPDATE id = id;
-- Insert router settings for each router (calls to upsert_router_settings are disabled)
-- Main Hospital Router
-- CALL upsert_router_settings(
-- @router1_id,
-- 'client',
-- '{
-- "dicom": {
-- "local": {
-- "aet": "MAIN_RAD",
-- "port": 104,
-- "file_directory": "/dicom_images",
-- "wait_time": 2,
-- "receiver_wait_time": 5000
-- },
-- "association": {
-- "acse_timeout": 5,
-- "dimse_timeout": 1000,
-- "network_timeout": 1000,
-- "retry": {
-- "attempts": 3,
-- "interval": 10
-- }
-- }
-- },
-- "rabbitmq": {
-- "local": {
-- "hostname": "router-rabbitmq",
-- "port": 5672,
-- "credentials": {
-- "username": "vitalengine",
-- "password": "vitalengine"
-- },
-- "settings": {
-- "durable": true,
-- "auto_delete": false,
-- "exchange_type": "direct",
-- "heartbeat": 50
-- }
-- }
-- },
-- "scp_connections": {
-- "pacs_nodes": [
-- {
-- "host": "pacsmain.example.com",
-- "port": 104
-- },
-- {
-- "host": "pacsbackup.example.com",
-- "port": 104
-- }
-- ]
-- }
-- }',
-- 'system',
-- 'Initial client configuration for Main Hospital'
-- );
-- Emergency Center Router
-- CALL upsert_router_settings(
-- @router2_id,
-- 'client',
-- '{
-- "dicom": {
-- "local": {
-- "aet": "ER_RAD",
-- "port": 104,
-- "file_directory": "/dicom_images",
-- "wait_time": 2,
-- "receiver_wait_time": 5000
-- },
-- "association": {
-- "acse_timeout": 5,
-- "dimse_timeout": 1000,
-- "network_timeout": 1000,
-- "retry": {
-- "attempts": 3,
-- "interval": 10
-- }
-- }
-- },
-- "rabbitmq": {
-- "local": {
-- "hostname": "router-rabbitmq",
-- "port": 5672,
-- "credentials": {
-- "username": "vitalengine",
-- "password": "vitalengine"
-- },
-- "settings": {
-- "durable": true,
-- "auto_delete": false,
-- "exchange_type": "direct",
-- "heartbeat": 50
-- }
-- }
-- },
-- "scp_connections": {
-- "pacs_nodes": [
-- {
-- "host": "pacsemergency.example.com",
-- "port": 104
-- }
-- ]
-- }
-- }',
-- 'system',
-- 'Initial client configuration for Emergency Center'
-- );
-- Insert settings for other routers as needed...
END //
DELIMITER ;

25
start.bat Normal file
View File

@ -0,0 +1,25 @@
@echo off
echo Stopping containers...
docker-compose down
echo Removing old volume...
docker-compose down -v
echo Starting services...
docker-compose up -d
echo Waiting for MySQL to be ready...
REM Replace timeout with ping for Windows
ping -n 30 127.0.0.1 > nul
echo Checking MySQL status...
docker-compose exec mysql mysqladmin -u ve_router_user -pve_router_password ping
IF %ERRORLEVEL% EQU 0 (
echo MySQL is up and running!
echo Showing tables in ve_router_db:
docker-compose exec mysql mysql -u ve_router_user -pve_router_password -h mysql ve_router_db -e "SHOW TABLES;"
) ELSE (
echo MySQL is not responding. Please check the logs:
docker-compose logs mysql
)

18
start.ps1 Normal file
View File

@ -0,0 +1,18 @@
# init-db.ps1
Write-Host "Stopping containers..."
docker-compose down
Write-Host "Removing old volume..."
docker-compose down -v
Write-Host "Starting services..."
docker-compose up -d
Write-Host "Waiting for MySQL to be ready..."
Start-Sleep -Seconds 30
Write-Host "Checking MySQL status..."
docker-compose logs mysql
Write-Host "Attempting to connect to MySQL..."
docker-compose exec mysql mysql -u ve_router_user -pve_router_password -h localhost ve_router_db -e "SHOW TABLES;"