diff --git a/.gitignore b/.gitignore
index 4e17dba..be5020e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ node_modules
package-lock.json
.cursor
*.excalidraw
+public/dist/assets/
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..00593be
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,240 @@
+
+
+
+
+
+ Excalidraw POC - Backend API Integration
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..30003db
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,523 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { Excalidraw, convertToExcalidrawElements } from '@excalidraw/excalidraw'
+import '@excalidraw/excalidraw/index.css'
+
+function App() {
+ const [excalidrawAPI, setExcalidrawAPI] = useState(null)
+ const [isConnected, setIsConnected] = useState(false)
+ const [elementCount, setElementCount] = useState(0)
+ const [apiPanelVisible, setApiPanelVisible] = useState(true)
+ const [notifications, setNotifications] = useState([])
+ const websocketRef = useRef(null)
+
+ // Form state
+ const [formData, setFormData] = useState({
+ type: 'rectangle',
+ x: 100,
+ y: 100,
+ width: 100,
+ height: 100,
+ text: '',
+ backgroundColor: '#ffffff',
+ strokeColor: '#000000',
+ strokeWidth: 2
+ })
+
+ // WebSocket connection
+ useEffect(() => {
+ connectWebSocket()
+ return () => {
+ if (websocketRef.current) {
+ websocketRef.current.close()
+ }
+ }
+ }, [])
+
+ // Load existing elements when Excalidraw API becomes available
+ useEffect(() => {
+ if (excalidrawAPI) {
+ console.log('ExcalidrawAPI ready, setting up real-time sync')
+
+ // Load existing elements immediately
+ loadExistingElements()
+
+ // Ensure WebSocket is connected for real-time updates
+ if (!isConnected) {
+ console.log('ExcalidrawAPI ready but WebSocket not connected, connecting...')
+ connectWebSocket()
+ } else {
+ console.log('Both ExcalidrawAPI and WebSocket are ready for real-time sync')
+ }
+ }
+ }, [excalidrawAPI, isConnected])
+
+ const loadExistingElements = async () => {
+ try {
+ const response = await fetch('/api/elements')
+ const result = await response.json()
+
+ if (result.success && result.elements && result.elements.length > 0) {
+ console.log('Loading existing elements from API:', result.elements.length)
+ const convertedElements = convertToExcalidrawElements(result.elements)
+ console.log('Converted existing elements:', convertedElements)
+ excalidrawAPI.updateScene({ elements: convertedElements })
+ }
+ } catch (error) {
+ console.error('Error loading existing elements:', error)
+ }
+ }
+
+ const connectWebSocket = () => {
+ if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
+ console.log('WebSocket already connected')
+ return
+ }
+
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
+ const wsUrl = `${protocol}//${window.location.host}`
+
+ console.log('Connecting to WebSocket:', wsUrl)
+ websocketRef.current = new WebSocket(wsUrl)
+
+ websocketRef.current.onopen = () => {
+ console.log('WebSocket connected successfully')
+ setIsConnected(true)
+
+ // Request existing elements when WebSocket connects and API is ready
+ if (excalidrawAPI) {
+ console.log('WebSocket connected and ExcalidrawAPI ready - requesting refresh')
+ setTimeout(loadExistingElements, 100) // Small delay to ensure connection is stable
+ }
+ }
+
+ websocketRef.current.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ handleWebSocketMessage(data)
+ } catch (error) {
+ console.error('Error parsing WebSocket message:', error, event.data)
+ }
+ }
+
+ websocketRef.current.onclose = (event) => {
+ console.log('WebSocket disconnected:', event.code, event.reason)
+ setIsConnected(false)
+
+ // Reconnect after 3 seconds if not a clean close
+ if (event.code !== 1000) {
+ console.log('Attempting to reconnect in 3 seconds...')
+ setTimeout(connectWebSocket, 3000)
+ }
+ }
+
+ websocketRef.current.onerror = (error) => {
+ console.error('WebSocket error:', error)
+ setIsConnected(false)
+ }
+ }
+
+ const handleWebSocketMessage = (data) => {
+ console.log('WebSocket message received:', data.type, data)
+
+ if (!excalidrawAPI) {
+ console.log('ExcalidrawAPI not ready, skipping message:', data.type)
+ return
+ }
+
+ try {
+ const currentElements = excalidrawAPI.getSceneElements()
+ console.log('Current elements count:', currentElements.length)
+
+ switch (data.type) {
+ case 'initial_elements':
+ if (data.elements && data.elements.length > 0) {
+ console.log('Loading initial elements:', data.elements.length)
+ const convertedElements = convertToExcalidrawElements(data.elements)
+ console.log('Converted initial elements:', convertedElements.length)
+ excalidrawAPI.updateScene({ elements: convertedElements })
+ }
+ break
+
+ case 'element_created':
+ console.log('Processing element_created:', data.element)
+ const newElement = convertToExcalidrawElements([data.element])
+ console.log('Converted new element:', newElement)
+ const updatedElementsAfterCreate = [...currentElements, ...newElement]
+ console.log('Total elements after create:', updatedElementsAfterCreate.length)
+ excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate })
+ showNotification('Element created successfully!', 'success')
+ break
+
+ case 'element_updated':
+ console.log('Processing element_updated:', data.element.id)
+ const convertedUpdatedElement = convertToExcalidrawElements([data.element])[0]
+ const updatedElements = currentElements.map(el =>
+ el.id === data.element.id ? convertedUpdatedElement : el
+ )
+ excalidrawAPI.updateScene({ elements: updatedElements })
+ showNotification('Element updated successfully!', 'success')
+ break
+
+ case 'element_deleted':
+ console.log('Processing element_deleted:', data.elementId)
+ const filteredElements = currentElements.filter(el => el.id !== data.elementId)
+ excalidrawAPI.updateScene({ elements: filteredElements })
+ showNotification('Element deleted successfully!', 'success')
+ break
+
+ case 'elements_batch_created':
+ console.log('Processing elements_batch_created:', data.elements.length)
+ const batchElements = convertToExcalidrawElements(data.elements)
+ console.log('Converted batch elements:', batchElements.length)
+ const updatedElementsAfterBatch = [...currentElements, ...batchElements]
+ console.log('Total elements after batch:', updatedElementsAfterBatch.length)
+ excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch })
+ showNotification(`${data.elements.length} elements created!`, 'success')
+ break
+
+ default:
+ console.log('Unknown WebSocket message type:', data.type)
+ }
+ } catch (error) {
+ console.error('Error processing WebSocket message:', error, data)
+ showNotification('Error processing real-time update', 'error')
+ }
+ }
+
+ const showNotification = (message, type = 'success') => {
+ const id = Date.now()
+ const notification = { id, message, type }
+ setNotifications(prev => [...prev, notification])
+
+ setTimeout(() => {
+ setNotifications(prev => prev.filter(n => n.id !== id))
+ }, 3000)
+ }
+
+ const handleExcalidrawChange = (elements, appState, files) => {
+ setElementCount(elements.length)
+ }
+
+ const handleFormSubmit = async (e) => {
+ e.preventDefault()
+
+ const elementData = {
+ type: formData.type,
+ x: parseInt(formData.x),
+ y: parseInt(formData.y),
+ width: parseInt(formData.width) || undefined,
+ height: parseInt(formData.height) || undefined,
+ backgroundColor: formData.backgroundColor,
+ strokeColor: formData.strokeColor,
+ strokeWidth: parseInt(formData.strokeWidth)
+ }
+
+ if (formData.text) {
+ elementData.text = formData.text
+ }
+
+ // Remove undefined values
+ Object.keys(elementData).forEach(key => {
+ if (elementData[key] === undefined) {
+ delete elementData[key]
+ }
+ })
+
+ try {
+ const response = await fetch('/api/elements', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(elementData)
+ })
+
+ const result = await response.json()
+
+ if (result.success) {
+ showNotification('Element created via API!', 'success')
+ // Reset form positions
+ setFormData(prev => ({
+ ...prev,
+ x: Math.floor(Math.random() * 400) + 50,
+ y: Math.floor(Math.random() * 300) + 50
+ }))
+ } else {
+ showNotification(`Error: ${result.error}`, 'error')
+ }
+ } catch (error) {
+ console.error('Error creating element:', error)
+ showNotification('Failed to create element', 'error')
+ }
+ }
+
+ const createSampleElements = async () => {
+ const sampleElements = [
+ {
+ type: 'rectangle',
+ x: 50,
+ y: 50,
+ width: 150,
+ height: 100,
+ backgroundColor: '#ffeaa7',
+ strokeColor: '#2d3436'
+ },
+ {
+ type: 'ellipse',
+ x: 250,
+ y: 50,
+ width: 120,
+ height: 120,
+ backgroundColor: '#74b9ff',
+ strokeColor: '#0984e3'
+ },
+ {
+ type: 'diamond',
+ x: 50,
+ y: 200,
+ width: 100,
+ height: 100,
+ backgroundColor: '#fd79a8',
+ strokeColor: '#e84393'
+ },
+ {
+ type: 'text',
+ x: 250,
+ y: 220,
+ text: 'Hello from API!',
+ fontSize: 20,
+ strokeColor: '#2d3436'
+ }
+ ]
+
+ try {
+ const response = await fetch('/api/elements/batch', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ elements: sampleElements })
+ })
+
+ const result = await response.json()
+
+ if (result.success) {
+ showNotification(`${result.count} sample elements created!`, 'success')
+ } else {
+ showNotification(`Error: ${result.error}`, 'error')
+ }
+ } catch (error) {
+ console.error('Error creating sample elements:', error)
+ showNotification('Failed to create sample elements', 'error')
+ }
+ }
+
+ const clearCanvas = () => {
+ if (excalidrawAPI) {
+ excalidrawAPI.updateScene({ elements: [] })
+ showNotification('Canvas cleared!', 'success')
+ }
+ }
+
+ const refreshElements = async () => {
+ console.log('Manual refresh requested')
+ if (excalidrawAPI) {
+ await loadExistingElements()
+ showNotification('Elements refreshed!', 'success')
+ } else {
+ showNotification('Canvas not ready yet', 'error')
+ }
+ }
+
+ const forceReconnectWebSocket = () => {
+ console.log('Force reconnecting WebSocket')
+ if (websocketRef.current) {
+ websocketRef.current.close()
+ }
+ setTimeout(connectWebSocket, 100)
+ }
+
+ const toggleApiPanel = () => {
+ setApiPanelVisible(!apiPanelVisible)
+ }
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }))
+ }
+
+ return (
+
+ {/* Header */}
+
+
Excalidraw POC - Backend API Integration
+
+
+
+
{isConnected ? 'Connected' : 'Disconnected'}
+
+
+ Elements: {elementCount}
+
+
+
+
+ {!isConnected && (
+
+ )}
+
+
+
+ {/* API Panel Toggle */}
+
+
+ {/* API Panel */}
+ {apiPanelVisible && (
+
+
Create Element via API
+
+
+ )}
+
+ {/* Canvas Container */}
+
+ setExcalidrawAPI(api)}
+ onChange={handleExcalidrawChange}
+ initialData={{
+ elements: [],
+ appState: {
+ theme: 'light',
+ viewBackgroundColor: '#ffffff'
+ }
+ }}
+ />
+
+
+ {/* Notifications */}
+ {notifications.map(notification => (
+
+ {notification.message}
+
+ ))}
+
+ )
+}
+
+export default App
\ No newline at end of file
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..aad322f
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
\ No newline at end of file
diff --git a/package.json b/package.json
index 0afd752..222d603 100644
--- a/package.json
+++ b/package.json
@@ -10,25 +10,39 @@
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
+ "server": "node src/server.js",
+ "server:dev": "nodemon src/server.js",
+ "frontend:dev": "vite",
+ "frontend:build": "vite build",
+ "frontend:preview": "vite preview",
+ "build": "npm run frontend:build",
+ "poc": "concurrently \"npm run server\" \"npm run frontend:dev\"",
"test": "jest",
"lint": "eslint src/**/*.js",
- "build": "tsc",
"docker:build": "docker build -t mcp/excalidraw .",
"docker:run": "docker run -p 3000:3000 mcp/excalidraw",
"prepare": "node -e \"try { require('fs').chmodSync('./src/cli.js', '755') } catch(e) { console.log(e) }\""
},
"dependencies": {
+ "@excalidraw/excalidraw": "^0.18.0",
"@modelcontextprotocol/sdk": "latest",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
"winston": "^3.11.0",
+ "ws": "^8.14.2",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.3"
},
"devDependencies": {
- "nodemon": "^3.0.2",
+ "@vitejs/plugin-react": "^4.6.0",
+ "concurrently": "^9.2.0",
+ "eslint": "^8.56.0",
"jest": "^29.7.0",
- "eslint": "^8.56.0"
+ "nodemon": "^3.0.2",
+ "vite": "^6.3.5"
},
"keywords": [
"mcp",
@@ -50,4 +64,4 @@
"README.md",
"LICENSE"
]
-}
\ No newline at end of file
+}
diff --git a/public/dist/frontend/index.html b/public/dist/frontend/index.html
new file mode 100644
index 0000000..709f2ae
--- /dev/null
+++ b/public/dist/frontend/index.html
@@ -0,0 +1,241 @@
+
+
+
+
+
+ Excalidraw POC - Backend API Integration
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/dist/index.html b/public/dist/index.html
new file mode 100644
index 0000000..709f2ae
--- /dev/null
+++ b/public/dist/index.html
@@ -0,0 +1,241 @@
+
+
+
+
+
+ Excalidraw POC - Backend API Integration
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..b99c9c9
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,621 @@
+
+
+
+
+
+ Excalidraw POC - Backend API Integration
+
+
+
+
+
+
+
+
+
Create Element via API
+
+
+
+
+
+
+
+
⏳
+
Loading Excalidraw...
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/cli.js b/src/cli.js
old mode 100644
new mode 100755
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000..a3d379b
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,382 @@
+import express from 'express';
+import cors from 'cors';
+import { WebSocketServer } from 'ws';
+import { createServer } from 'http';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import logger from './utils/logger.js';
+import {
+ elements,
+ generateId,
+ EXCALIDRAW_ELEMENT_TYPES
+} from './types.js';
+import { z } from 'zod';
+
+// Load environment variables
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const app = express();
+const server = createServer(app);
+const wss = new WebSocketServer({ server });
+
+// Middleware
+app.use(cors());
+app.use(express.json());
+
+// Serve static files from the build directory for production
+// or from the old public directory for development
+const staticDir = process.env.NODE_ENV === 'production'
+ ? path.join(__dirname, '../public/dist')
+ : path.join(__dirname, '../public');
+app.use(express.static(staticDir));
+
+// WebSocket connections
+const clients = new Set();
+
+// Broadcast to all connected clients
+function broadcast(message) {
+ const data = JSON.stringify(message);
+ clients.forEach(client => {
+ if (client.readyState === client.OPEN) {
+ client.send(data);
+ }
+ });
+}
+
+// WebSocket connection handling
+wss.on('connection', (ws) => {
+ clients.add(ws);
+ logger.info('New WebSocket connection established');
+
+ // Send current elements to new client
+ ws.send(JSON.stringify({
+ type: 'initial_elements',
+ elements: Array.from(elements.values())
+ }));
+
+ ws.on('close', () => {
+ clients.delete(ws);
+ logger.info('WebSocket connection closed');
+ });
+
+ ws.on('error', (error) => {
+ logger.error('WebSocket error:', error);
+ clients.delete(ws);
+ });
+});
+
+// Schema validation
+const CreateElementSchema = z.object({
+ type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)),
+ x: z.number(),
+ y: z.number(),
+ width: z.number().optional(),
+ height: z.number().optional(),
+ backgroundColor: z.string().optional(),
+ strokeColor: z.string().optional(),
+ strokeWidth: z.number().optional(),
+ roughness: z.number().optional(),
+ opacity: z.number().optional(),
+ text: z.string().optional(),
+ fontSize: z.number().optional(),
+ fontFamily: z.string().optional()
+});
+
+const UpdateElementSchema = z.object({
+ id: z.string(),
+ type: z.enum(Object.values(EXCALIDRAW_ELEMENT_TYPES)).optional(),
+ x: z.number().optional(),
+ y: z.number().optional(),
+ width: z.number().optional(),
+ height: z.number().optional(),
+ backgroundColor: z.string().optional(),
+ strokeColor: z.string().optional(),
+ strokeWidth: z.number().optional(),
+ roughness: z.number().optional(),
+ opacity: z.number().optional(),
+ text: z.string().optional(),
+ fontSize: z.number().optional(),
+ fontFamily: z.string().optional()
+});
+
+// API Routes
+
+// Get all elements
+app.get('/api/elements', (req, res) => {
+ try {
+ const elementsArray = Array.from(elements.values());
+ res.json({
+ success: true,
+ elements: elementsArray,
+ count: elementsArray.length
+ });
+ } catch (error) {
+ logger.error('Error fetching elements:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Create new element
+app.post('/api/elements', (req, res) => {
+ try {
+ const params = CreateElementSchema.parse(req.body);
+ logger.info('Creating element via API', { type: params.type });
+
+ const id = generateId();
+ const element = {
+ id,
+ ...params,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ version: 1
+ };
+
+ elements.set(id, element);
+
+ // Broadcast to all connected clients
+ broadcast({
+ type: 'element_created',
+ element: element
+ });
+
+ res.json({
+ success: true,
+ element: element
+ });
+ } catch (error) {
+ logger.error('Error creating element:', error);
+ res.status(400).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Update element
+app.put('/api/elements/:id', (req, res) => {
+ try {
+ const { id } = req.params;
+ const updates = UpdateElementSchema.parse({ id, ...req.body });
+
+ const existingElement = elements.get(id);
+ if (!existingElement) {
+ return res.status(404).json({
+ success: false,
+ error: `Element with ID ${id} not found`
+ });
+ }
+
+ const updatedElement = {
+ ...existingElement,
+ ...updates,
+ updatedAt: new Date().toISOString(),
+ version: existingElement.version + 1
+ };
+
+ elements.set(id, updatedElement);
+
+ // Broadcast to all connected clients
+ broadcast({
+ type: 'element_updated',
+ element: updatedElement
+ });
+
+ res.json({
+ success: true,
+ element: updatedElement
+ });
+ } catch (error) {
+ logger.error('Error updating element:', error);
+ res.status(400).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Delete element
+app.delete('/api/elements/:id', (req, res) => {
+ try {
+ const { id } = req.params;
+
+ if (!elements.has(id)) {
+ return res.status(404).json({
+ success: false,
+ error: `Element with ID ${id} not found`
+ });
+ }
+
+ elements.delete(id);
+
+ // Broadcast to all connected clients
+ broadcast({
+ type: 'element_deleted',
+ elementId: id
+ });
+
+ res.json({
+ success: true,
+ message: `Element ${id} deleted successfully`
+ });
+ } catch (error) {
+ logger.error('Error deleting element:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Get element by ID
+app.get('/api/elements/:id', (req, res) => {
+ try {
+ const { id } = req.params;
+ const element = elements.get(id);
+
+ if (!element) {
+ return res.status(404).json({
+ success: false,
+ error: `Element with ID ${id} not found`
+ });
+ }
+
+ res.json({
+ success: true,
+ element: element
+ });
+ } catch (error) {
+ logger.error('Error fetching element:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Query elements with filters
+app.get('/api/elements/search', (req, res) => {
+ try {
+ const { type, ...filters } = req.query;
+ let results = Array.from(elements.values());
+
+ // Filter by type if specified
+ if (type) {
+ results = results.filter(element => element.type === type);
+ }
+
+ // Apply additional filters
+ if (Object.keys(filters).length > 0) {
+ results = results.filter(element => {
+ return Object.entries(filters).every(([key, value]) => {
+ return element[key] === value;
+ });
+ });
+ }
+
+ res.json({
+ success: true,
+ elements: results,
+ count: results.length
+ });
+ } catch (error) {
+ logger.error('Error querying elements:', error);
+ res.status(500).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Batch create elements
+app.post('/api/elements/batch', (req, res) => {
+ try {
+ const { elements: elementsToCreate } = req.body;
+
+ if (!Array.isArray(elementsToCreate)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Expected an array of elements'
+ });
+ }
+
+ const createdElements = [];
+
+ elementsToCreate.forEach(elementData => {
+ const params = CreateElementSchema.parse(elementData);
+ const id = generateId();
+ const element = {
+ id,
+ ...params,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ version: 1
+ };
+
+ elements.set(id, element);
+ createdElements.push(element);
+ });
+
+ // Broadcast to all connected clients
+ broadcast({
+ type: 'elements_batch_created',
+ elements: createdElements
+ });
+
+ res.json({
+ success: true,
+ elements: createdElements,
+ count: createdElements.length
+ });
+ } catch (error) {
+ logger.error('Error batch creating elements:', error);
+ res.status(400).json({
+ success: false,
+ error: error.message
+ });
+ }
+});
+
+// Serve the frontend
+app.get('/', (req, res) => {
+ const htmlFile = process.env.NODE_ENV === 'production'
+ ? path.join(__dirname, '../public/dist/index.html')
+ : path.join(__dirname, '../public/index.html');
+ res.sendFile(htmlFile);
+});
+
+// Health check endpoint
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ elements_count: elements.size,
+ websocket_clients: clients.size
+ });
+});
+
+// Error handling middleware
+app.use((err, req, res, next) => {
+ logger.error('Unhandled error:', err);
+ res.status(500).json({
+ success: false,
+ error: 'Internal server error'
+ });
+});
+
+// Start server
+const PORT = process.env.PORT || 3000;
+const HOST = process.env.HOST || 'localhost';
+
+server.listen(PORT, HOST, () => {
+ logger.info(`POC server running on http://${HOST}:${PORT}`);
+ logger.info(`WebSocket server running on ws://${HOST}:${PORT}`);
+});
+
+export default app;
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..14eeede
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'public/dist',
+ rollupOptions: {
+ input: {
+ main: './frontend/index.html',
+ },
+ },
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ },
+ '/health': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ },
+ },
+ },
+})
\ No newline at end of file