Compare commits

...

3 Commits

Author SHA1 Message Date
dfa974fe6b Remove study count showing on Activity column. Collapse Navigation bar by default. Show Active and Idle for router activity. Fixed card view count. Fixed card view router row info as per correct status. Show only one recent study on Activity column. Show router details on click of router id to expanded row. 2024-11-28 20:08:10 +05:30
8630a3e2c5 Fixed React backend startup issue.
Updated SQL script to correct procedure and column sizes.
Moved SQL scripts to the project root folder (outside React frontend).
Resolved backend container dependency issue to ensure MySQL is up before starting React backend.
Moved common values to .env file in the project root.
Updated React backend and MySQL ports to use default values.
Added code to get last study received, containers status and updated into DB.
2024-11-22 13:44:03 +05:30
8fe130f918 Fixed React backend startup issue.
Updated SQL script to correct procedure and column sizes.
Moved SQL scripts to the project root folder (outside React frontend).
Resolved backend container dependency issue to ensure MySQL is up before starting React backend.
Moved common values to .env file in the project root.
Updated React backend and MySQL ports to use default values.
2024-11-18 10:16:05 +05:30
39 changed files with 1065 additions and 571 deletions

6
.env Normal file
View File

@ -0,0 +1,6 @@
VITE_API_URL=http://localhost:3000/api/v1
NODE_ENV=development
#Database Configuration
DB_NAME=ve_router_db

25
db-scripts/00-init-db.sh Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
# Wait for MySQL to become available
echo "Waiting for MySQL to become healthy..."
until mysql -h "localhost" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SHOW DATABASES;" &>/dev/null; do
echo "Waiting for MySQL..."
sleep 2
done
echo "MySQL is up and running!"
# Run the initial common setup (always executed)
echo "Running 01-init.sql"
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < /docker-entrypoint-initdb.d/01-init.sql
echo "Running 02-seed_common_data.sql"
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < /docker-entrypoint-initdb.d/02-seed_common_data.sql
# Check the environment variable and execute environment-specific scripts
if [ "$ENVIRONMENT" != "production" ]; then
echo "Running 03-seed_router_data_qa.sql for QA (non-production)"
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < /docker-entrypoint-initdb.d/03-seed_router_data_qa.sql
else
echo "Production environment detected. Skipping seeding data for QA (non-production)."
fi

View File

@ -8,9 +8,13 @@ CREATE TABLE IF NOT EXISTS routers (
router_id VARCHAR(10) UNIQUE NOT NULL, -- Unique router identifier
facility VARCHAR(50) NOT NULL,
router_alias VARCHAR(50) NOT NULL,
facility_aet VARCHAR(50) NOT NULL,
openvpn_ip VARCHAR(15) NOT NULL,
router_vm_primary_ip VARCHAR(15) NOT NULL,
last_seen TIMESTAMP NOT NULL,
vpn_status_code VARCHAR(50) NOT NULL,
disk_status_code VARCHAR(50) NOT NULL,
app_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),
@ -63,33 +67,15 @@ CREATE TABLE IF NOT EXISTS user_sessions (
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)
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
router_id varchar(50) NOT NULL,
container_name varchar(50) NOT NULL,
status_code varchar(50) NOT NULL,
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE(router_id, container_name)
);
-- DICOM study overview table with router_id as a string reference
@ -118,7 +104,7 @@ CREATE TABLE IF NOT EXISTS status_type (
category_id VARCHAR(50),
name VARCHAR(100),
code VARCHAR(100),
description VARCHAR(20),
description VARCHAR(150),
severity INT
);

View File

@ -0,0 +1,34 @@
-- Check if the procedure exists, and create it only if it does not
DROP PROCEDURE IF EXISTS `seed_common_router_data`;
DELIMITER //
CREATE PROCEDURE seed_common_router_data()
BEGIN
-- 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;
END //
DELIMITER ;

View File

@ -0,0 +1,49 @@
-- Check if the procedure exists, and create it only if it does not
DROP PROCEDURE IF EXISTS `seed_router_data`;
DELIMITER //
CREATE PROCEDURE seed_router_data()
BEGIN
-- Insert Routers
INSERT INTO routers (router_id, facility, router_alias, facility_aet, openvpn_ip, router_vm_primary_ip,
last_seen, vpn_status_code, disk_status_code, app_status_code, license_status, free_disk, total_disk, disk_usage)
VALUES
('RTR001', 'Main Hospital', 'MAIN_RAD', 'RTR_1', '10.8.0.101', '192.168.1.101', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'CONTAINER_RUNNING', 'active', 500000000000, 1000000000000, 50.00),
('RTR002', 'Emergency Center', 'ER_RAD', 'RTR_2', '10.8.0.102', '192.168.1.102', NOW(), 'VPN_DISCONNECTED', 'DISK_WARNING', 'CONTAINER_RUNNING', 'active', 400000000000, 1000000000000, 60.00),
('RTR003', 'Imaging Center', 'IMG_CENTER', 'RTR_3', '10.8.0.103', '192.168.1.103', NOW(), 'VPN_CONNECTED', 'DISK_NORMAL', 'CONTAINER_RUNNING', 'active', 600000000000, 1000000000000, 40.00)
ON DUPLICATE KEY UPDATE id = id;
-- Store Router IDs
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 Container Status
INSERT INTO container_status (router_id, container_name, status_code, created_at, updated_at)
VALUES
(@router1_id, 'router-cstore-scp', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router1_id, 'router-cstore-scu', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router2_id, 'router-cstore-scp', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router2_id, 'router-cstore-scu', 'CONTAINER_RUNNING', NOW(), NOW()),
(@router3_id, 'router-cstore-scp', 'CONTAINER_RUNNING', NOW(), NOW())
ON DUPLICATE KEY UPDATE id = id;
-- Insert DICOM Study Overview
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
)
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', 'idle'),
(@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', 'idle')
ON DUPLICATE KEY UPDATE id = id;
END //
DELIMITER ;
-- Automatically call the procedure after creation
CALL seed_router_data();

142
deploy.sh Normal file
View File

@ -0,0 +1,142 @@
#!/bin/bash
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to print colored messages
print_message() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Function to check if command was successful
check_status() {
if [ $? -eq 0 ]; then
print_message "$GREEN" "✔ Success: $1"
else
print_message "$RED" "✘ Error: $1"
exit 1
fi
}
# Default values
ENV="production"
SERVER_IP=""
FRONTEND_IP=""
DB_HOST="mysql"
# Help message
show_help() {
echo "Usage: ./deploy.sh [OPTIONS]"
echo "Deploy the router dashboard application"
echo
echo "Options:"
echo " -s, --server-ip Server IP address (required)"
echo " -f, --frontend-ip Frontend IP address (defaults to server IP if not provided)"
echo " -d, --db-host Database host (defaults to mysql)"
echo " -e, --environment Environment (development/staging/production, defaults to production)"
echo " -h, --help Show this help message"
echo
echo "Example:"
echo " ./deploy.sh -s 192.168.1.100 -f 192.168.1.101 -e production"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-s|--server-ip)
SERVER_IP="$2"
shift 2
;;
-f|--frontend-ip)
FRONTEND_IP="$2"
shift 2
;;
-d|--db-host)
DB_HOST="$2"
shift 2
;;
-e|--environment)
ENV="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
print_message "$RED" "Unknown option: $1"
show_help
exit 1
;;
esac
done
# Validate required parameters
if [ -z "$SERVER_IP" ]; then
print_message "$RED" "Error: Server IP is required"
show_help
exit 1
fi
# If frontend IP is not provided, use server IP
if [ -z "$FRONTEND_IP" ]; then
FRONTEND_IP=$SERVER_IP
print_message "$YELLOW" "Frontend IP not provided, using Server IP: $FRONTEND_IP"
fi
# Export environment variables
export SERVER_IP
export FRONTEND_IP
export DB_HOST
export NODE_ENV=$ENV
# Display deployment information
print_message "$GREEN" "\nDeployment Configuration:"
echo "Server IP: $SERVER_IP"
echo "Frontend IP: $FRONTEND_IP"
echo "Database Host: $DB_HOST"
echo "Environment: $ENV"
echo
# Confirm deployment
read -p "Do you want to continue with deployment? (y/n) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
print_message "$YELLOW" "Deployment cancelled"
exit 1
fi
# Start deployment
print_message "$GREEN" "\nStarting deployment..."
# Pull latest changes
print_message "$YELLOW" "Pulling latest changes..."
git pull
check_status "Git pull"
# Stop existing containers
print_message "$YELLOW" "Stopping existing containers..."
docker-compose down
check_status "Stopping containers"
# Build containers
print_message "$YELLOW" "Building containers..."
docker-compose build
check_status "Building containers"
# Start containers
print_message "$YELLOW" "Starting containers..."
docker-compose up -d
check_status "Starting containers"
# Check container status
print_message "$YELLOW" "Checking container status..."
docker-compose ps
check_status "Container status check"
print_message "$GREEN" "\nDeployment completed successfully!"

