Initial commit: Eventify Server Health Monitor

Full-stack real-time server monitoring dashboard with:
- Express.js backend with SSH-based metrics collection
- React + TypeScript + Tailwind CSS + shadcn/ui frontend
- CPU, memory, disk, Docker container, and Nginx monitoring
- Pure SVG charts (CPU area chart + memory donut)
- Gilroy font, 10s auto-refresh polling
- Multi-page dashboard with sidebar navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 14:02:27 +05:30
commit 7dee2d8069
60 changed files with 8719 additions and 0 deletions

23
server/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "server",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts"
},
"dependencies": {
"express": "^4.21.0",
"ssh2": "^1.16.0",
"cors": "^2.8.5",
"dotenv": "^16.4.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/ssh2": "^1.15.0",
"@types/cors": "^2.8.17",
"typescript": "^5.8.0",
"tsx": "^4.19.0"
}
}

21
server/src/config.ts Normal file
View File

@@ -0,0 +1,21 @@
import { homedir } from 'os';
import { resolve } from 'path';
import 'dotenv/config';
function resolveHome(filepath: string): string {
if (filepath.startsWith('~')) {
return resolve(homedir(), filepath.slice(2));
}
return resolve(filepath);
}
export const config = {
ssh: {
host: process.env.SSH_HOST ?? 'ec2-174-129-72-160.compute-1.amazonaws.com',
user: process.env.SSH_USER ?? 'ubuntu',
keyPath: resolveHome(
process.env.SSH_KEY_PATH ?? '~/.ssh/eventify_keys_21_03_2026.pem'
),
},
port: parseInt(process.env.PORT ?? '3002', 10),
} as const;

57
server/src/index.ts Normal file
View File

@@ -0,0 +1,57 @@
import express from 'express';
import cors from 'cors';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import { existsSync } from 'fs';
import { config } from './config.js';
import { healthRouter } from './routes/health.js';
import { sshManager } from './ssh/client.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
// ── Middleware ──
app.use(
cors({
origin: ['http://localhost:5173', 'http://localhost:5174'],
credentials: true,
})
);
app.use(express.json());
// ── API Routes ──
app.use('/api', healthRouter);
// ── Static file serving for production SPA ──
const clientDistPath = resolve(__dirname, '../../client/dist');
if (existsSync(clientDistPath)) {
app.use(express.static(clientDistPath));
// SPA fallback: serve index.html for any non-API route
app.get('*', (_req, res) => {
res.sendFile(resolve(clientDistPath, 'index.html'));
});
}
// ── Start server ──
const server = app.listen(config.port, () => {
console.log(`[Server] Listening on http://localhost:${config.port}`);
console.log(`[Server] SSH target: ${config.ssh.user}@${config.ssh.host}`);
});
// ── Graceful shutdown ──
async function shutdown(signal: string) {
console.log(`\n[Server] ${signal} received, shutting down gracefully...`);
server.close(() => {
console.log('[Server] HTTP server closed');
});
await sshManager.disconnect();
process.exit(0);
}
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));

View File

