diff --git a/services/news-engine-console/docker-compose.yml b/services/news-engine-console/docker-compose.yml
new file mode 100644
index 0000000..89def55
--- /dev/null
+++ b/services/news-engine-console/docker-compose.yml
@@ -0,0 +1,46 @@
+version: '3.8'
+
+services:
+ backend:
+ build:
+ context: ./backend
+ dockerfile: Dockerfile
+ container_name: news-engine-console-backend
+ ports:
+ - "8101:8101"
+ environment:
+ - MONGODB_URL=mongodb://host.docker.internal:27017
+ - DB_NAME=news_engine_console_db
+ - JWT_SECRET=your-secret-key-change-this-in-production
+ - JWT_ALGORITHM=HS256
+ - ACCESS_TOKEN_EXPIRE_MINUTES=30
+ - REFRESH_TOKEN_EXPIRE_DAYS=7
+ volumes:
+ - ./backend:/app
+ command: python main.py
+ restart: unless-stopped
+ networks:
+ - news-engine-console-network
+
+ frontend:
+ build:
+ context: ./frontend
+ dockerfile: Dockerfile.dev
+ container_name: news-engine-console-frontend
+ ports:
+ - "3000:3000"
+ environment:
+ - VITE_API_URL=http://localhost:8101
+ volumes:
+ - ./frontend:/app
+ - /app/node_modules
+ command: npm run dev
+ depends_on:
+ - backend
+ restart: unless-stopped
+ networks:
+ - news-engine-console-network
+
+networks:
+ news-engine-console-network:
+ driver: bridge
diff --git a/services/news-engine-console/frontend/.env.example b/services/news-engine-console/frontend/.env.example
new file mode 100644
index 0000000..694f72b
--- /dev/null
+++ b/services/news-engine-console/frontend/.env.example
@@ -0,0 +1,2 @@
+VITE_API_URL=http://localhost:8101
+VITE_APP_TITLE=News Engine Console
diff --git a/services/news-engine-console/frontend/.gitignore b/services/news-engine-console/frontend/.gitignore
new file mode 100644
index 0000000..e72dae6
--- /dev/null
+++ b/services/news-engine-console/frontend/.gitignore
@@ -0,0 +1,29 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Environment variables
+.env
+.env.local
+.env.production
diff --git a/services/news-engine-console/frontend/Dockerfile b/services/news-engine-console/frontend/Dockerfile
new file mode 100644
index 0000000..7730bc4
--- /dev/null
+++ b/services/news-engine-console/frontend/Dockerfile
@@ -0,0 +1,29 @@
+# Multi-stage build for production
+FROM node:18-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package.json ./
+
+# Install dependencies
+RUN npm install
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Production stage
+FROM nginx:alpine
+
+# Copy built files from builder
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+# Copy nginx configuration
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/services/news-engine-console/frontend/Dockerfile.dev b/services/news-engine-console/frontend/Dockerfile.dev
new file mode 100644
index 0000000..004a563
--- /dev/null
+++ b/services/news-engine-console/frontend/Dockerfile.dev
@@ -0,0 +1,18 @@
+# Development Dockerfile for frontend
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Copy package files
+COPY package.json ./
+
+# Install dependencies
+RUN npm install
+
+# Copy source code
+COPY . .
+
+EXPOSE 3000
+
+# Start development server
+CMD ["npm", "run", "dev"]
diff --git a/services/news-engine-console/frontend/index.html b/services/news-engine-console/frontend/index.html
new file mode 100644
index 0000000..e592bd2
--- /dev/null
+++ b/services/news-engine-console/frontend/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ News Engine Console
+
+
+
+
+
+
+
diff --git a/services/news-engine-console/frontend/nginx.conf b/services/news-engine-console/frontend/nginx.conf
new file mode 100644
index 0000000..6cc696d
--- /dev/null
+++ b/services/news-engine-console/frontend/nginx.conf
@@ -0,0 +1,34 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Enable gzip compression
+ gzip on;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+ # SPA routing - fallback to index.html
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Proxy API requests to backend
+ location /api {
+ proxy_pass http://news-engine-console-backend:8101;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ 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;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+}
diff --git a/services/news-engine-console/frontend/package.json b/services/news-engine-console/frontend/package.json
new file mode 100644
index 0000000..bac88bf
--- /dev/null
+++ b/services/news-engine-console/frontend/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "news-engine-console-frontend",
+ "version": "1.0.0",
+ "description": "News Engine Console - Frontend (React + TypeScript + MUI v7)",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.26.2",
+ "@mui/material": "^6.1.3",
+ "@mui/icons-material": "^6.1.3",
+ "@emotion/react": "^11.13.3",
+ "@emotion/styled": "^11.13.0",
+ "@tanstack/react-query": "^5.56.2",
+ "axios": "^1.7.7",
+ "recharts": "^2.12.7",
+ "date-fns": "^4.1.0",
+ "zustand": "^5.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.10",
+ "@types/react-dom": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "^8.8.0",
+ "@typescript-eslint/parser": "^8.8.0",
+ "@vitejs/plugin-react": "^4.3.2",
+ "eslint": "^9.11.1",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "eslint-plugin-react-refresh": "^0.4.12",
+ "typescript": "^5.6.2",
+ "vite": "^5.4.8"
+ }
+}
diff --git a/services/news-engine-console/frontend/src/App.tsx b/services/news-engine-console/frontend/src/App.tsx
new file mode 100644
index 0000000..cf9ddd4
--- /dev/null
+++ b/services/news-engine-console/frontend/src/App.tsx
@@ -0,0 +1,33 @@
+import { Routes, Route, Navigate } from 'react-router-dom'
+import { useAuthStore } from './stores/authStore'
+import Login from './pages/Login'
+import Dashboard from './pages/Dashboard'
+
+function App() {
+ const { isAuthenticated } = useAuthStore()
+
+ return (
+
+ } />
+
+ :
+ }
+ />
+
+ :
+ }
+ />
+
+ {/* Catch all - redirect to dashboard or login */}
+ } />
+
+ )
+}
+
+export default App
diff --git a/services/news-engine-console/frontend/src/api/applications.ts b/services/news-engine-console/frontend/src/api/applications.ts
new file mode 100644
index 0000000..1026f37
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/applications.ts
@@ -0,0 +1,66 @@
+import apiClient from './client'
+import type {
+ Application,
+ ApplicationCreate,
+ ApplicationUpdate,
+ ApplicationSecretResponse,
+} from '@/types'
+
+export const getApplications = async (params?: {
+ skip?: number
+ limit?: number
+}): Promise => {
+ const response = await apiClient.get('/api/v1/applications/', { params })
+ return response.data
+}
+
+export const getApplication = async (applicationId: string): Promise => {
+ const response = await apiClient.get(
+ `/api/v1/applications/${applicationId}`
+ )
+ return response.data
+}
+
+export const createApplication = async (
+ applicationData: ApplicationCreate
+): Promise => {
+ const response = await apiClient.post(
+ '/api/v1/applications/',
+ applicationData
+ )
+ return response.data
+}
+
+export const updateApplication = async (
+ applicationId: string,
+ applicationData: ApplicationUpdate
+): Promise => {
+ const response = await apiClient.put(
+ `/api/v1/applications/${applicationId}`,
+ applicationData
+ )
+ return response.data
+}
+
+export const deleteApplication = async (
+ applicationId: string
+): Promise<{ message: string }> => {
+ const response = await apiClient.delete<{ message: string }>(
+ `/api/v1/applications/${applicationId}`
+ )
+ return response.data
+}
+
+export const regenerateSecret = async (
+ applicationId: string
+): Promise => {
+ const response = await apiClient.post(
+ `/api/v1/applications/${applicationId}/regenerate-secret`
+ )
+ return response.data
+}
+
+export const getMyApplications = async (): Promise => {
+ const response = await apiClient.get('/api/v1/applications/my-apps')
+ return response.data
+}
diff --git a/services/news-engine-console/frontend/src/api/client.ts b/services/news-engine-console/frontend/src/api/client.ts
new file mode 100644
index 0000000..b6fd103
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/client.ts
@@ -0,0 +1,78 @@
+import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
+import type { ApiError } from '@/types'
+
+// Create axios instance with default config
+const apiClient: AxiosInstance = axios.create({
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8101',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ timeout: 30000, // 30 seconds
+})
+
+// Request interceptor - add auth token
+apiClient.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('access_token')
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+)
+
+// Response interceptor - handle errors and token refresh
+apiClient.interceptors.response.use(
+ (response) => response,
+ async (error: AxiosError) => {
+ const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }
+
+ // Handle 401 Unauthorized - token expired
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ originalRequest._retry = true
+
+ try {
+ const refreshToken = localStorage.getItem('refresh_token')
+ if (!refreshToken) {
+ throw new Error('No refresh token available')
+ }
+
+ // Try to refresh the token
+ const response = await axios.post(
+ `${import.meta.env.VITE_API_URL || 'http://localhost:8101'}/api/v1/users/refresh`,
+ { refresh_token: refreshToken }
+ )
+
+ const { access_token, refresh_token: new_refresh_token } = response.data
+
+ // Store new tokens
+ localStorage.setItem('access_token', access_token)
+ if (new_refresh_token) {
+ localStorage.setItem('refresh_token', new_refresh_token)
+ }
+
+ // Retry original request with new token
+ if (originalRequest.headers) {
+ originalRequest.headers.Authorization = `Bearer ${access_token}`
+ }
+ return apiClient(originalRequest)
+ } catch (refreshError) {
+ // Refresh failed - redirect to login
+ localStorage.removeItem('access_token')
+ localStorage.removeItem('refresh_token')
+ localStorage.removeItem('user')
+ window.location.href = '/login'
+ return Promise.reject(refreshError)
+ }
+ }
+
+ // Handle other errors
+ const errorMessage = error.response?.data?.detail || error.message || 'An error occurred'
+ return Promise.reject({ message: errorMessage, status: error.response?.status })
+ }
+)
+
+export default apiClient
diff --git a/services/news-engine-console/frontend/src/api/index.ts b/services/news-engine-console/frontend/src/api/index.ts
new file mode 100644
index 0000000..7e3c1ee
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/index.ts
@@ -0,0 +1,7 @@
+// Export all API modules
+export * from './users'
+export * from './keywords'
+export * from './pipelines'
+export * from './applications'
+export * from './monitoring'
+export { default as apiClient } from './client'
diff --git a/services/news-engine-console/frontend/src/api/keywords.ts b/services/news-engine-console/frontend/src/api/keywords.ts
new file mode 100644
index 0000000..1124abf
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/keywords.ts
@@ -0,0 +1,72 @@
+import apiClient from './client'
+import type {
+ Keyword,
+ KeywordCreate,
+ KeywordUpdate,
+ KeywordStats,
+ KeywordBulkCreate,
+} from '@/types'
+
+export const getKeywords = async (params?: {
+ category?: string
+ status?: string
+ search?: string
+ skip?: number
+ limit?: number
+ sort_by?: string
+ sort_order?: 'asc' | 'desc'
+}): Promise => {
+ const response = await apiClient.get('/api/v1/keywords/', { params })
+ return response.data
+}
+
+export const getKeyword = async (keywordId: string): Promise => {
+ const response = await apiClient.get(`/api/v1/keywords/${keywordId}`)
+ return response.data
+}
+
+export const createKeyword = async (keywordData: KeywordCreate): Promise => {
+ const response = await apiClient.post('/api/v1/keywords/', keywordData)
+ return response.data
+}
+
+export const updateKeyword = async (
+ keywordId: string,
+ keywordData: KeywordUpdate
+): Promise => {
+ const response = await apiClient.put(
+ `/api/v1/keywords/${keywordId}`,
+ keywordData
+ )
+ return response.data
+}
+
+export const deleteKeyword = async (keywordId: string): Promise<{ message: string }> => {
+ const response = await apiClient.delete<{ message: string }>(
+ `/api/v1/keywords/${keywordId}`
+ )
+ return response.data
+}
+
+export const toggleKeywordStatus = async (keywordId: string): Promise => {
+ const response = await apiClient.post(`/api/v1/keywords/${keywordId}/toggle`)
+ return response.data
+}
+
+export const getKeywordStats = async (keywordId: string): Promise => {
+ const response = await apiClient.get(
+ `/api/v1/keywords/${keywordId}/stats`
+ )
+ return response.data
+}
+
+export const bulkCreateKeywords = async (
+ bulkData: KeywordBulkCreate
+): Promise<{ created: number; failed: number; errors: string[] }> => {
+ const response = await apiClient.post<{
+ created: number
+ failed: number
+ errors: string[]
+ }>('/api/v1/keywords/bulk', bulkData)
+ return response.data
+}
diff --git a/services/news-engine-console/frontend/src/api/monitoring.ts b/services/news-engine-console/frontend/src/api/monitoring.ts
new file mode 100644
index 0000000..4838d11
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/monitoring.ts
@@ -0,0 +1,67 @@
+import apiClient from './client'
+import type {
+ MonitoringOverview,
+ SystemStatus,
+ ServiceStatus,
+ SystemMetrics,
+ DatabaseStats,
+ LogEntry,
+ PipelineLog,
+} from '@/types'
+
+export const getMonitoringOverview = async (): Promise => {
+ const response = await apiClient.get('/api/v1/monitoring/')
+ return response.data
+}
+
+export const getHealthCheck = async (): Promise => {
+ const response = await apiClient.get('/api/v1/monitoring/health')
+ return response.data
+}
+
+export const getSystemStatus = async (): Promise => {
+ const response = await apiClient.get('/api/v1/monitoring/system')
+ return response.data
+}
+
+export const getServiceStatus = async (): Promise => {
+ const response = await apiClient.get('/api/v1/monitoring/services')
+ return response.data
+}
+
+export const getDatabaseStats = async (): Promise => {
+ const response = await apiClient.get('/api/v1/monitoring/database')
+ return response.data
+}
+
+export const getRecentLogs = async (params?: { limit?: number }): Promise => {
+ const response = await apiClient.get('/api/v1/monitoring/logs/recent', {
+ params,
+ })
+ return response.data
+}
+
+export const getMetrics = async (): Promise<{
+ cpu: number
+ memory: number
+ disk: number
+ timestamp: string
+}> => {
+ const response = await apiClient.get<{
+ cpu: number
+ memory: number
+ disk: number
+ timestamp: string
+ }>('/api/v1/monitoring/metrics')
+ return response.data
+}
+
+export const getPipelineActivity = async (params?: {
+ limit?: number
+}): Promise => {
+ const response = await apiClient.get(
+ '/api/v1/monitoring/pipelines/activity',
+ { params }
+ )
+ return response.data
+}
diff --git a/services/news-engine-console/frontend/src/api/pipelines.ts b/services/news-engine-console/frontend/src/api/pipelines.ts
new file mode 100644
index 0000000..46ab088
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/pipelines.ts
@@ -0,0 +1,90 @@
+import apiClient from './client'
+import type {
+ Pipeline,
+ PipelineCreate,
+ PipelineUpdate,
+ PipelineLog,
+ PipelineType,
+} from '@/types'
+
+export const getPipelines = async (params?: {
+ type?: string
+ status?: string
+ skip?: number
+ limit?: number
+}): Promise => {
+ const response = await apiClient.get('/api/v1/pipelines/', { params })
+ return response.data
+}
+
+export const getPipeline = async (pipelineId: string): Promise => {
+ const response = await apiClient.get(`/api/v1/pipelines/${pipelineId}`)
+ return response.data
+}
+
+export const createPipeline = async (pipelineData: PipelineCreate): Promise => {
+ const response = await apiClient.post('/api/v1/pipelines/', pipelineData)
+ return response.data
+}
+
+export const updatePipeline = async (
+ pipelineId: string,
+ pipelineData: PipelineUpdate
+): Promise => {
+ const response = await apiClient.put(
+ `/api/v1/pipelines/${pipelineId}`,
+ pipelineData
+ )
+ return response.data
+}
+
+export const deletePipeline = async (pipelineId: string): Promise<{ message: string }> => {
+ const response = await apiClient.delete<{ message: string }>(
+ `/api/v1/pipelines/${pipelineId}`
+ )
+ return response.data
+}
+
+export const startPipeline = async (pipelineId: string): Promise => {
+ const response = await apiClient.post(`/api/v1/pipelines/${pipelineId}/start`)
+ return response.data
+}
+
+export const stopPipeline = async (pipelineId: string): Promise => {
+ const response = await apiClient.post(`/api/v1/pipelines/${pipelineId}/stop`)
+ return response.data
+}
+
+export const restartPipeline = async (pipelineId: string): Promise => {
+ const response = await apiClient.post(
+ `/api/v1/pipelines/${pipelineId}/restart`
+ )
+ return response.data
+}
+
+export const getPipelineLogs = async (
+ pipelineId: string,
+ params?: { limit?: number }
+): Promise => {
+ const response = await apiClient.get(
+ `/api/v1/pipelines/${pipelineId}/logs`,
+ { params }
+ )
+ return response.data
+}
+
+export const updatePipelineConfig = async (
+ pipelineId: string,
+ config: Record
+): Promise => {
+ const response = await apiClient.put(
+ `/api/v1/pipelines/${pipelineId}/config`,
+ config
+ )
+ return response.data
+}
+
+export const getPipelineTypes = async (): Promise => {
+ const response = await apiClient.get('/api/v1/pipelines/types')
+ return response.data
+}
diff --git a/services/news-engine-console/frontend/src/api/users.ts b/services/news-engine-console/frontend/src/api/users.ts
new file mode 100644
index 0000000..bab4a6e
--- /dev/null
+++ b/services/news-engine-console/frontend/src/api/users.ts
@@ -0,0 +1,81 @@
+import apiClient from './client'
+import type {
+ User,
+ UserCreate,
+ UserUpdate,
+ UserLogin,
+ TokenResponse,
+} from '@/types'
+
+// Authentication
+export const login = async (credentials: UserLogin): Promise => {
+ const formData = new URLSearchParams()
+ formData.append('username', credentials.username)
+ formData.append('password', credentials.password)
+
+ const response = await apiClient.post('/api/v1/users/login', formData, {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ })
+ return response.data
+}
+
+export const register = async (userData: UserCreate): Promise => {
+ const response = await apiClient.post('/api/v1/users/register', userData)
+ return response.data
+}
+
+export const refreshToken = async (refreshToken: string): Promise => {
+ const response = await apiClient.post('/api/v1/users/refresh', {
+ refresh_token: refreshToken,
+ })
+ return response.data
+}
+
+export const logout = async (): Promise => {
+ await apiClient.post('/api/v1/users/logout')
+}
+
+// Current user
+export const getCurrentUser = async (): Promise => {
+ const response = await apiClient.get('/api/v1/users/me')
+ return response.data
+}
+
+export const updateCurrentUser = async (userData: UserUpdate): Promise => {
+ const response = await apiClient.put('/api/v1/users/me', userData)
+ return response.data
+}
+
+// User management
+export const getUsers = async (params?: {
+ role?: string
+ disabled?: boolean
+ search?: string
+ skip?: number
+ limit?: number
+}): Promise => {
+ const response = await apiClient.get('/api/v1/users/', { params })
+ return response.data
+}
+
+export const getUser = async (userId: string): Promise => {
+ const response = await apiClient.get(`/api/v1/users/${userId}`)
+ return response.data
+}
+
+export const createUser = async (userData: UserCreate): Promise => {
+ const response = await apiClient.post('/api/v1/users/', userData)
+ return response.data
+}
+
+export const updateUser = async (userId: string, userData: UserUpdate): Promise => {
+ const response = await apiClient.put(`/api/v1/users/${userId}`, userData)
+ return response.data
+}
+
+export const deleteUser = async (userId: string): Promise<{ message: string }> => {
+ const response = await apiClient.delete<{ message: string }>(`/api/v1/users/${userId}`)
+ return response.data
+}
diff --git a/services/news-engine-console/frontend/src/main.tsx b/services/news-engine-console/frontend/src/main.tsx
new file mode 100644
index 0000000..7ca8f9a
--- /dev/null
+++ b/services/news-engine-console/frontend/src/main.tsx
@@ -0,0 +1,55 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import CssBaseline from '@mui/material/CssBaseline'
+import { ThemeProvider, createTheme } from '@mui/material/styles'
+import App from './App'
+
+// Create a client for React Query
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: false,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ },
+ },
+})
+
+// Create MUI theme
+const theme = createTheme({
+ palette: {
+ mode: 'light',
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ },
+ typography: {
+ fontFamily: [
+ '-apple-system',
+ 'BlinkMacSystemFont',
+ '"Segoe UI"',
+ 'Roboto',
+ '"Helvetica Neue"',
+ 'Arial',
+ 'sans-serif',
+ ].join(','),
+ },
+})
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/services/news-engine-console/frontend/src/pages/Dashboard.tsx b/services/news-engine-console/frontend/src/pages/Dashboard.tsx
new file mode 100644
index 0000000..9ad45db
--- /dev/null
+++ b/services/news-engine-console/frontend/src/pages/Dashboard.tsx
@@ -0,0 +1,122 @@
+import { Box, Container, Typography, Button, Paper, Grid } from '@mui/material'
+import { useNavigate } from 'react-router-dom'
+import { useAuthStore } from '../stores/authStore'
+
+export default function Dashboard() {
+ const navigate = useNavigate()
+ const { user, logout } = useAuthStore()
+
+ const handleLogout = async () => {
+ await logout()
+ navigate('/login')
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ News Engine Console
+
+
+
+ {user?.full_name} ({user?.role})
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+ Dashboard
+
+
+
+
+
+
+ Keywords
+
+ 0
+
+ Total keywords
+
+
+
+
+
+
+
+ Pipelines
+
+ 0
+
+ Active pipelines
+
+
+
+
+
+
+
+ Users
+
+ 1
+
+ Registered users
+
+
+
+
+
+
+
+ Applications
+
+ 0
+
+ OAuth apps
+
+
+
+
+
+
+
+ Welcome to News Engine Console
+
+
+ This is your central dashboard for managing news pipelines, keywords, users, and applications.
+
+
+ Frontend is now up and running! Next steps:
+
+
+ Implement sidebar navigation
+ Create Keywords management page
+ Create Pipelines management page
+ Create Users management page
+ Create Applications management page
+ Create Monitoring page
+
+
+
+
+
+
+ )
+}
diff --git a/services/news-engine-console/frontend/src/pages/Login.tsx b/services/news-engine-console/frontend/src/pages/Login.tsx
new file mode 100644
index 0000000..123d4f1
--- /dev/null
+++ b/services/news-engine-console/frontend/src/pages/Login.tsx
@@ -0,0 +1,124 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import {
+ Box,
+ Card,
+ CardContent,
+ TextField,
+ Button,
+ Typography,
+ Alert,
+ Container,
+ Link,
+} from '@mui/material'
+import { useAuthStore } from '../stores/authStore'
+
+export default function Login() {
+ const navigate = useNavigate()
+ const { login, isLoading, error, clearError } = useAuthStore()
+
+ const [formData, setFormData] = useState({
+ username: '',
+ password: '',
+ })
+
+ const handleChange = (e: React.ChangeEvent) => {
+ setFormData({
+ ...formData,
+ [e.target.name]: e.target.value,
+ })
+ clearError()
+ }
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ try {
+ await login(formData)
+ navigate('/dashboard')
+ } catch (error) {
+ console.error('Login failed:', error)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ News Engine Console
+
+
+ Sign in to manage your news pipelines
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/services/news-engine-console/frontend/src/stores/authStore.ts b/services/news-engine-console/frontend/src/stores/authStore.ts
new file mode 100644
index 0000000..4c75f71
--- /dev/null
+++ b/services/news-engine-console/frontend/src/stores/authStore.ts
@@ -0,0 +1,148 @@
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+import type { User, UserLogin } from '@/types'
+import * as authApi from '@/api/users'
+
+interface AuthState {
+ user: User | null
+ accessToken: string | null
+ refreshToken: string | null
+ isAuthenticated: boolean
+ isLoading: boolean
+ error: string | null
+
+ // Actions
+ login: (credentials: UserLogin) => Promise
+ logout: () => Promise
+ register: (userData: any) => Promise
+ fetchCurrentUser: () => Promise
+ updateCurrentUser: (userData: any) => Promise
+ clearError: () => void
+}
+
+export const useAuthStore = create()(
+ persist(
+ (set, get) => ({
+ user: null,
+ accessToken: null,
+ refreshToken: null,
+ isAuthenticated: false,
+ isLoading: false,
+ error: null,
+
+ login: async (credentials: UserLogin) => {
+ try {
+ set({ isLoading: true, error: null })
+
+ const tokenResponse = await authApi.login(credentials)
+
+ // Store tokens in localStorage
+ localStorage.setItem('access_token', tokenResponse.access_token)
+ localStorage.setItem('refresh_token', tokenResponse.refresh_token)
+
+ // Fetch user details
+ const user = await authApi.getCurrentUser()
+
+ set({
+ user,
+ accessToken: tokenResponse.access_token,
+ refreshToken: tokenResponse.refresh_token,
+ isAuthenticated: true,
+ isLoading: false,
+ })
+ } catch (error: any) {
+ set({
+ error: error.message || 'Login failed',
+ isLoading: false,
+ isAuthenticated: false,
+ })
+ throw error
+ }
+ },
+
+ logout: async () => {
+ try {
+ await authApi.logout()
+ } catch (error) {
+ console.error('Logout error:', error)
+ } finally {
+ // Clear tokens from localStorage
+ localStorage.removeItem('access_token')
+ localStorage.removeItem('refresh_token')
+ localStorage.removeItem('user')
+
+ set({
+ user: null,
+ accessToken: null,
+ refreshToken: null,
+ isAuthenticated: false,
+ error: null,
+ })
+ }
+ },
+
+ register: async (userData: any) => {
+ try {
+ set({ isLoading: true, error: null })
+ await authApi.register(userData)
+ set({ isLoading: false })
+ } catch (error: any) {
+ set({
+ error: error.message || 'Registration failed',
+ isLoading: false,
+ })
+ throw error
+ }
+ },
+
+ fetchCurrentUser: async () => {
+ try {
+ set({ isLoading: true, error: null })
+ const user = await authApi.getCurrentUser()
+ set({
+ user,
+ isAuthenticated: true,
+ isLoading: false,
+ })
+ } catch (error: any) {
+ set({
+ error: error.message || 'Failed to fetch user',
+ isLoading: false,
+ isAuthenticated: false,
+ })
+ throw error
+ }
+ },
+
+ updateCurrentUser: async (userData: any) => {
+ try {
+ set({ isLoading: true, error: null })
+ const updatedUser = await authApi.updateCurrentUser(userData)
+ set({
+ user: updatedUser,
+ isLoading: false,
+ })
+ } catch (error: any) {
+ set({
+ error: error.message || 'Failed to update user',
+ isLoading: false,
+ })
+ throw error
+ }
+ },
+
+ clearError: () => {
+ set({ error: null })
+ },
+ }),
+ {
+ name: 'auth-storage',
+ partialize: (state) => ({
+ user: state.user,
+ accessToken: state.accessToken,
+ refreshToken: state.refreshToken,
+ isAuthenticated: state.isAuthenticated,
+ }),
+ }
+ )
+)
diff --git a/services/news-engine-console/frontend/src/types/index.ts b/services/news-engine-console/frontend/src/types/index.ts
new file mode 100644
index 0000000..9b93b98
--- /dev/null
+++ b/services/news-engine-console/frontend/src/types/index.ts
@@ -0,0 +1,264 @@
+// =====================================================
+// User Types
+// =====================================================
+
+export interface User {
+ id?: string
+ username: string
+ email: string
+ full_name: string
+ role: 'admin' | 'editor' | 'viewer'
+ disabled: boolean
+ created_at: string
+ last_login?: string
+}
+
+export interface UserCreate {
+ username: string
+ email: string
+ password: string
+ full_name: string
+ role?: 'admin' | 'editor' | 'viewer'
+}
+
+export interface UserUpdate {
+ email?: string
+ password?: string
+ full_name?: string
+ role?: 'admin' | 'editor' | 'viewer'
+ disabled?: boolean
+}
+
+export interface UserLogin {
+ username: string
+ password: string
+}
+
+export interface TokenResponse {
+ access_token: string
+ refresh_token: string
+ token_type: string
+}
+
+// =====================================================
+// Keyword Types
+// =====================================================
+
+export interface Keyword {
+ id?: string
+ keyword: string
+ category: 'people' | 'topics' | 'companies'
+ status: 'active' | 'inactive'
+ pipeline_type: string
+ priority: number
+ metadata: Record
+ created_at: string
+ updated_at: string
+ created_by?: string
+}
+
+export interface KeywordCreate {
+ keyword: string
+ category: 'people' | 'topics' | 'companies'
+ status?: 'active' | 'inactive'
+ pipeline_type?: string
+ priority?: number
+ metadata?: Record
+}
+
+export interface KeywordUpdate {
+ keyword?: string
+ category?: 'people' | 'topics' | 'companies'
+ status?: 'active' | 'inactive'
+ pipeline_type?: string
+ priority?: number
+ metadata?: Record
+}
+
+export interface KeywordStats {
+ keyword_id: string
+ keyword: string
+ total_articles: number
+ total_translations: number
+ total_images: number
+ last_processed: string | null
+ success_rate: number
+}
+
+export interface KeywordBulkCreate {
+ keywords: KeywordCreate[]
+}
+
+// =====================================================
+// Pipeline Types
+// =====================================================
+
+export interface PipelineStats {
+ total_runs: number
+ successful_runs: number
+ failed_runs: number
+ last_success: string | null
+ last_failure: string | null
+ avg_duration_seconds: number
+}
+
+export interface Pipeline {
+ id?: string
+ name: string
+ type: 'rss_collector' | 'translator' | 'image_generator'
+ status: 'running' | 'stopped' | 'error'
+ config: Record
+ schedule?: string
+ stats: PipelineStats
+ last_run?: string
+ next_run?: string
+ created_at: string
+ updated_at: string
+ created_by?: string
+}
+
+export interface PipelineCreate {
+ name: string
+ type: 'rss_collector' | 'translator' | 'image_generator'
+ config?: Record
+ schedule?: string
+}
+
+export interface PipelineUpdate {
+ name?: string
+ type?: 'rss_collector' | 'translator' | 'image_generator'
+ status?: 'running' | 'stopped' | 'error'
+ config?: Record
+ schedule?: string
+}
+
+export interface PipelineLog {
+ timestamp: string
+ level: 'info' | 'warning' | 'error'
+ message: string
+ details?: Record
+}
+
+export interface PipelineType {
+ type: string
+ name: string
+ description: string
+}
+
+// =====================================================
+// Application Types (OAuth2)
+// =====================================================
+
+export interface Application {
+ id?: string
+ name: string
+ client_id: string
+ client_secret?: string
+ redirect_uris: string[]
+ grant_types: string[]
+ scopes: string[]
+ owner_id: string
+ created_at: string
+ updated_at?: string
+}
+
+export interface ApplicationCreate {
+ name: string
+ redirect_uris?: string[]
+ grant_types?: string[]
+ scopes?: string[]
+}
+
+export interface ApplicationUpdate {
+ name?: string
+ redirect_uris?: string[]
+ grant_types?: string[]
+ scopes?: string[]
+}
+
+export interface ApplicationSecretResponse {
+ client_id: string
+ client_secret: string
+ message: string
+}
+
+// =====================================================
+// Monitoring Types
+// =====================================================
+
+export interface SystemStatus {
+ status: 'healthy' | 'degraded' | 'down'
+ timestamp: string
+ uptime_seconds: number
+ version: string
+}
+
+export interface ServiceStatus {
+ name: string
+ status: 'up' | 'down'
+ response_time_ms?: number
+ last_check: string
+ details?: Record
+}
+
+export interface DatabaseStats {
+ collections: number
+ total_documents: number
+ total_size_mb: number
+ indexes: number
+}
+
+export interface SystemMetrics {
+ cpu_percent: number
+ memory_percent: number
+ memory_used_mb: number
+ memory_total_mb: number
+ disk_percent: number
+ disk_used_gb: number
+ disk_total_gb: number
+}
+
+export interface LogEntry {
+ timestamp: string
+ level: 'info' | 'warning' | 'error' | 'debug'
+ message: string
+ source?: string
+ details?: Record
+}
+
+export interface MonitoringOverview {
+ system: SystemStatus
+ services: ServiceStatus[]
+ metrics: SystemMetrics
+ recent_logs: LogEntry[]
+}
+
+// =====================================================
+// API Response Types
+// =====================================================
+
+export interface ApiError {
+ detail: string
+ status_code?: number
+}
+
+export interface PaginationParams {
+ skip?: number
+ limit?: number
+}
+
+export interface PaginatedResponse {
+ items: T[]
+ total: number
+ skip: number
+ limit: number
+}
+
+// =====================================================
+// Common Types
+// =====================================================
+
+export type Status = 'active' | 'inactive' | 'running' | 'stopped' | 'error'
+export type Role = 'admin' | 'editor' | 'viewer'
+export type Category = 'people' | 'topics' | 'companies'
+export type PipelineTypeEnum = 'rss_collector' | 'translator' | 'image_generator'
diff --git a/services/news-engine-console/frontend/tsconfig.json b/services/news-engine-console/frontend/tsconfig.json
new file mode 100644
index 0000000..d1e16d4
--- /dev/null
+++ b/services/news-engine-console/frontend/tsconfig.json
@@ -0,0 +1,36 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Path aliases */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@/components/*": ["./src/components/*"],
+ "@/pages/*": ["./src/pages/*"],
+ "@/api/*": ["./src/api/*"],
+ "@/hooks/*": ["./src/hooks/*"],
+ "@/types/*": ["./src/types/*"],
+ "@/utils/*": ["./src/utils/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/services/news-engine-console/frontend/vite.config.ts b/services/news-engine-console/frontend/vite.config.ts
new file mode 100644
index 0000000..dfeacd8
--- /dev/null
+++ b/services/news-engine-console/frontend/vite.config.ts
@@ -0,0 +1,33 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ '@/components': path.resolve(__dirname, './src/components'),
+ '@/pages': path.resolve(__dirname, './src/pages'),
+ '@/api': path.resolve(__dirname, './src/api'),
+ '@/hooks': path.resolve(__dirname, './src/hooks'),
+ '@/types': path.resolve(__dirname, './src/types'),
+ '@/utils': path.resolve(__dirname, './src/utils'),
+ },
+ },
+ server: {
+ host: '0.0.0.0',
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: process.env.VITE_API_URL || 'http://localhost:8101',
+ changeOrigin: true,
+ },
+ },
+ },
+ build: {
+ outDir: 'dist',
+ sourcemap: true,
+ },
+})