View File

@ -4,57 +4,87 @@ services:
frontend:
build:
context: ./router-dashboard
dockerfile: Dockerfile
dockerfile: dockerfile
args:
- SERVER_IP=${SERVER_IP:-localhost}
ports:
- "5173:5173"
- "${FRONTEND_PORT:-5173}:5173"
environment:
- VITE_API_URL=http://localhost:3001/api/v1
- SERVER_IP=${SERVER_IP:-localhost}
- FRONTEND_IP=${FRONTEND_IP:-localhost}
- VITE_API_URL=http://${SERVER_IP:-localhost}:${BACKEND_PORT:-3000}/api/v1
- VITE_NODE_ENV=${NODE_ENV:-development}
restart: always
depends_on:
- backend
volumes:
- ./router-dashboard:/app
- /app/node_modules
backend:
condition: service_healthy
container_name: router_dashboard_frontend
networks:
- app_network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://${SERVER_IP:-localhost}:5173"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
backend:
build:
context: ./ve-router-backend
dockerfile: Dockerfile
dockerfile: dockerfile
ports:
- "3001:3000"
- "${BACKEND_PORT:-3000}: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
- NODE_ENV=${NODE_ENV:-development}
- DB_HOST=${DB_HOST:-mysql}
- DB_PORT=${DB_PORT:-3306}
- DB_USER=${DB_USER:-ve_router_user}
- DB_PASSWORD=${DB_PASSWORD:-ve_router_password}
- DB_NAME=${DB_NAME:-ve_router_db}
- CORS_ORIGIN=http://${FRONTEND_IP:-localhost}:${FRONTEND_PORT:-5173},http://${SERVER_IP:-localhost}:${BACKEND_PORT:-3000}
restart: always
depends_on:
- mysql
volumes:
- ./ve-router-backend:/app
- /app/node_modules
mysql:
condition: service_healthy
healthcheck:
test: ["CMD", "nc", "-z", "${SERVER_IP:-localhost}", "${BACKEND_PORT:-3000}"]
interval: 30s
retries: 3
start_period: 30s
timeout: 10s
networks:
- app_network
container_name: router_dashboard_backend
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
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: ${DB_NAME:-ve_router_db}
MYSQL_USER: ${DB_USER:-ve_router_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-ve_router_password}
ENVIRONMENT: ${NODE_ENV:-development}
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
- ./db-scripts:/docker-entrypoint-initdb.d:ro
ports:
- "3307:3306"
- "${DB_PORT:-3306}:3306"
command: --default-authentication-plugin=mysql_native_password
restart: always
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "ve_router_user", "-pve_router_password"]
test: ["CMD", "mysqladmin", "ping", "-h", "${DB_HOST:-mysql}", "-u${DB_USER:-ve_router_user}", "-p${DB_PASSWORD:-ve_router_password}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- app_network
container_name: router_dashboard_mysql
networks:
app_network:
driver: bridge
volumes:
mysql_data:
name: router_dashboard_mysql_data

38
readme.txt Normal file
View File

@ -0,0 +1,38 @@
1. Go to router-dashboard directory
2. Run below command to build the docker images, create sql schema, insert data and start containers
docker-compose up --build -d
3. When code changes done, then just run above command mentioned in point 2,
it will udpate the changes and restart containers for which code changed
4. Open below URL in web browser to verify UI
http://localhost:5173
5. Open mysql workbench/any tool to view schema details
database:ve_router_db
host: localhost
port:3306
user/password: ve_router_user/ve_router_password
6. Run below command to stop and remove containers
docker-compose down
7. Run below command to stop, remove and delete all the volumes
Caution : if mysql has volumes then all the existing data will be erasesd
docker-compose down -v
Deploy Instructions:
1. Basic usage with just server IP:
./deploy.sh -s 192.168.1.100
2.Specify different IPs for frontend and backend:
./deploy.sh -s 192.168.1.100 -f 192.168.1.101
3.Specify environment:
./deploy.sh -s 192.168.1.100 -e staging
4.Full configuration:
./deploy.sh -s 192.168.1.100 -f 192.168.1.101 -d mysql -e production
5.Run it with the help flag to see options:
./deploy.sh --help
Important notes:
In production, you should use HTTPS instead of HTTP
Make sure your firewall rules allow the necessary ports (3000, 5173, 3306)
Consider using a reverse proxy like Nginx in front of your services
The MySQL container is accessible to other containers through the service name "mysql" when using Docker networks

View File

@ -1,2 +1,2 @@
VITE_API_URL=http://localhost:3001/api/v1
VITE_API_URL=http://${SERVER_IP:-localhost}:3000/api/v1
VITE_NODE_ENV=development

View File

@ -0,0 +1,2 @@
VITE_API_URL=http://${SERVER_IP}:3000/api/v1
VITE_NODE_ENV=production

View File

@ -8,7 +8,7 @@ RUN npm install
COPY . .
ENV VITE_API_URL=http://localhost:3001/api/v1
ENV VITE_API_URL=http://localhost:3000/api/v1
EXPOSE 5173

View File

@ -2,9 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<link rel="icon" type="image/png" href="ve.png" >
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<!-- <title>Vite + React + TS </title> -->
<title>Router Management</title>
</head>
<body>
<div id="root"></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

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

@ -1 +1,99 @@
<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>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 225.9 45.8" style="enable-background:new 0 0 225.9 45.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#231F52;}
.st1{fill:#05887A;}
.st2{fill:#87C35F;}
.st3{fill:#68C7D7;}
.st4{fill:#24767B;}
.st5{fill:#2377B5;}
.st6{fill:#069666;}
.st7{fill:#60B046;}
.st8{fill:#ECE843;}
.st9{fill:#D8DF25;}
.st10{fill:#204780;}
.st11{fill:#81BC42;}
.st12{fill:#244087;}
.st13{fill:#224480;}
.st14{fill:#148745;}
.st15{fill:#298DC9;}
.st16{fill:#17464A;}
.st17{fill:#2B67A8;}
.st18{fill:#25AC6D;}
.st19{fill:#276780;}
.st20{fill:#2783C1;}
.st21{fill:#2894CE;}
</style>
<g id="VitalEngine_Horizontal_4c" transform="translate(-135.213 -220.46)">
<g id="Group_3" transform="translate(188.083 239.605)">
<g id="Group_2" transform="translate(67.686)">
<g id="Group_1">
<path id="Path_1" class="st0" d="M14.4,22H0V0.9h14.4v2.9h-11v5.8h9.1v2.9H3.4v6.6h11V22z"/>
<path id="Path_2" class="st0" d="M18.2,22V5.7h3.1v2c2-1.7,2.7-2,4.1-2h2.6c1-0.1,2,0.2,2.8,0.9c0.6,0.6,1,1.4,1,3.7V22h-3.1
V10.5c0.1-0.6-0.1-1.1-0.4-1.6c-0.3-0.3-0.6-0.5-1.7-0.5h-2.1c-1.1,0-2.1,0.4-3,0.9V22L18.2,22L18.2,22z"/>
<path id="Path_3" class="st0" d="M39.8,26.5c-1,0.1-1.9-0.2-2.7-0.8c-0.6-0.6-0.9-1.4-0.9-3.2h3.1c0,0.7,0.1,1,0.3,1.2
c0.2,0.2,0.4,0.2,1.2,0.2h3.9c0.9,0,1.2-0.1,1.4-0.3c0.2-0.2,0.3-0.5,0.3-2v-3.4c-2.1,1.7-2.7,2-4.1,2h-2.2
c-1.7,0-2.6-0.3-3.3-1c-0.9-0.9-1.3-1.8-1.3-6.3s0.4-5.4,1.3-6.3c0.7-0.7,1.6-1,3.3-1h2.2c1.3,0,2,0.3,4.1,2v-2h3.1v15.5
c0,2.9-0.3,3.7-1,4.4c-0.5,0.5-1.3,0.9-3.2,0.9L39.8,26.5L39.8,26.5z M46.6,16.5V9.4c-1-0.6-2-0.9-3.2-1h-2.3
c-1,0-1.4,0.1-1.7,0.4C39,9.3,38.9,9.7,38.9,13s0.1,3.6,0.6,4.1c0.3,0.3,0.7,0.4,1.7,0.4h2.4C44.7,17.4,45.7,17.1,46.6,16.5
L46.6,16.5z"/>
<path id="Path_4" class="st0" d="M57.7,3.4h-3.6V0h3.6V3.4z M57.5,22h-3.1V5.7h3.1V22z"/>
<path id="Path_5" class="st0" d="M62.1,22V5.7h3.1v2c2-1.7,2.7-2,4.1-2h2.6c1-0.1,2,0.2,2.8,0.9c0.6,0.6,1,1.4,1,3.7V22h-3.1
V10.5c0.1-0.6-0.1-1.1-0.4-1.6c-0.3-0.3-0.6-0.5-1.7-0.5h-2.1c-1.1,0-2.1,0.4-3,0.9V22L62.1,22L62.1,22z"/>
<path id="Path_6" class="st0" d="M82.7,14.9c0,3.2,0.2,3.8,0.5,4.1s0.5,0.3,1.4,0.3h4.2c0.7,0,0.9-0.1,1.1-0.3
c0.2-0.2,0.3-0.6,0.3-2h3c-0.1,2.5-0.3,3.4-1.1,4.2c-0.7,0.6-1.7,0.9-2.7,0.8h-5.3c-1.2,0.1-2.3-0.2-3.2-0.9
c-1.1-1.1-1.4-2.3-1.4-7.2s0.3-6.1,1.4-7.2c0.7-0.7,1.6-0.9,3.2-0.9h4.5c1.2-0.1,2.3,0.2,3.2,0.9c1.1,1.1,1.4,2.3,1.4,7.1v0.8
c0,0.2-0.1,0.4-0.4,0.4L82.7,14.9L82.7,14.9z M82.7,12.4h7.4c0-2.7-0.2-3.3-0.5-3.6c-0.2-0.2-0.5-0.3-1.4-0.3h-3.7
c-0.9,0-1.2,0.1-1.4,0.3C82.9,9.1,82.8,9.6,82.7,12.4z"/>
</g>
</g>
<path id="Path_7" class="st0" d="M20.8,3.4h3.6V0h-3.6V3.4z M21,22h3.1V5.7H21L21,22z M9.4,18.7H9L3.6,0.9H0l6.4,20.4
c0.2,0.7,0.5,0.8,1.2,0.8h2.9c0.8,0,1-0.1,1.2-0.8l6.4-20.4h-3.4L9.4,18.7z M49.1,5.7H44c-1-0.1-2,0.2-2.7,0.9
c-0.6,0.6-0.9,1.4-1,4h3.1c0.1-1.2,0.1-1.8,0.4-2c0.2-0.2,0.5-0.3,1.3-0.3h3.2c0.9,0,1.2,0.1,1.4,0.3c0.2,0.2,0.4,0.7,0.4,2.6v1.8
c-1.3-0.3-2.7-0.5-4-0.5c-3.9,0-4.7,0.5-5.4,1.2c-0.6,0.6-1,1.9-1,3.8c0,2,0.4,3,1,3.6c0.7,0.7,1.4,0.8,2.9,0.8h2.5
c1.4,0,2-0.4,4-2v2h3.1V11c0-2.9-0.2-3.7-0.9-4.4C51.7,6.1,50.9,5.7,49.1,5.7z M50.1,18.3c-1,0.7-2.2,1-3.4,1h-2.3
c-0.7,0-1.1,0-1.3-0.2c-0.2-0.2-0.3-0.8-0.3-2c0-1.1,0.1-1.4,0.4-1.7c0.3-0.3,0.7-0.4,2.7-0.4h4.2L50.1,18.3L50.1,18.3z M33,1.7
h-3.2v4h-2.8v2.7h2.8v10.4c-0.1,0.9,0.1,1.8,0.7,2.5c0.7,0.6,1.6,0.9,2.4,0.8c0.7,0,1.4-0.1,2-0.2l1.8-0.5v-2.1h-2.5
c-0.8,0-1.1,0-1.2-0.1C33.1,18.9,33,18.7,33,18V8.5h3.9V5.7H33L33,1.7z M62.2,19.2c-0.8,0-1.1,0-1.2-0.1c-0.1-0.1-0.2-0.4-0.2-1.1
V8.5l0,0V0.3h-3.1v8.2h0v10.4c-0.1,0.9,0.1,1.8,0.7,2.5c0.7,0.6,1.6,0.9,2.4,0.8c0.7,0,1.4-0.1,2-0.2l1.8-0.5v-2.1H62.2L62.2,19.2
z"/>
</g>
<g id="Group_4" transform="translate(135.213 220.46)">
<path id="Path_8" class="st1" d="M29.6,25.9l-4.1,7.6L17,18.4h8.4L29.6,25.9z"/>
<path id="Path_9" class="st2" d="M25.4,18.4l5-9.2h10.1l-4.2,7.6c0,0-1,0-1.5,0c-0.2,0-0.4,0.1-0.4,0.2c-0.2,0.4-0.8,1.3-0.8,1.3
L25.4,18.4z"/>
<path id="Path_10" class="st3" d="M11.9,9.3l-1.8-3.2L9.2,7.5L5.1,0l9.8,0c0.2,0,0.4,0.1,0.5,0.3c1.3,2.3,5,8.9,5,8.9L11.9,9.3z"
/>
<path id="Path_11" class="st4" d="M23.6,36.6l1.8,3.2l0.8-1.4l4.2,7.4c0,0,0,0-0.2,0c-3.2,0-6.4,0-9.7,0c-0.1,0-0.3-0.1-0.3-0.2
l-5-9L23.6,36.6z"/>
<path id="Path_12" class="st5" d="M5.1,0l4.2,7.5l-1,1.8c0,0-7.7,0-8.3,0c0-0.1,0-0.1,0-0.2c0.3-0.5,4.1-7.5,4.9-8.9
C4.9,0.2,4.9,0.1,5.1,0C5,0,5,0,5.1,0z"/>
<path id="Path_13" class="st6" d="M40.6,27.5l-5.1,9.1h-8.3l5-9.1L40.6,27.5z"/>
<path id="Path_14" class="st7" d="M30.4,9.2c0,0,4.4-8,4.8-8.8c0.1-0.1,0.2-0.2,0.3-0.3l5.1,9.1L30.4,9.2z"/>
<path id="Path_15" class="st8" d="M40.6,9.2c0,0,5-9,5.1-9.1c1.5,2.8,5,9.1,5,9.1c0,0,0,0,0,0C50.6,9.2,43.9,9.2,40.6,9.2z"/>
<path id="Path_16" class="st9" d="M45.7,0.1l-5.1,9.1l-5.1-9.1c0,0,0-0.1,0.3-0.1c3.2,0,6.3,0,9.5,0C45.5,0,45.6,0,45.7,0.1z"/>
<path id="Path_17" class="st10" d="M27.2,36.7h8.3c0,0-4.6,8.4-5,9c0,0-0.1,0.1-0.1,0.1l-4.2-7.4L27.2,36.7z"/>
<path id="Path_18" class="st11" d="M50.7,9.2c0,0-2.8,5.2-4,7.4c-0.1,0.2-0.2,0.3-0.4,0.2c-0.5,0-1.5,0-1.5,0l-4.2-7.6L50.7,9.2z"
/>
<path id="Path_19" class="st12" d="M19.5,29.2l-4.2,7.4l-5.1-9.1l8.3,0L19.5,29.2z"/>
<path id="Path_20" class="st13" d="M25.4,18.4H17l-0.9-1.6l4.2-7.6L25.4,18.4z"/>
<path id="Path_21" class="st14" d="M40.6,27.5L36.4,20c0.1-0.1,0.2-0.1,0.3-0.1h8.1L40.6,27.5z"/>
<path id="Path_22" class="st7" d="M44.8,16.8h-8.4l4.2-7.6L44.8,16.8z"/>
<path id="Path_23" class="st15" d="M16.1,16.8l-4.2-7.5l8.4-0.1L16.1,16.8z"/>
<path id="Path_24" class="st16" d="M36.4,20l4.2,7.5l-8.4,0c0,0,2.9-5.3,4-7.3C36.3,20.1,36.3,20.1,36.4,20z"/>
<path id="Path_25" class="st17" d="M14.4,20c0.2,0.4,4.2,7.5,4.2,7.5l-8.3,0C10.2,27.5,14.3,20.1,14.4,20z"/>
<path id="Path_26" class="st18" d="M25.4,18.4l8.3,0l-4.1,7.5L25.4,18.4z"/>
<path id="Path_27" class="st19" d="M19.5,29.2l4.1,7.4l-8.3,0L19.5,29.2z"/>
<path id="Path_28" class="st20" d="M6,20l4.2,7.5l4.2-7.5L6,20z"/>
<path id="Path_29" class="st21" d="M0,9.2l8.3,0l-4.1,7.5L0,9.2z"/>
<path id="Path_30" class="st3" d="M14.4,20L6,20l4.1-7.6L14.4,20z"/>
</g>
</g>
<g>
<path class="st0" d="M218.2,16.4h-2v5.5h-1.4v-5.5h-2v-1.1h5.4V16.4z"/>
<path class="st0" d="M220.7,15.3l1.7,4.8l1.7-4.8h1.8v6.6h-1.4v-1.8l0.1-3.1l-1.8,4.9h-0.9l-1.8-4.9l0.1,3.1v1.8h-1.4v-6.6H220.7z"
/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -40,7 +40,7 @@ const Dashboard = () => {
router.diskStatus === 'DISK_CRITICAL'
).length,
diskWarnings: data.filter(router =>
router.diskUsage >= 80
router.diskUsage >= 70
).length
};

View File

@ -36,11 +36,10 @@ const DashboardLayout: React.FC = () => {
data = data.filter(router => {
switch (activeFilter) {
case 'active':
return router.routerActivity?.studies &&
router.routerActivity.studies.length > 0;
return router.systemStatus.routerStatus === 'CONNECTED' &&
router.routerActivity?.studies?.some(study => study.studyStatusCode === 'Active');
case 'critical':
return router.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
router.systemStatus.appStatus === 'DISK_CRITICAL' ||
return router.systemStatus.routerStatus === 'DISCONNECTED' ||
router.diskStatus === 'DISK_CRITICAL';
case 'diskAlert':
return router.diskUsage > 80;
@ -74,18 +73,19 @@ const DashboardLayout: React.FC = () => {
return {
total: routerData.length,
active: routerData.filter(r =>
r.routerActivity?.studies &&
r.routerActivity.studies.length > 0
r.systemStatus.routerStatus === 'CONNECTED' && // Router (VM, app, VPN) is up
r.routerActivity?.studies?.some(study => study.studyStatusCode === 'Active') // At least one study is active
).length,
critical: routerData.filter(r =>
r.systemStatus.vpnStatus === 'VPN_DISCONNECTED' ||
r.systemStatus.appStatus === 'DISK_CRITICAL' ||
r.diskStatus === 'DISK_CRITICAL'
r.systemStatus.routerStatus === 'DISCONNECTED' || // Router (VM, app, VPN) is down
r.diskStatus === 'DISK_CRITICAL' // Disk is critical
).length,
diskAlert: routerData.filter(r => r.diskUsage > 80).length
diskAlert: routerData.filter(r => r.diskUsage > 80).length // Disk usage alert
};
};
const renderContent = () => {
if (loading) {
return (
@ -128,6 +128,7 @@ const DashboardLayout: React.FC = () => {
activeFilter === 'active' ? 'ring-2 ring-blue-500' : ''
}`}
onClick={() => setActiveFilter('active')}
title="Study in transmit currently"
>
<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>

View File

@ -22,8 +22,8 @@ interface Tab {
}
const Navbar: React.FC<NavbarProps> = ({ activeTab, onTabChange }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const [isPinned, setIsPinned] = useState(true);
const [isCollapsed, setIsCollapsed] = useState(true);
const [isPinned, setIsPinned] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const tabs: Tab[] = [

View File

@ -8,7 +8,7 @@ interface RouterTableRowProps {
router: RouterData;
expandedRows: Set<string>;
timeLeft: { [key: string]: number };
onToggleExpansion: (id: number, section: 'activity' | 'status' | 'disk') => void;
onToggleExpansion: (id: number, section: 'router_details' |'activity' | 'status' | 'disk') => void;
onExpandedContentHover: (id: number, section: 'activity' | 'status' | 'disk') => void;
}
@ -41,6 +41,32 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
return 'bg-green-500';
};
const renderRouterDetailsPanel = () => (
<div
className="bg-gray-50 p-4 relative"
onMouseEnter={() => onExpandedContentHover(router.id, 'router_details')}
>
<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}-router_details`] || 0}%` }}
/>
</div>
<h3 className="font-semibold mb-3">Router Details</h3>
<div className="grid grid-cols-3 gap-4">
<div className="bg-white p-3 rounded shadow-sm">
<div className="grid gap-2">
<div><span className="font-medium">Facility AET:</span> {router.facilityAET}</div>
<div><span className="font-medium">OpenVPN IP:</span> {router.openvpnIp}</div>
<div><span className="font-medium">Router VM Primary IP:</span> {router.routerVmPrimaryIp}</div>
</div>
</div>
</div>
</div>
);
const renderActivityPanel = () => (
<div
className="bg-gray-50 p-4 relative"
@ -88,6 +114,12 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
/>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<h4 className="font-semibold mb-2">VM Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vmStatus)}`}>
{formatStatus(router.systemStatus.vmStatus)}
</span>
</div>
<div>
<h4 className="font-semibold mb-2">VPN Status</h4>
<span className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
@ -96,22 +128,22 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
</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 className={`px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.appStatus)}`}>
{formatStatus(router.systemStatus.appStatus)}
</span>
</div>
<div>
<h4 className="font-semibold mb-2">VM Status</h4>
{router.systemStatus.vms.length > 0 ? (
router.systemStatus.vms.map((vm, idx) => (
<h4 className="font-semibold mb-2">Container Status</h4>
{router.systemStatus.containers.length > 0 ? (
router.systemStatus.containers.map((container, 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 className={`px-2 py-1 rounded-full text-sm ${getStatusColor(container.status)}`}>
{container.container_name}: {formatStatus(container.status)}
</span>
</div>
))
) : (
<span className="text-gray-500">No VMs configured</span>
<span className="text-gray-500">No Containers configured</span>
)}
</div>
</div>
@ -150,7 +182,19 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
<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">
<button
onClick={() => onToggleExpansion(router.id, 'router_details')}
className="flex items-center gap-1 text-blue-500 hover:text-blue-700"
>
{expandedRows.has(`${router.id}-router_details`) ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)}
<span>{router.routerId}</span>
</button>
</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">
@ -163,7 +207,8 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
) : (
<ChevronRight size={16} />
)}
View Activity ({router.routerActivity.studies.length} studies)
<span>{router?.routerActivity?.studies?.[0]?.studyStatusCode || "Idle"}</span>
</button>
</td>
<td className="px-4 py-2">
@ -176,8 +221,8 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
) : (
<ChevronRight size={16} />
)}
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.vpnStatus)}`}>
{formatStatus(router.systemStatus.vpnStatus)}
<span className={`ml-2 px-2 py-1 rounded-full text-sm ${getStatusColor(router.systemStatus.routerStatus)}`}>
{formatStatus(router.systemStatus.routerStatus)}
</span>
</button>
</td>
@ -206,6 +251,13 @@ export const RouterTableRow: React.FC<RouterTableRowProps> = ({
</tr>
{/* Expandable Panels */}
{expandedRows.has(`${router.id}-router_details`) && (
<tr>
<td colSpan={8}>
{renderRouterDetailsPanel()}
</td>
</tr>
)}
{expandedRows.has(`${router.id}-activity`) && (
<tr>
<td colSpan={8}>

View File

@ -5,7 +5,7 @@ interface Config {
}
const config: Config = {
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3001/api/v1',
apiUrl: import.meta.env.VITE_API_URL,
environment: import.meta.env.VITE_NODE_ENV || 'development',
};

View File

@ -21,6 +21,9 @@ export const MOCK_ROUTERS: RouterData[] = [
routerId: 'RTR001',
facility: 'City Hospital',
routerAlias: 'Main-Router-1',
facilityAET: 'RTR_1',
openvpnIp: '10.8.0.101',
routerVmPrimaryIp: '192.168.0.101',
lastSeen: '2024-03-07T14:30:00Z',
diskStatus: 'Normal',
diskUsage: 45,
@ -64,6 +67,9 @@ export const MOCK_ROUTERS: RouterData[] = [
routerId: 'RTR002',
facility: 'Medical Center',
routerAlias: 'Emergency-Router',
facilityAET: 'RTR_2',
openvpnIp: '10.8.0.102',
routerVmPrimaryIp: '192.168.0.102',
lastSeen: '2024-03-07T14:25:00Z',
diskStatus: 'Critical',
diskUsage: 92,
@ -97,6 +103,9 @@ export const MOCK_ROUTERS: RouterData[] = [
routerId: 'RTR003',
facility: 'Imaging Center',
routerAlias: 'Radiology-Router',
facilityAET: 'RTR_3',
openvpnIp: '10.8.0.103',
routerVmPrimaryIp: '192.168.0.103',
lastSeen: '2024-03-07T14:20:00Z',
diskStatus: 'Warning',
diskUsage: 78,

View File

@ -1,7 +1,7 @@
// router-dashboard/src/services/api.service.ts
import { RouterData, FilterType, BackendRouter } from '../types';
const API_BASE_URL = 'http://localhost:3001/api/v1';
const API_BASE_URL = 'http://localhost:3000/api/v1';
// Default request options for all API calls
const DEFAULT_OPTIONS = {
@ -58,6 +58,9 @@ class ApiService {
routerId: router.routerId, // Changed from router.router_id
facility: router.facility,
routerAlias: router.routerAlias, // Changed from router.router_alias
facilityAET: router.facilityAET, // Changed from router.facility_aet
openvpnIp: router.openvpnIp, // Changed from router.openvpn_ip
routerVmPrimaryIp: router.routerVmPrimaryIp, // Changed from router.router_vm_primary_ip
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
@ -72,18 +75,28 @@ class ApiService {
patientName: study.patientName,
studyDate: study.studyDate,
modality: study.modality,
studyDescription: study.studyDescription
studyDescription: study.studyDescription,
studyStatusCode: study.studyStatusCode
}))
: []
},
systemStatus: {
vpnStatus: router.systemStatus?.vpnStatus || 'unknown',
appStatus: router.systemStatus?.appStatus || 'unknown',
vmStatus: router.systemStatus?.vmStatus || 'unknown',
routerStatus: router.systemStatus?.routerStatus || 'unknown',
vms: Array.isArray(router.systemStatus?.vms)
? router.systemStatus.vms.map((vm: any) => ({
id: vm.id,
status: vm.status
}))
: [],
containers: Array.isArray(router.systemStatus?.containers)
? router.systemStatus.containers.map((container: any) => ({
container_name: container.container_name,
status: container.status_code
}))
: []
}
};

View File

@ -7,6 +7,7 @@ export interface BackendStudy {
study_date: string;
modality: string;
study_description: string;
study_status_code: string;
}
export interface BackendVM {
@ -21,6 +22,9 @@ export interface BackendStudy {
router_id: string;
facility: string;
router_alias: string;
facility_aet: string;
openvpn_ip: string;
router_vm_primary_ip: string;
last_seen: string;
disk_status_code: string;
disk_usage: number;

View File

@ -7,6 +7,7 @@ export interface Study {
studyDate: string;
modality: string;
studyDescription: string;
studyStatusCode: string;
}
export interface VM {
@ -14,6 +15,11 @@ export interface VM {
status: string;
}
export interface Container {
container_name: string;
status_code: string;
}
export type FilterType = 'all' | 'active' | 'critical' | 'diskAlert';
export interface RouterData {
@ -22,6 +28,9 @@ export interface RouterData {
routerId: string;
facility: string;
routerAlias: string;
facilityAET: string;
openvpnIp: string;
routerVmPrimaryIp: string;
lastSeen: string;
diskStatus: string;
diskUsage: number;
@ -33,7 +42,10 @@ export interface RouterData {
systemStatus: {
vpnStatus: string;
appStatus: string;
vmStatus: string;
routerStatus: string;
vms: VM[];
containers: Container[];
};
}

View File

@ -1,6 +1,7 @@
// src/utils/statusHelpers.ts
// Define all possible status values
// Below's are for demo purpose
export type StatusType =
| 'RUNNING'
| 'STOPPED'
@ -8,11 +9,16 @@ export type StatusType =
| 'CONNECTED'
| 'DISCONNECTED'
| 'ERROR'
| 'UNKNOWN';
| 'UNKNOWN'
| 'ONLINE'
| 'OFFLINE';
export const STATUS_COLORS: Record<StatusType | string, string> = {
'RUNNING': 'bg-green-100 text-green-700',
'CONNECTED': 'bg-green-100 text-green-700',
'ONLINE': 'bg-green-100 text-green-700',
'OFFLINE': 'bg-red-100 text-red-700',
'STOPPED': 'bg-red-100 text-red-700',
'DISCONNECTED': 'bg-red-100 text-red-700',
'WARNING': 'bg-yellow-100 text-yellow-700',
@ -20,11 +26,26 @@ export const STATUS_COLORS: Record<StatusType | string, string> = {
'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
function getStatusAfterUnderscore(status: string): string {
if (status.includes('_')) {
const parts = status.split('_');
return parts[1] || ''; // Get the part after the underscore or return an empty string
}
return status; // Return the original string if no underscore is present
}
export const getStatus = (status: string): string => {
const keywords = ['CONNECTED', 'ONLINE', 'RUNNING'];
return keywords.some((keyword) => status === keyword) ? 'Up' : 'Down';
};
// Add this helper function
export const getStatusColor = (status: string): string => {
return STATUS_COLORS[status] || STATUS_COLORS['UNKNOWN'];
return STATUS_COLORS[getStatusAfterUnderscore(status)] || STATUS_COLORS['UNKNOWN'];
};
export const formatStatus = (status: string): string => {
return getStatus(getStatusAfterUnderscore(status));
};

View File

@ -1,267 +0,0 @@
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 ;

View File

@ -1,15 +1,13 @@
# Server Configuration
NODE_ENV=development
NODE_ENV=production
PORT=3000
CORS_ORIGIN=http://localhost:5173,http://localhost:3000
CORS_ORIGIN=http://${FRONTEND_IP}:5173,http://${SERVER_IP}:3000
# Database Configuration
DB_HOST=localhost
DB_PORT=3307
DB_USER=root
DB_PASSWORD=rootpassword
DB_HOST=${DB_HOST}
DB_PORT=3306
DB_USER=ve_router_user
DB_PASSWORD=ve_router_password
DB_NAME=ve_router_db
DB_CONNECTION_LIMIT=10

View File

@ -1,22 +1,20 @@
// 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'],
corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['*'], // In production, replace with actual domains
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
user: process.env.DB_USER || 've_router_user',
password: process.env.DB_PASSWORD || 've_router_password',
database: process.env.DB_NAME || 've_router_db',
connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10', 10)
},
@ -26,13 +24,4 @@ const config = {
}
};
// 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;

View File

@ -3,6 +3,7 @@
import { Request, Response, NextFunction } from 'express';
import { DicomStudyService } from '../services/DicomStudyService';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
interface ApiError {
message: string;
@ -13,8 +14,8 @@ interface ApiError {
export class DicomStudyController {
private service: DicomStudyService;
constructor() {
this.service = new DicomStudyService();
constructor(pool:Pool) {
this.service = new DicomStudyService(pool);
}
private handleError(error: unknown, message: string): ApiError {

View File

@ -1,15 +1,21 @@
// src/controllers/RouterController.ts
import { Request, Response } from 'express';
import { RouterService } from '../services/RouterService';
import { RouterService, DicomStudyService, UtilityService } from '../services';
import { Pool } from 'mysql2/promise';
import { RouterData, VMUpdate, VMUpdateRequest } from '../types';
import logger from '../utils/logger';
export class RouterController {
private service: RouterService;
private dicomStudyService: DicomStudyService;
private utilityService: UtilityService
constructor(pool: Pool) {
this.service = new RouterService(pool);
this.dicomStudyService = new DicomStudyService(pool);
this.utilityService = new UtilityService();
}
// src/controllers/RouterController.ts
@ -52,7 +58,6 @@ updateRouterVMs = async (req: Request, res: Response) => {
}
};
getAllRouters = async (req: Request, res: Response) => {
try {
const routers = await this.service.getAllRouters();
@ -67,11 +72,9 @@ getAllRouters = async (req: Request, res: Response) => {
}
};
getRouterById = async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const id = parseInt(req.params.routerId);
const router = await this.service.getRouterById(id);
if (!router) {
@ -86,9 +89,54 @@ getAllRouters = async (req: Request, res: Response) => {
createRouter = async (req: Request, res: Response) => {
try {
const router = await this.service.createRouter(req.body);
res.status(201).json(router);
const routerMetrics = req.body;
let routerId: number;
logger.info(`Initiating to create or update router: ${routerMetrics.routerId}`);
// Check for existing router
const existingRouter = await this.service.getRouterByRouterId(routerMetrics.routerId);
if (existingRouter) {
// Update existing router
const updatedRouter = await this.service.updateRouter(existingRouter.id, routerMetrics);
if (!updatedRouter) {
return res.status(404).json({ error: 'Failed to update existing router' });
}
routerId = existingRouter.id;
} else {
// Create a new router
routerMetrics.diskUsage = ((routerMetrics.totalDisk - routerMetrics.freeDisk) / routerMetrics.totalDisk) * 100;
// Get disk status
routerMetrics.diskStatus = this.utilityService.getDiskStatus(routerMetrics.diskUsage);
const router = await this.service.createRouter(routerMetrics);
if (!router) {
return res.status(404).json({ error: 'Failed to create router' });
}
routerId = router.id;
}
// Process studies after router processing
const studies = routerMetrics.routerActivity?.studies || [];
if (Array.isArray(studies) && studies.length > 0) {
logger.info(`Processing study for router: ${routerMetrics.routerId}`);
await this.dicomStudyService.processStudies(routerId, studies);
logger.info(`Successfully processed study for router: ${routerMetrics.routerId}`);
} else {
logger.info(`No study to process for router: ${routerMetrics.routerId}`);
}
// Process containers after study processing
const containers = routerMetrics.systemStatus?.containers || [];
if (Array.isArray(containers) && containers.length > 0) {
logger.info(`Processing containers for router: ${routerMetrics.routerId}`);
const result = await this.service.processContainers(routerId, containers);
logger.info(`Successfully processed ${result.affectedRows} containers for router: ${routerId}`
);
}
res.status(201).json({message: 'Router created successfully'});
} catch (error) {
logger.error('Error creating router:', error); // Log error for debugging
res.status(500).json({ error: 'Failed to create router' });
}
};

View File

@ -3,8 +3,12 @@
import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DBDicomStudy, DicomStudySearchParams } from '../types/dicom';
import pool from '../config/db';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
import { RowDataPacket, ResultSetHeader } from 'mysql2';
export class DicomStudyRepository {
constructor(private pool: Pool) {} // Modified constructor
private async getRouterStringId(numericId: number): Promise<string> {
try {
const [result] = await pool.query(
@ -40,10 +44,9 @@ export class DicomStudyRepository {
}
private async mapDBStudyToDicomStudy(dbStudy: DBDicomStudy): Promise<DicomStudy> {
const routerStringId = await this.getRouterStringId(dbStudy.router_id);
return {
id: dbStudy.id,
router_id: routerStringId,
router_id: dbStudy.router_id.toString(),
study_instance_uid: dbStudy.study_instance_uid,
patient_id: dbStudy.patient_id,
patient_name: dbStudy.patient_name,
@ -63,18 +66,16 @@ export class DicomStudyRepository {
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
study_status_code, association_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
[
numericRouterId,
studyData.router_id,
studyData.study_instance_uid,
studyData.patient_id,
studyData.patient_name,
@ -246,4 +247,26 @@ export class DicomStudyRepository {
throw new Error('Failed to search DICOM studies');
}
}
async findByStudyInstanceUid(studyInstanceUid: string): Promise<DicomStudy | null> {
try {
// Use RowDataPacket[] to align with mysql2/promise type expectations
const [rows] = await pool.query<DBDicomStudy[] & RowDataPacket[]>(`
SELECT * FROM dicom_study_overview WHERE study_instance_uid = ?`,
[studyInstanceUid]
);
// Check if rows is empty
if (rows.length === 0) {
return null;
}
// Map the first result to your DicomStudy object
return await this.mapDBStudyToDicomStudy(rows[0]);
} catch (error) {
logger.error('Error fetching DICOM study by Study Instance UID:', error);
throw new Error('Failed to fetch DICOM study');
}
}
}

View File

@ -1,5 +1,5 @@
// src/repositories/RouterRepository.ts
import { RouterData, Study, VM,VMUpdate } from '../types';
import { RouterData, Study, VM, VMUpdate, Container } from '../types';
import pool from '../config/db';
import { RowDataPacket, ResultSetHeader } from 'mysql2';
import logger from '../utils/logger';
@ -81,10 +81,12 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
patient_name as patientName,
DATE_FORMAT(study_date, '%Y-%m-%d') as studyDate,
modality,
study_description as studyDescription
study_description as studyDescription,
CONCAT(UPPER(SUBSTRING(study_status_code, 1, 1)), LOWER(SUBSTRING(study_status_code, 2))) as studyStatusCode
FROM dicom_study_overview
WHERE router_id = ?
ORDER BY study_date DESC`,
ORDER BY updated_at DESC
LIMIT 1`,
[routerId]
);
return rows as Study[];
@ -126,10 +128,52 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
}
}
private async getRouterContainers(routerId: number): Promise<Container[]> {
try {
// Then use this to query vm_details
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT
container_name,
status_code
FROM container_status
WHERE router_id = ?`,
[routerId]
);
logger.info(`Containers for router ${routerId}:`, rows);
return rows as Container[];
} catch (error) {
logger.error(`Error fetching Containers for router ${routerId}:`, error);
return [];
}
}
private async calculateVMStatus (lastSeen: string | number | Date): Promise<string> {
const currentTime = new Date();
const lastSeenTime = new Date(lastSeen);
// Use getTime() to get timestamps in milliseconds
const diffInMinutes = (currentTime.getTime() - lastSeenTime.getTime()) / (1000 * 60);
return diffInMinutes > 1 ? 'NET_OFFLINE' : 'NET_ONLINE';
};
private async calculateSystemStatus (vpnStatus: string, appStatus: string, vmStatus: string): Promise<string> {
return vpnStatus === 'VPN_CONNECTED' && appStatus === 'CONTAINER_RUNNING' && vmStatus === 'NET_ONLINE' ? 'CONNECTED' : 'DISCONNECTED';
};
private async transformDatabaseRouter(dbRouter: any, index: number): Promise<RouterData> {
try {
const studies = await this.getRouterStudies(dbRouter.id);
const vms = await this.getRouterVMs(dbRouter.id);
//const vms = await this.getRouterVMs(dbRouter.id);
const vms:VM[] = [];
const containers = await this.getRouterContainers(dbRouter.id);
const lastSeen = new Date(dbRouter.last_seen).toISOString();
const vpnStatus = dbRouter.vpn_status_code.toString();
const appStatus = dbRouter.app_status_code.toString();
const vmStatus = await this.calculateVMStatus(lastSeen);
const routerStatus = await this.calculateSystemStatus(vpnStatus, appStatus, vmStatus);
return {
id: dbRouter.id,
@ -137,7 +181,10 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
routerId: dbRouter.router_id,
facility: dbRouter.facility,
routerAlias: dbRouter.router_alias,
lastSeen: new Date(dbRouter.last_seen).toISOString(),
facilityAET: dbRouter.facility_aet,
openvpnIp: dbRouter.openvpn_ip,
routerVmPrimaryIp: dbRouter.router_vm_primary_ip,
lastSeen: lastSeen,
diskStatus: dbRouter.disk_status_code,
diskUsage: parseFloat(dbRouter.disk_usage),
freeDisk: parseInt(dbRouter.free_disk),
@ -146,9 +193,12 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
studies
},
systemStatus: {
vpnStatus: dbRouter.vpn_status_code,
appStatus: dbRouter.disk_status_code,
vms
vpnStatus: vpnStatus,
appStatus: appStatus,
vmStatus: vmStatus,
routerStatus: routerStatus,
vms,
containers
}
};
} catch (error) {
@ -184,6 +234,16 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
return this.transformDatabaseRouter(rows[0], 0);
}
async findByRouterId(routerId: string): Promise<RouterData | null> {
const [rows] = await pool.query<RowDataPacket[]>(
'SELECT * FROM routers WHERE router_id = ?',
[routerId]
);
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',
@ -200,16 +260,20 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
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_id, facility, router_alias, facility_aet, openvpn_ip, router_vm_primary_ip,
last_seen, vpn_status_code, disk_status_code, app_status_code,
license_status, free_disk, total_disk, disk_usage
) VALUES (?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, 'inactive', ?, ?, ?)`,
[
router.routerId,
router.facility,
router.routerAlias,
'unknown',
router.facilityAET,
router.openvpnIp,
router.routerVmPrimaryIp,
router.systemStatus?.vpnStatus || 'unknown',
router.diskStatus || 'unknown',
router.systemStatus?.appStatus || 'unknown',
router.freeDisk,
router.totalDisk,
router.diskUsage || 0
@ -225,8 +289,14 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<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.facilityAET) updates.facility_aet = router.facilityAET;
if (router.openvpnIp) updates.openvpn_ip = router.openvpnIp;
if (router.routerVmPrimaryIp) updates.router_vm_primary_ip = router.routerVmPrimaryIp;
if (router.diskStatus) updates.disk_status_code = router.diskStatus;
if (router.systemStatus?.vpnStatus) updates.vpn_status_code = router.systemStatus?.vpnStatus;
if (router.systemStatus?.appStatus) updates.app_status_code = router.systemStatus?.appStatus;
if (router.freeDisk !== undefined || router.totalDisk !== undefined) {
const existingRouter = await this.findById(id);
if (!existingRouter) return null;
@ -242,7 +312,7 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
.join(', ');
await pool.query(
`UPDATE routers SET ${setClauses}, updated_at = NOW() WHERE id = ?`,
`UPDATE routers SET ${setClauses}, last_seen = NOW(), updated_at = NOW() WHERE id = ?`,
[...Object.values(updates), id]
);
}
@ -257,4 +327,37 @@ async updateVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
);
return result.affectedRows > 0;
}
async upsertContainerStatus(routerId: string, containers: Container[]): Promise<ResultSetHeader> {
const values = containers.map((container) => [
routerId,
container.container_name,
container.status_code,
new Date(), // created_at
new Date(), // updated_at
]);
const query = `
INSERT INTO container_status (router_id, container_name, status_code, created_at, updated_at)
VALUES ${values.map(() => "(?, ?, ?, ?, ?)").join(", ")}
ON DUPLICATE KEY UPDATE
status_code = VALUES(status_code),
updated_at = VALUES(updated_at);
`;
// Flatten the values array manually (compatible with older TypeScript versions)
const flattenedValues = values.reduce((acc, val) => acc.concat(val), []);
try {
const [result] = await pool.query<ResultSetHeader>(query, flattenedValues);
return result;
} catch (error) {
logger.error("Error inserting or updating container status:");
logger.error(`Query: ${query}`);
logger.error(`Flattened Values: ${JSON.stringify(flattenedValues)}`);
logger.error("Error Details:", error);
throw new Error("Error inserting or updating container status");
}
}
}

View File

@ -3,9 +3,10 @@
import express from 'express';
import { DicomStudyController } from '../controllers/DicomStudyController';
import logger from '../utils/logger';
import pool from '../config/db'; // If using default export
const router = express.Router();
const dicomStudyController = new DicomStudyController();
const dicomStudyController = new DicomStudyController(pool);
// Debug logging
logger.info('Initializing DICOM routes');

View File

@ -2,12 +2,13 @@ import { DicomStudy, CreateDicomStudyDTO, UpdateDicomStudyDTO, DicomStudySearchP
import { DicomStudyRepository } from '../repositories/DicomStudyRepository';
import pool from '../config/db';
import logger from '../utils/logger';
import { Pool } from 'mysql2/promise';
export class DicomStudyService {
private repository: DicomStudyRepository;
constructor() {
this.repository = new DicomStudyRepository();
constructor(pool: Pool) {
this.repository = new DicomStudyRepository(pool);
}
private async isValidStatusCode(statusCode: string): Promise<boolean> {
@ -39,21 +40,18 @@ export class DicomStudyService {
];
for (const field of requiredFields) {
if (!studyData[field as keyof CreateDicomStudyDTO]) {
// Check for undefined or null only (allow empty strings)
if (studyData[field as keyof CreateDicomStudyDTO] == null) {
throw new Error(`Missing required field: ${field}`);
}
}
// Commented below, currently this field is inserted with active/idle
// 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');
}
//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`);
//}
logger.info('Creating new study', { studyData });
return await this.repository.create(studyData);
@ -151,4 +149,19 @@ export class DicomStudyService {
throw new Error('Failed to search studies');
}
}
async processStudies(routerId: number, studies: DicomStudy[]): Promise<void> {
for (const study of studies) {
const existingStudy = await this.repository.findByStudyInstanceUid(study.study_instance_uid);
if (existingStudy) {
study.router_id = existingStudy.router_id;
await this.updateStudy(existingStudy.id, study);
} else {
study.router_id = routerId.toString();
logger.info(`Inserting study for router: ${routerId}`);
await this.createStudy(study);
}
}
}
}

View File

@ -1,7 +1,8 @@
// src/services/RouterService.ts
import { RouterRepository } from '../repositories/RouterRepository';
import { RouterData,VMUpdate} from '../types';
import { Container, RouterData,VMUpdate} from '../types';
import { Pool } from 'mysql2/promise';
import logger from '../utils/logger';
export class RouterService {
@ -30,6 +31,10 @@ async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
return this.repository.findById(id);
}
async getRouterByRouterId(routerId: string): Promise<RouterData | null> {
return this.repository.findByRouterId(routerId);
}
async getRoutersByFacility(facility: string): Promise<RouterData[]> {
return this.repository.findByFacility(facility);
}
@ -45,5 +50,10 @@ async updateRouterVMs(routerId: string, vms: VMUpdate[]): Promise<any> {
async deleteRouter(id: number): Promise<boolean> {
return this.repository.delete(id);
}
async processContainers(routerId: number, containers: Container[]): Promise<any> {
return this.repository.upsertContainerStatus(routerId.toString(), containers);
}
}

View File

@ -0,0 +1,17 @@
import logger from '../utils/logger';
export class UtilityService {
// Get disk status based on disk usage (handles float values like 10.25)
getDiskStatus(diskUsage: number): string {
if (diskUsage >= 90) {
return 'DISK_CRITICAL'; // Critical disk status
} else if (diskUsage >= 70) {
return 'DISK_WARNING'; // Warning disk status
} else {
return 'DISK_NORMAL'; // Normal disk status
}
}
}

View File

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

View File

@ -72,3 +72,18 @@ export interface DicomStudySearchParams {
modality?: string;
patientName?: string;
}
export interface Study {
patientId: string;
patientName: string;
siuid: string;
accessionNumber: string;
studyDate: string;
modality: string;
studyDescription: string;
seriesInstanceUid: string;
procedureCode: string;
referringPhysicianName: string;
associationId: string;
studyStatusCode: string;
}

View File

@ -5,6 +5,9 @@ export interface RouterData {
routerId: string; // maps to backend 'router_id'
facility: string; // maps to backend 'facility'
routerAlias: string; // maps to backend 'router_alias'
facilityAET: string; // maps to backend 'facility_aet'
openvpnIp: string; // maps to backend 'openvpn_ip'
routerVmPrimaryIp: string; // maps to backend 'router_vm_primary_ip'
lastSeen: string; // maps to backend 'last_seen'
diskStatus: string; // maps to backend 'disk_status_code'
diskUsage: number; // maps to backend 'disk_usage'
@ -15,19 +18,27 @@ export interface RouterData {
};
systemStatus: {
vpnStatus: string; // maps to backend 'vpn_status_code'
appStatus: string; // maps to backend 'disk_status_code'
appStatus: string; // maps to backend 'app_status_code'
vmStatus: string; // router machine status
routerStatus: string; // overall operational status of the router
vms: VM[];
containers: Container[];
};
}
export interface Study {
siuid: string;
patientId: string;
accessionNumber: string;
patientName: string;
siuid: string;
accessionNumber: string;
studyDate: string;
modality: string;
studyDescription: string;
seriesInstanceUid: string;
procedureCode: string;
referringPhysicianName: string;
associationId: string;
studyStatusCode: string;
}
export interface VM {
@ -44,3 +55,8 @@ export interface VMUpdate {
export interface VMUpdateRequest {
vms: VMUpdate[];
}
export interface Container {
container_name: string;
status_code: string;
}