diff --git a/console/frontend/Dockerfile b/console/frontend/Dockerfile
new file mode 100644
index 0000000..2d7f71f
--- /dev/null
+++ b/console/frontend/Dockerfile
@@ -0,0 +1,20 @@
+# Build stage
+FROM node:18-alpine as builder
+
+WORKDIR /app
+
+COPY package.json ./
+RUN npm install
+
+COPY . .
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/console/frontend/index.html b/console/frontend/index.html
new file mode 100644
index 0000000..9e817c5
--- /dev/null
+++ b/console/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Console - Microservices Dashboard
+
+
+
+
+
+
\ No newline at end of file
diff --git a/console/frontend/nginx.conf b/console/frontend/nginx.conf
new file mode 100644
index 0000000..9db5c76
--- /dev/null
+++ b/console/frontend/nginx.conf
@@ -0,0 +1,22 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /api {
+ proxy_pass http://console-backend:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
\ No newline at end of file
diff --git a/console/frontend/package.json b/console/frontend/package.json
new file mode 100644
index 0000000..8085dec
--- /dev/null
+++ b/console/frontend/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "console-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.11.3",
+ "@emotion/styled": "^11.11.0",
+ "@mui/material": "^5.15.2",
+ "@mui/icons-material": "^5.15.2",
+ "axios": "^1.6.3",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.21.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
+ "@typescript-eslint/parser": "^6.14.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "eslint": "^8.55.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.8"
+ }
+}
\ No newline at end of file
diff --git a/console/frontend/src/App.tsx b/console/frontend/src/App.tsx
new file mode 100644
index 0000000..7a5285e
--- /dev/null
+++ b/console/frontend/src/App.tsx
@@ -0,0 +1,19 @@
+import { Routes, Route } from 'react-router-dom'
+import Layout from './components/Layout'
+import Dashboard from './pages/Dashboard'
+import Services from './pages/Services'
+import Users from './pages/Users'
+
+function App() {
+ return (
+
+ }>
+ } />
+ } />
+ } />
+
+
+ )
+}
+
+export default App
\ No newline at end of file
diff --git a/console/frontend/src/components/Layout.tsx b/console/frontend/src/components/Layout.tsx
new file mode 100644
index 0000000..5c93367
--- /dev/null
+++ b/console/frontend/src/components/Layout.tsx
@@ -0,0 +1,102 @@
+import { useState } from 'react'
+import { Outlet, Link as RouterLink } from 'react-router-dom'
+import {
+ AppBar,
+ Box,
+ Drawer,
+ IconButton,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Toolbar,
+ Typography,
+} from '@mui/material'
+import {
+ Menu as MenuIcon,
+ Dashboard as DashboardIcon,
+ Cloud as CloudIcon,
+ People as PeopleIcon,
+} from '@mui/icons-material'
+
+const drawerWidth = 240
+
+const menuItems = [
+ { text: 'Dashboard', icon: , path: '/' },
+ { text: 'Services', icon: , path: '/services' },
+ { text: 'Users', icon: , path: '/users' },
+]
+
+function Layout() {
+ const [open, setOpen] = useState(true)
+
+ const handleDrawerToggle = () => {
+ setOpen(!open)
+ }
+
+ return (
+
+ theme.zIndex.drawer + 1 }}
+ >
+
+
+
+
+
+ Microservices Console
+
+
+
+
+
+
+
+ {menuItems.map((item) => (
+
+
+ {item.icon}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Layout
\ No newline at end of file
diff --git a/console/frontend/src/main.tsx b/console/frontend/src/main.tsx
new file mode 100644
index 0000000..e59a55f
--- /dev/null
+++ b/console/frontend/src/main.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import { ThemeProvider, createTheme } from '@mui/material/styles'
+import CssBaseline from '@mui/material/CssBaseline'
+import App from './App'
+
+const theme = createTheme({
+ palette: {
+ mode: 'light',
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ },
+})
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+
+ ,
+)
\ No newline at end of file
diff --git a/console/frontend/src/pages/Dashboard.tsx b/console/frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..86eb4e8
--- /dev/null
+++ b/console/frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,153 @@
+import { useEffect, useState } from 'react'
+import {
+ Grid,
+ Paper,
+ Typography,
+ Box,
+ Card,
+ CardContent,
+ Chip,
+} from '@mui/material'
+import {
+ CheckCircle as CheckCircleIcon,
+ Error as ErrorIcon
+} from '@mui/icons-material'
+import axios from 'axios'
+
+interface ServiceStatus {
+ name: string
+ status: 'healthy' | 'unhealthy'
+ endpoint: string
+ lastChecked: string
+}
+
+function Dashboard() {
+ const [services, setServices] = useState([])
+ const [stats, setStats] = useState({
+ totalServices: 0,
+ healthyServices: 0,
+ unhealthyServices: 0,
+ })
+
+ useEffect(() => {
+ checkServices()
+ const interval = setInterval(checkServices, 10000)
+ return () => clearInterval(interval)
+ }, [])
+
+ const checkServices = async () => {
+ const serviceChecks = [
+ { name: 'Console Backend', endpoint: '/api/health' },
+ { name: 'Users Service', endpoint: '/api/users/health' },
+ ]
+
+ const results = await Promise.all(
+ serviceChecks.map(async (service) => {
+ try {
+ await axios.get(service.endpoint)
+ return {
+ ...service,
+ status: 'healthy' as const,
+ lastChecked: new Date().toLocaleTimeString(),
+ }
+ } catch {
+ return {
+ ...service,
+ status: 'unhealthy' as const,
+ lastChecked: new Date().toLocaleTimeString(),
+ }
+ }
+ })
+ )
+
+ setServices(results)
+
+ const healthy = results.filter(s => s.status === 'healthy').length
+ setStats({
+ totalServices: results.length,
+ healthyServices: healthy,
+ unhealthyServices: results.length - healthy,
+ })
+ }
+
+ return (
+
+
+ Dashboard
+
+
+
+
+
+
+
+ Total Services
+
+
+ {stats.totalServices}
+
+
+
+
+
+
+
+
+ Healthy Services
+
+
+ {stats.healthyServices}
+
+
+
+
+
+
+
+
+ Unhealthy Services
+
+
+ {stats.unhealthyServices}
+
+
+
+
+
+
+
+
+ Service Status
+
+
+ {services.map((service) => (
+
+
+
+
+
+ {service.name}
+
+ {service.endpoint}
+
+
+ Last checked: {service.lastChecked}
+
+
+ : }
+ />
+
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export default Dashboard
\ No newline at end of file
diff --git a/console/frontend/src/pages/Services.tsx b/console/frontend/src/pages/Services.tsx
new file mode 100644
index 0000000..f492f7d
--- /dev/null
+++ b/console/frontend/src/pages/Services.tsx
@@ -0,0 +1,98 @@
+import {
+ Box,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Chip,
+} from '@mui/material'
+
+const servicesData = [
+ {
+ id: 1,
+ name: 'Console',
+ type: 'API Gateway',
+ port: 8011,
+ status: 'Running',
+ description: 'Central orchestrator and API gateway',
+ },
+ {
+ id: 2,
+ name: 'Users',
+ type: 'Microservice',
+ port: 8001,
+ status: 'Running',
+ description: 'User management service',
+ },
+ {
+ id: 3,
+ name: 'MongoDB',
+ type: 'Database',
+ port: 27017,
+ status: 'Running',
+ description: 'Document database for persistence',
+ },
+ {
+ id: 4,
+ name: 'Redis',
+ type: 'Cache',
+ port: 6379,
+ status: 'Running',
+ description: 'In-memory cache and pub/sub',
+ },
+]
+
+function Services() {
+ return (
+
+
+ Services
+
+
+
+
+
+
+ Service Name
+ Type
+ Port
+ Status
+ Description
+
+
+
+ {servicesData.map((service) => (
+
+
+ {service.name}
+
+
+
+
+ {service.port}
+
+
+
+ {service.description}
+
+ ))}
+
+
+
+
+ )
+}
+
+export default Services
\ No newline at end of file
diff --git a/console/frontend/src/pages/Users.tsx b/console/frontend/src/pages/Users.tsx
new file mode 100644
index 0000000..07c4e4e
--- /dev/null
+++ b/console/frontend/src/pages/Users.tsx
@@ -0,0 +1,208 @@
+import { useState, useEffect } from 'react'
+import {
+ Box,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Button,
+ IconButton,
+ TextField,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Stack,
+} from '@mui/material'
+import {
+ Add as AddIcon,
+ Edit as EditIcon,
+ Delete as DeleteIcon,
+} from '@mui/icons-material'
+import axios from 'axios'
+
+interface User {
+ _id: string
+ username: string
+ email: string
+ full_name?: string
+ created_at: string
+}
+
+function Users() {
+ const [users, setUsers] = useState([])
+ const [openDialog, setOpenDialog] = useState(false)
+ const [editingUser, setEditingUser] = useState(null)
+ const [formData, setFormData] = useState({
+ username: '',
+ email: '',
+ full_name: '',
+ })
+
+ useEffect(() => {
+ fetchUsers()
+ }, [])
+
+ const fetchUsers = async () => {
+ try {
+ const response = await axios.get('/api/users/')
+ setUsers(response.data)
+ } catch (error) {
+ console.error('Failed to fetch users:', error)
+ }
+ }
+
+ const handleOpenDialog = (user?: User) => {
+ if (user) {
+ setEditingUser(user)
+ setFormData({
+ username: user.username,
+ email: user.email,
+ full_name: user.full_name || '',
+ })
+ } else {
+ setEditingUser(null)
+ setFormData({
+ username: '',
+ email: '',
+ full_name: '',
+ })
+ }
+ setOpenDialog(true)
+ }
+
+ const handleCloseDialog = () => {
+ setOpenDialog(false)
+ setEditingUser(null)
+ setFormData({
+ username: '',
+ email: '',
+ full_name: '',
+ })
+ }
+
+ const handleSubmit = async () => {
+ try {
+ if (editingUser) {
+ await axios.put(`/api/users/${editingUser._id}`, formData)
+ } else {
+ await axios.post('/api/users/', formData)
+ }
+ fetchUsers()
+ handleCloseDialog()
+ } catch (error) {
+ console.error('Failed to save user:', error)
+ }
+ }
+
+ const handleDelete = async (id: string) => {
+ if (confirm('Are you sure you want to delete this user?')) {
+ try {
+ await axios.delete(`/api/users/${id}`)
+ fetchUsers()
+ } catch (error) {
+ console.error('Failed to delete user:', error)
+ }
+ }
+ }
+
+ return (
+
+
+
+ Users
+
+ }
+ onClick={() => handleOpenDialog()}
+ >
+ Add User
+
+
+
+
+
+
+
+ Username
+ Email
+ Full Name
+ Created At
+ Actions
+
+
+
+ {users.map((user) => (
+
+ {user.username}
+ {user.email}
+ {user.full_name || '-'}
+
+ {new Date(user.created_at).toLocaleDateString()}
+
+
+ handleOpenDialog(user)}
+ >
+
+
+ handleDelete(user._id)}
+ >
+
+
+
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+export default Users
\ No newline at end of file
diff --git a/console/frontend/tsconfig.json b/console/frontend/tsconfig.json
new file mode 100644
index 0000000..7a7611e
--- /dev/null
+++ b/console/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
\ No newline at end of file
diff --git a/console/frontend/tsconfig.node.json b/console/frontend/tsconfig.node.json
new file mode 100644
index 0000000..099658c
--- /dev/null
+++ b/console/frontend/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
\ No newline at end of file
diff --git a/console/frontend/vite.config.ts b/console/frontend/vite.config.ts
new file mode 100644
index 0000000..62f7534
--- /dev/null
+++ b/console/frontend/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ host: '0.0.0.0',
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://console-backend:8000',
+ changeOrigin: true
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index bcafb57..14ee06f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,19 @@
version: '3.8'
services:
+ console-frontend:
+ build:
+ context: ./console/frontend
+ dockerfile: Dockerfile
+ container_name: site11_console_frontend
+ ports:
+ - "3000:80"
+ networks:
+ - site11_network
+ restart: unless-stopped
+ depends_on:
+ - console-backend
+
console-backend:
build:
context: ./console/backend