diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fa5d4f2 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VPS_API_KEY=your-key-here +PORT=4002 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894f99c --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Dependencies +node_modules/ + +# Next.js +.next/ +out/ + +# Production +build/ +dist/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..3c543bb --- /dev/null +++ b/next.config.js @@ -0,0 +1,9 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + experimental: { + appDir: true + } +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5368a12 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "vps-command-center", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 4002", + "build": "next build", + "start": "next start -p 4002", + "lint": "next lint" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "systeminformation": "^5.21.20", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20.8.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.51.0", + "eslint-config-next": "^15.0.0", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.5", + "typescript": "^5.2.2" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..96bb01e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/src/app/api/system/route.ts b/src/app/api/system/route.ts new file mode 100644 index 0000000..50f879a --- /dev/null +++ b/src/app/api/system/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server' +import * as si from 'systeminformation' +import { SystemMetrics } from '@/lib/types' + +export async function GET(): Promise> { + try { + const [cpu, mem, disk, network] = await Promise.all([ + si.currentLoad(), + si.mem(), + si.fsSize(), + si.networkStats() + ]) + + const uptime = si.time().uptime + + const metrics: SystemMetrics = { + cpu: cpu.currentLoad, + memory: { + used: mem.used, + total: mem.total + }, + disk: disk.length > 0 ? disk[0].use : 0, + network: { + rx: network.length > 0 ? network[0].rx_bytes : 0, + tx: network.length > 0 ? network[0].tx_bytes : 0 + }, + uptime: uptime + } + + return NextResponse.json(metrics) + } catch (error) { + console.error('Error fetching system metrics:', error) + return NextResponse.json( + { error: 'Failed to fetch system metrics' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..44a4f8c --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,20 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --primary: #3B82F6; + --secondary: #9CA3AF; + --background: #0B1120; + --foreground: #F9FAFB; + --muted: #374151; + --accent: #3B82F6; + --destructive: #EF4444; + --card: #111827; +} + +body { + font-family: 'Inter', system-ui, sans-serif; + background-color: var(--background); + color: var(--foreground); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..ce0a78e --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import './globals.css' + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'VPS Command Center', + description: 'Self-hosted VPS monitoring dashboard', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}): JSX.Element { + return ( + + + {children} + + + ) +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..50cf0be --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,123 @@ +'use client' + +import { useEffect, useState } from 'react' +import { SystemMetrics } from '@/lib/types' + +export default function Dashboard(): JSX.Element { + const [metrics, setMetrics] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchMetrics = async (): Promise => { + try { + const response = await fetch('/api/system') + if (!response.ok) { + throw new Error('Failed to fetch metrics') + } + const data = await response.json() + setMetrics(data) + setError(null) + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchMetrics() + const interval = setInterval(fetchMetrics, 3000) + return () => clearInterval(interval) + }, []) + + const formatBytes = (bytes: number): string => { + const gb = bytes / (1024 * 1024 * 1024) + return `${gb.toFixed(2)} GB` + } + + const formatUptime = (seconds: number): string => { + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + return `${days}d ${hours}h ${minutes}m` + } + + if (loading) { + return ( +
+
Loading system metrics...
+
+ ) + } + + if (error) { + return ( +
+
Error: {error}
+
+ ) + } + + return ( +
+

VPS Command Center

+ +
+ {/* CPU Usage */} +
+

CPU Usage

+
+ {metrics?.cpu.toFixed(1)}% +
+
+ + {/* Memory Usage */} +
+

Memory

+
+ {metrics ? formatBytes(metrics.memory.used) : '0 GB'} +
+
+ of {metrics ? formatBytes(metrics.memory.total) : '0 GB'} +
+
+ + {/* Disk Usage */} +
+

Disk Usage

+
+ {metrics?.disk.toFixed(1)}% +
+
+ + {/* Network RX */} +
+

Network RX

+
+ {metrics ? formatBytes(metrics.network.rx) : '0 GB'} +
+
+ + {/* Network TX */} +
+

Network TX

+
+ {metrics ? formatBytes(metrics.network.tx) : '0 GB'} +
+
+ + {/* Uptime */} +
+

Uptime

+
+ {metrics ? formatUptime(metrics.uptime) : '0d 0h 0m'} +
+
+
+ + {/* TODO: Sprint 2 - Add PM2 process management */} + {/* TODO: Sprint 2 - Add deployed projects section */} + {/* TODO: Sprint 2 - Add web terminal */} +
+ ) +} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..48bf0e6 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,13 @@ +export interface SystemMetrics { + cpu: number; + memory: { + used: number; + total: number; + }; + disk: number; + network: { + rx: number; + tx: number; + }; + uptime: number; +} \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..384cd23 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,28 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + primary: '#3B82F6', + secondary: '#9CA3AF', + background: '#0B1120', + foreground: '#F9FAFB', + muted: '#374151', + accent: '#3B82F6', + destructive: '#EF4444', + card: '#111827' + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'] + } + }, + }, + plugins: [], +} +export default config \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b1814a0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "es6"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file