@@ -0,0 +1,95 @@
import { Router } from 'express';
import { getSystemInfo } from '../services/system.js';
import { getMemoryInfo } from '../services/memory.js';
import { getDiskInfo } from '../services/disk.js';
import { getDockerInfo } from '../services/docker.js';
import { getNginxStatus } from '../services/nginx.js';
import type { OverviewResponse } from '../types/index.js';
export const healthRouter = Router();
// GET /api/overview - all metrics collected sequentially to avoid SSH channel exhaustion
healthRouter.get('/overview', async (_req, res) => {
try {
const errors: Record<string, string> = {};
const safe = async <T>(name: string, fn: () => Promise<T>): Promise<T | null> => {
try {
return await fn();
} catch (err) {
errors[name] = (err as Error).message ?? 'Unknown error';
return null;
}
};
const system = await safe('system', getSystemInfo);
const memory = await safe('memory', getMemoryInfo);
const disk = await safe('disk', getDiskInfo);
const docker = await safe('docker', getDockerInfo);
const nginx = await safe('nginx', getNginxStatus);
const response: OverviewResponse = {
system,
memory,
disk,
docker,
nginx,
errors,
timestamp: new Date().toISOString(),
};
res.json(response);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch server overview', details: (err as Error).message });
}
});
// GET /api/system
healthRouter.get('/system', async (_req, res) => {
try {
const data = await getSystemInfo();
res.json(data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch system info', details: (err as Error).message });
}
});
// GET /api/memory
healthRouter.get('/memory', async (_req, res) => {
try {
const data = await getMemoryInfo();
res.json(data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch memory info', details: (err as Error).message });
}
});
// GET /api/disk
healthRouter.get('/disk', async (_req, res) => {
try {
const data = await getDiskInfo();
res.json(data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch disk info', details: (err as Error).message });
}
});
// GET /api/docker
healthRouter.get('/docker', async (_req, res) => {
try {
const data = await getDockerInfo();
res.json(data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch docker info', details: (err as Error).message });
}
});
// GET /api/nginx
healthRouter.get('/nginx', async (_req, res) => {
try {
const data = await getNginxStatus();
res.json(data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch nginx status', details: (err as Error).message });
}
});

View File

@@ -0,0 +1,28 @@
import { sshManager } from '../ssh/client.js';
import type { DiskInfo } from '../types/index.js';
export async function getDiskInfo(): Promise<DiskInfo> {
const output = await sshManager.execCommand('df -B1 /');
// Example output:
// Filesystem 1B-blocks Used Available Use% Mounted on
// /dev/xvda1 32212254720 18345678912 13866575808 57% /
const lines = output.split('\n');
const dataLine = lines.find((l) => l.startsWith('/'));
if (!dataLine) {
return { total: 0, used: 0, free: 0, usedPercent: 0, mountPoint: '/' };
}
const parts = dataLine.trim().split(/\s+/);
// parts: [filesystem, 1B-blocks, used, available, use%, mounted-on]
const total = parseInt(parts[1], 10) || 0;
const used = parseInt(parts[2], 10) || 0;
const free = parseInt(parts[3], 10) || 0;
const usedPercent = parseFloat(parts[4]?.replace('%', '') ?? '0') || 0;
const mountPoint = parts[5] ?? '/';
return { total, used, free, usedPercent, mountPoint };
}

View File

@@ -0,0 +1,92 @@
import { sshManager } from '../ssh/client.js';
import type { DockerContainer } from '../types/index.js';
function parseByteString(str: string): number {
const cleaned = str.trim();
const match = cleaned.match(/^([\d.]+)\s*([A-Za-z]+)$/);
if (!match) return 0;
const value = parseFloat(match[1]);
const unit = match[2].toLowerCase();
const multipliers: Record<string, number> = {
b: 1,
kb: 1000,
mb: 1_000_000,
gb: 1_000_000_000,
tb: 1_000_000_000_000,
kib: 1024,
mib: 1024 ** 2,
gib: 1024 ** 3,
tib: 1024 ** 4,
};
return Math.round(value * (multipliers[unit] ?? 0));
}
export async function getDockerInfo(): Promise<DockerContainer[]> {
// Run both commands in a single SSH channel
const output = await sshManager.execCommand(
"docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}' && echo '---DOCKER_SEP---' && docker ps --format '{{.Names}}\t{{.Status}}\t{{.Image}}'"
);
const [statsOutput, psOutput] = output.split('---DOCKER_SEP---').map((s) => s.trim());
// Build lookup from docker ps output
const psMap = new Map<string, { status: string; image: string }>();
if (psOutput) {
for (const line of psOutput.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
const [name, status, image] = trimmed.split('\t');
if (name) {
psMap.set(name, { status: status ?? 'unknown', image: image ?? 'unknown' });
}
}
}
const containers: DockerContainer[] = [];
if (statsOutput) {
for (const line of statsOutput.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
const parts = trimmed.split('\t');
if (parts.length < 7) continue;
const [name, cpuPerc, memUsage, memPerc, netIO, blockIO, pids] = parts;
const memParts = memUsage.split('/').map((s) => s.trim());
const memUsageBytes = parseByteString(memParts[0] ?? '0B');
const memLimitBytes = parseByteString(memParts[1] ?? '0B');
const netParts = netIO.split('/').map((s) => s.trim());
const netInput = parseByteString(netParts[0] ?? '0B');
const netOutputBytes = parseByteString(netParts[1] ?? '0B');
const blockParts = blockIO.split('/').map((s) => s.trim());
const blockInput = parseByteString(blockParts[0] ?? '0B');
const blockOutputBytes = parseByteString(blockParts[1] ?? '0B');
const psInfo = psMap.get(name) ?? { status: 'unknown', image: 'unknown' };
containers.push({
name,
status: psInfo.status,
image: psInfo.image,
cpuPercent: parseFloat(cpuPerc.replace('%', '')) || 0,
memUsage: memUsageBytes,
memLimit: memLimitBytes,
memPercent: parseFloat(memPerc.replace('%', '')) || 0,
netInput,
netOutput: netOutputBytes,
blockInput,
blockOutput: blockOutputBytes,
pids: parseInt(pids, 10) || 0,
});
}
}
return containers;
}

View File

@@ -0,0 +1,30 @@
import { sshManager } from '../ssh/client.js';
import type { MemoryInfo } from '../types/index.js';
export async function getMemoryInfo(): Promise<MemoryInfo> {
const output = await sshManager.execCommand('free -b');
// Example output:
// total used free shared buff/cache available
// Mem: 16508469248 3456789504 8765432320 123456789 4286247424 12345678848
// Swap: 2147483648 0 2147483648
const lines = output.split('\n');
const memLine = lines.find((l) => l.startsWith('Mem:'));
if (!memLine) {
return { total: 0, used: 0, free: 0, cached: 0, available: 0, usedPercent: 0 };
}
const parts = memLine.trim().split(/\s+/);
// parts: ["Mem:", total, used, free, shared, buff/cache, available]
const total = parseInt(parts[1], 10) || 0;
const used = parseInt(parts[2], 10) || 0;
const free = parseInt(parts[3], 10) || 0;
const cached = parseInt(parts[5], 10) || 0; // buff/cache column
const available = parseInt(parts[6], 10) || 0;
const usedPercent = total > 0 ? Math.round((used / total) * 10000) / 100 : 0;
return { total, used, free, cached, available, usedPercent };
}

View File

@@ -0,0 +1,28 @@
import { sshManager } from '../ssh/client.js';
import type { NginxStatus } from '../types/index.js';
export async function getNginxStatus(): Promise<NginxStatus> {
let statusText: string;
try {
statusText = await sshManager.execCommand('systemctl is-active nginx');
} catch {
statusText = 'failed';
}
const normalized = statusText.trim().toLowerCase();
let status: NginxStatus['status'];
if (normalized === 'active') {
status = 'active';
} else if (normalized === 'inactive') {
status = 'inactive';
} else {
status = 'failed';
}
return {
status,
checkedAt: new Date().toISOString(),
};
}

View File

@@ -0,0 +1,36 @@
import { sshManager } from '../ssh/client.js';
import type { SystemInfo } from '../types/index.js';
export async function getSystemInfo(): Promise<SystemInfo> {
// Run all commands in a single SSH channel to avoid channel exhaustion
const output = await sshManager.execCommand(
"hostname && echo '---SEP---' && uptime -p && echo '---SEP---' && cat /proc/loadavg && echo '---SEP---' && top -bn1 | grep 'Cpu(s)' | awk '{print $2}' && echo '---SEP---' && cat /etc/os-release | grep PRETTY_NAME"
);
const parts = output.split('---SEP---').map((s) => s.trim());
const hostnameOut = parts[0] || 'unknown';
const uptimeOut = parts[1] || 'unknown';
const loadAvgOut = parts[2] || '0 0 0';
const cpuOut = parts[3] || '0';
const osOut = parts[4] || 'PRETTY_NAME="Unknown"';
const loadParts = loadAvgOut.split(/\s+/);
const loadAvg: [number, number, number] = [
parseFloat(loadParts[0]) || 0,
parseFloat(loadParts[1]) || 0,
parseFloat(loadParts[2]) || 0,
];
const cpuPercent = parseFloat(cpuOut.replace('%', '')) || 0;
const osMatch = osOut.match(/PRETTY_NAME="?([^"]*)"?/);
const os = osMatch ? osMatch[1] : 'Unknown';
return {
hostname: hostnameOut,
uptime: uptimeOut,
loadAvg,
cpuPercent,
os,
};
}

141
server/src/ssh/client.ts Normal file
View File

@@ -0,0 +1,141 @@
import { Client } from 'ssh2';
import { readFileSync } from 'fs';
import { config } from '../config.js';
class SSHConnectionManager {
private client: Client | null = null;
private connected = false;
private connecting = false;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
async connect(): Promise<void> {
if (this.connected || this.connecting) return;
this.connecting = true;
return new Promise<void>((resolve, reject) => {
const conn = new Client();
conn.on('ready', () => {
console.log('[SSH] Connection established');
this.client = conn;
this.connected = true;
this.connecting = false;
resolve();
});
conn.on('error', (err) => {
console.error('[SSH] Connection error:', err.message);
this.handleDisconnect();
if (this.connecting) {
this.connecting = false;
reject(err);
}
});
conn.on('close', () => {
console.warn('[SSH] Connection closed');
this.handleDisconnect();
});
conn.on('end', () => {
this.handleDisconnect();
});
let privateKey: Buffer;
try {
privateKey = readFileSync(config.ssh.keyPath);
} catch (err) {
this.connecting = false;
reject(new Error(`Failed to read SSH key at ${config.ssh.keyPath}: ${(err as Error).message}`));
return;
}
conn.connect({
host: config.ssh.host,
username: config.ssh.user,
privateKey,
readyTimeout: 10_000,
keepaliveInterval: 30_000,
keepaliveCountMax: 3,
});
});
}
private handleDisconnect(): void {
this.connected = false;
this.client = null;
if (!this.reconnectTimer) {
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
console.log('[SSH] Attempting reconnect...');
this.connect().catch((err) => {
console.error('[SSH] Reconnect failed:', err.message);
});
}, 5_000);
}
}
async ensureConnected(): Promise<void> {
if (!this.connected || !this.client) {
await this.connect();
}
}
async execCommand(cmd: string): Promise<string> {
await this.ensureConnected();
if (!this.client) {
throw new Error('SSH client is not connected');
}
return new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`Command timed out after 10s: ${cmd}`));
}, 10_000);
this.client!.exec(cmd, (err, stream) => {
if (err) {
clearTimeout(timeout);
reject(err);
return;
}
let stdout = '';
let stderr = '';
stream.on('data', (data: Buffer) => {
stdout += data.toString();
});
stream.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
stream.on('close', (code: number) => {
clearTimeout(timeout);
if (code !== 0 && !stdout) {
reject(new Error(`Command exited with code ${code}: ${stderr.trim()}`));
} else {
resolve(stdout.trim());
}
});
});
});
}
async disconnect(): Promise<void> {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.client) {
this.client.end();
this.client = null;
this.connected = false;
console.log('[SSH] Connection closed gracefully');
}
}
}
export const sshManager = new SSHConnectionManager();

56
server/src/types/index.ts Normal file
View File

@@ -0,0 +1,56 @@
// ─── Server Health Monitoring API Types ───
export interface SystemInfo {
hostname: string;
uptime: string;
loadAvg: [number, number, number];
cpuPercent: number;
os: string;
}
export interface MemoryInfo {
total: number;
used: number;
free: number;
cached: number;
available: number;
usedPercent: number;
}
export interface DiskInfo {
total: number;
used: number;
free: number;
usedPercent: number;
mountPoint: string;
}
export interface DockerContainer {
name: string;
status: string;
image: string;
cpuPercent: number;
memUsage: number;
memLimit: number;
memPercent: number;
netInput: number;
netOutput: number;
blockInput: number;
blockOutput: number;
pids: number;
}
export interface NginxStatus {
status: 'active' | 'inactive' | 'failed';
checkedAt: string;
}
export interface OverviewResponse {
system: SystemInfo | null;
memory: MemoryInfo | null;
disk: DiskInfo | null;
docker: DockerContainer[] | null;
nginx: NginxStatus | null;
errors: Record<string, string>;
timestamp: string;
}

13
server/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"]
}