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:
23
server/package.json
Normal file
23
server/package.json
Normal 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
21
server/src/config.ts
Normal 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
57
server/src/index.ts
Normal 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'));
|
||||
95
server/src/routes/health.ts
Normal file
95
server/src/routes/health.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
28
server/src/services/disk.ts
Normal file
28
server/src/services/disk.ts
Normal 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 };
|
||||
}
|
||||
92
server/src/services/docker.ts
Normal file
92
server/src/services/docker.ts
Normal 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;
|
||||
}
|
||||
30
server/src/services/memory.ts
Normal file
30
server/src/services/memory.ts
Normal 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 };
|
||||
}
|
||||
28
server/src/services/nginx.ts
Normal file
28
server/src/services/nginx.ts
Normal 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(),
|
||||
};
|
||||
}
|
||||
36
server/src/services/system.ts
Normal file
36
server/src/services/system.ts
Normal 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
141
server/src/ssh/client.ts
Normal 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
56
server/src/types/index.ts
Normal 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
13
server/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user