feat: initial project scaffold
- React 19 + Vite + TailwindCSS frontend - Express + TypeScript backend API - PostgreSQL schema and migrations - Docker Compose orchestration - Drone CI/CD pipeline - Pages: Dashboard, Servers, Containers, Services, Logs, Metrics, Settings
This commit is contained in:
26
backend/src/db/index.ts
Normal file
26
backend/src/db/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Pool } from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
export const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'server_manager',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
pool.on('connect', () => {
|
||||
console.log('🐘 Connected to PostgreSQL database');
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('❌ PostgreSQL pool error:', err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
export default pool;
|
||||
98
backend/src/db/migrate.ts
Normal file
98
backend/src/db/migrate.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import pool from './index';
|
||||
|
||||
export const runMigrations = async () => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Create servers table
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
hostname VARCHAR(255) NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
port INTEGER DEFAULT 22,
|
||||
username VARCHAR(100),
|
||||
ssh_key_path TEXT,
|
||||
status VARCHAR(50) DEFAULT 'unknown',
|
||||
tags JSONB DEFAULT '[]',
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Create containers table
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS containers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
server_id UUID REFERENCES servers(id) ON DELETE CASCADE,
|
||||
container_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
image VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Create services table
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
server_id UUID REFERENCES servers(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50),
|
||||
port INTEGER,
|
||||
config JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Create metrics table
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
server_id UUID REFERENCES servers(id) ON DELETE CASCADE,
|
||||
cpu_usage DECIMAL(5,2),
|
||||
memory_usage DECIMAL(5,2),
|
||||
disk_usage DECIMAL(5,2),
|
||||
network_rx BIGINT,
|
||||
network_tx BIGINT,
|
||||
timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Create index on metrics for faster queries
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_metrics_server_timestamp
|
||||
ON metrics(server_id, timestamp DESC);
|
||||
`);
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('✅ Database migrations completed successfully');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('❌ Migration error:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
// Run migrations if this file is executed directly
|
||||
if (require.main === module) {
|
||||
runMigrations()
|
||||
.then(() => {
|
||||
console.log('Migration complete');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Migration failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
46
backend/src/index.ts
Normal file
46
backend/src/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import express, { Express, Request, Response } from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
import { errorHandler } from './middleware/errorHandler';
|
||||
import { notFound } from './middleware/notFound';
|
||||
import apiRoutes from './routes';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app: Express = express();
|
||||
const port = process.env.PORT || 3001;
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||
credentials: true
|
||||
}));
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime()
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes
|
||||
app.use('/api', apiRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`⚡️ Server is running on port ${port}`);
|
||||
console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
18
backend/src/middleware/errorHandler.ts
Normal file
18
backend/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
console.error('Error:', err);
|
||||
|
||||
const statusCode = res.statusCode !== 200 ? res.statusCode : 500;
|
||||
|
||||
res.status(statusCode).json({
|
||||
error: err.message,
|
||||
stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
10
backend/src/middleware/notFound.ts
Normal file
10
backend/src/middleware/notFound.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const notFound = (req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(404).json({
|
||||
error: 'Not Found',
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
55
backend/src/routes/containers.ts
Normal file
55
backend/src/routes/containers.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/containers - List all containers
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement container listing via Docker API
|
||||
res.json({ containers: [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch containers' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/containers/:id - Get container details
|
||||
router.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement container details
|
||||
res.json({ id: req.params.id });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch container' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/containers/:id/start - Start container
|
||||
router.post('/:id/start', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement container start
|
||||
res.json({ status: 'started' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to start container' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/containers/:id/stop - Stop container
|
||||
router.post('/:id/stop', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement container stop
|
||||
res.json({ status: 'stopped' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to stop container' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/containers/:id/restart - Restart container
|
||||
router.post('/:id/restart', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement container restart
|
||||
res.json({ status: 'restarted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to restart container' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
30
backend/src/routes/index.ts
Normal file
30
backend/src/routes/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from 'express';
|
||||
import serversRouter from './servers';
|
||||
import containersRouter from './containers';
|
||||
import servicesRouter from './services';
|
||||
import logsRouter from './logs';
|
||||
import metricsRouter from './metrics';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Server Manager API',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
servers: '/api/servers',
|
||||
containers: '/api/containers',
|
||||
services: '/api/services',
|
||||
logs: '/api/logs',
|
||||
metrics: '/api/metrics'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.use('/servers', serversRouter);
|
||||
router.use('/containers', containersRouter);
|
||||
router.use('/services', servicesRouter);
|
||||
router.use('/logs', logsRouter);
|
||||
router.use('/metrics', metricsRouter);
|
||||
|
||||
export default router;
|
||||
23
backend/src/routes/logs.ts
Normal file
23
backend/src/routes/logs.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/logs/:type/:id - Get logs for a specific resource
|
||||
router.get('/:type/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { type, id } = req.params;
|
||||
const { lines = 100, follow = false } = req.query;
|
||||
|
||||
// TODO: Implement log streaming (container logs, service logs, system logs)
|
||||
res.json({
|
||||
type,
|
||||
id,
|
||||
logs: [],
|
||||
lines: parseInt(lines as string)
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch logs' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
34
backend/src/routes/metrics.ts
Normal file
34
backend/src/routes/metrics.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/metrics - Get system metrics
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement system metrics (CPU, memory, disk, network)
|
||||
res.json({
|
||||
cpu: { usage: 0, cores: 0 },
|
||||
memory: { used: 0, total: 0, percentage: 0 },
|
||||
disk: { used: 0, total: 0, percentage: 0 },
|
||||
network: { rx: 0, tx: 0 },
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch metrics' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/metrics/:serverId - Get metrics for specific server
|
||||
router.get('/:serverId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement per-server metrics
|
||||
res.json({
|
||||
serverId: req.params.serverId,
|
||||
metrics: {}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch server metrics' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
70
backend/src/routes/servers.ts
Normal file
70
backend/src/routes/servers.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { ServerService } from '../services/serverService';
|
||||
|
||||
const router = Router();
|
||||
const serverService = new ServerService();
|
||||
|
||||
// GET /api/servers - List all servers
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const servers = await serverService.listServers();
|
||||
res.json(servers);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch servers' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/servers/:id - Get server details
|
||||
router.get('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const server = await serverService.getServer(req.params.id);
|
||||
if (!server) {
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
}
|
||||
res.json(server);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch server' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/servers - Add new server
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const server = await serverService.createServer(req.body);
|
||||
res.status(201).json(server);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: 'Failed to create server' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/servers/:id - Update server
|
||||
router.put('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const server = await serverService.updateServer(req.params.id, req.body);
|
||||
res.json(server);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: 'Failed to update server' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/servers/:id - Remove server
|
||||
router.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
await serverService.deleteServer(req.params.id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete server' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/servers/:id/status - Get server status
|
||||
router.get('/:id/status', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const status = await serverService.getServerStatus(req.params.id);
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch server status' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
55
backend/src/routes/services.ts
Normal file
55
backend/src/routes/services.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/services - List all services
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement service listing (systemd, docker compose, etc.)
|
||||
res.json({ services: [] });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch services' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/services/:name - Get service status
|
||||
router.get('/:name', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement service status check
|
||||
res.json({ name: req.params.name, status: 'unknown' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch service status' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/services/:name/start - Start service
|
||||
router.post('/:name/start', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement service start
|
||||
res.json({ status: 'started' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to start service' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/services/:name/stop - Stop service
|
||||
router.post('/:name/stop', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement service stop
|
||||
res.json({ status: 'stopped' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to stop service' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/services/:name/restart - Restart service
|
||||
router.post('/:name/restart', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// TODO: Implement service restart
|
||||
res.json({ status: 'restarted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to restart service' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
103
backend/src/services/serverService.ts
Normal file
103
backend/src/services/serverService.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import pool from '../db';
|
||||
|
||||
export interface Server {
|
||||
id: string;
|
||||
name: string;
|
||||
hostname: string;
|
||||
ip_address: string;
|
||||
port: number;
|
||||
username?: string;
|
||||
ssh_key_path?: string;
|
||||
status: string;
|
||||
tags: string[];
|
||||
metadata: Record<string, any>;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export class ServerService {
|
||||
async listServers(): Promise<Server[]> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM servers ORDER BY created_at DESC'
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async getServer(id: string): Promise<Server | null> {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM servers WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async createServer(data: Partial<Server>): Promise<Server> {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO servers (name, hostname, ip_address, port, username, ssh_key_path, tags, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *`,
|
||||
[
|
||||
data.name,
|
||||
data.hostname,
|
||||
data.ip_address,
|
||||
data.port || 22,
|
||||
data.username,
|
||||
data.ssh_key_path,
|
||||
JSON.stringify(data.tags || []),
|
||||
JSON.stringify(data.metadata || {})
|
||||
]
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async updateServer(id: string, data: Partial<Server>): Promise<Server> {
|
||||
const result = await pool.query(
|
||||
`UPDATE servers
|
||||
SET name = COALESCE($1, name),
|
||||
hostname = COALESCE($2, hostname),
|
||||
ip_address = COALESCE($3, ip_address),
|
||||
port = COALESCE($4, port),
|
||||
username = COALESCE($5, username),
|
||||
ssh_key_path = COALESCE($6, ssh_key_path),
|
||||
status = COALESCE($7, status),
|
||||
tags = COALESCE($8, tags),
|
||||
metadata = COALESCE($9, metadata),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $10
|
||||
RETURNING *`,
|
||||
[
|
||||
data.name,
|
||||
data.hostname,
|
||||
data.ip_address,
|
||||
data.port,
|
||||
data.username,
|
||||
data.ssh_key_path,
|
||||
data.status,
|
||||
data.tags ? JSON.stringify(data.tags) : null,
|
||||
data.metadata ? JSON.stringify(data.metadata) : null,
|
||||
id
|
||||
]
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async deleteServer(id: string): Promise<void> {
|
||||
await pool.query('DELETE FROM servers WHERE id = $1', [id]);
|
||||
}
|
||||
|
||||
async getServerStatus(id: string): Promise<any> {
|
||||
const server = await this.getServer(id);
|
||||
if (!server) {
|
||||
throw new Error('Server not found');
|
||||
}
|
||||
|
||||
// TODO: Implement actual server status check (SSH, ping, etc.)
|
||||
return {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
status: 'online',
|
||||
uptime: 0,
|
||||
lastCheck: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user