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:
Ernie Butcher
2026-03-18 17:09:08 -04:00
commit 65471c2a70
54 changed files with 7304 additions and 0 deletions

26
backend/src/db/index.ts Normal file
View 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
View 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
View 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;

View 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()
});
};

View 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()
});
};

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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()
};
}
}