diff --git a/QUICKSTART-TESTING.md b/QUICKSTART-TESTING.md deleted file mode 100644 index 43502df..0000000 --- a/QUICKSTART-TESTING.md +++ /dev/null @@ -1,222 +0,0 @@ -# Testing Quick Start Guide - -Get up and running with ProjectHub tests in minutes! - -## Quick Setup - -### Backend Tests - -⚠️ **IMPORTANT:** The application uses old package versions that require **Python 3.7 or 3.8** -They will NOT work with Python 3.9+ or 3.13. - -**With Python 3.8 (Recommended):** - -```powershell -# Windows - Create Python 3.8 virtual environment -py -3.8 -m venv venv38 -.\venv38\Scripts\Activate.ps1 - -# Navigate to backend and install -cd backend -pip install -r requirements-test.txt - -# Run tests -pytest -v -``` - -```bash -# Linux/macOS -python3.8 -m venv venv38 -source venv38/bin/activate -cd backend -pip install -r requirements-test.txt -pytest -v -``` - -> **Note:** See `backend/PYTHON-COMPATIBILITY.md` for detailed Python version information and upgrade options. - -### Frontend Tests - -```bash -# 1. Navigate to frontend directory -cd frontend - -# 2. Install dependencies -npm install - -# 3. Run tests -npm test -- --watchAll=false -``` - -## Run All Tests (One Command) - -### Linux/macOS - -```bash -# Make script executable (first time only) -chmod +x run-tests.sh - -# Run all tests -./run-tests.sh -``` - -### Windows (PowerShell) - -```powershell -# Run all tests -.\run-tests.ps1 -``` - -## Generate Coverage Reports - -### Linux/macOS - -```bash -chmod +x run-tests-coverage.sh -./run-tests-coverage.sh -``` - -Then open: -- Backend: `backend/htmlcov/index.html` -- Frontend: `frontend/coverage/lcov-report/index.html` - -### Windows (PowerShell) - -```powershell -# Backend coverage -cd backend -pytest --cov=. --cov-report=html -# Open: backend\htmlcov\index.html - -# Frontend coverage -cd ..\frontend -npm run test:coverage -# Open: frontend\coverage\lcov-report\index.html -``` - -## What's Being Tested? - -### Backend (Python/Flask) -✅ **Models** - User, Project, Task, Message, Document -✅ **Authentication** - JWT tokens, login, permissions -✅ **API Routes** - Projects, Tasks, Documents -✅ **Utilities** - Date formatting, file handling, logging - -**Test Files:** -- `backend/tests/test_models.py` -- `backend/tests/test_auth.py` -- `backend/tests/test_routes_projects.py` -- `backend/tests/test_routes_tasks.py` -- `backend/tests/test_utils.py` - -### Frontend (React) -✅ **Components** - Dashboard, Login, TaskList, ProjectDetail -✅ **API Service** - HTTP requests, token management -✅ **User Interactions** - Clicks, form submissions, navigation -✅ **Error Handling** - API errors, loading states - -**Test Files:** -- `frontend/src/components/__tests__/Dashboard.test.js` -- `frontend/src/components/__tests__/Login.test.js` -- `frontend/src/components/__tests__/TaskList.test.js` -- `frontend/src/components/__tests__/ProjectDetail.test.js` -- `frontend/src/services/__tests__/api.test.js` - -## Common Commands - -### Backend - -```bash -# Run all tests -pytest - -# Run specific test file -pytest tests/test_models.py - -# Run with coverage -pytest --cov=. --cov-report=term - -# Run in verbose mode -pytest -v - -# Run specific test -pytest tests/test_models.py::TestUserModel::test_create_user -``` - -### Frontend - -```bash -# Run tests (watch mode) -npm test - -# Run tests once -npm test -- --watchAll=false - -# Run with coverage -npm run test:coverage - -# Run specific test file -npm test -- Dashboard.test.js - -# Update snapshots -npm test -- -u -``` - -## Troubleshooting - -### Backend: "ModuleNotFoundError" -```bash -cd backend -pip install -r requirements-test.txt -``` - -### Backend: "No module named 'pytest'" -```bash -pip install pytest pytest-cov pytest-flask -``` - -### Frontend: "Cannot find module" -```bash -cd frontend -rm -rf node_modules package-lock.json -npm install -``` - -### Frontend: Tests timeout -Increase timeout in test: -```javascript -jest.setTimeout(10000); // 10 seconds -``` - -## Next Steps - -- Read the full [Testing Guide](TESTING.md) -- Check out [Backend Test README](backend/tests/README.md) -- Check out [Frontend Test README](frontend/src/components/__tests__/README.md) - -## Test Statistics - -### Backend -- **Test Files**: 5 -- **Test Classes**: ~15 -- **Test Functions**: ~50+ -- **Coverage Goal**: >80% - -### Frontend -- **Test Files**: 5 -- **Test Suites**: 5 -- **Test Cases**: ~40+ -- **Coverage Goal**: >70% - -## Need Help? - -1. Check the full [TESTING.md](TESTING.md) documentation -2. Review example tests in the test directories -3. Consult the official documentation: - - [pytest docs](https://docs.pytest.org/) - - [React Testing Library](https://testing-library.com/react) - ---- - -**Happy Testing! 🧪** - diff --git a/QUICKSTART.md b/QUICKSTART.md index 7f7f86e..95c1349 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -73,11 +73,13 @@ docker compose -f docker/docker-compose.yml up -d --build backend ## Access Points -- **Frontend**: http://localhost:3000 -- **Backend API**: http://localhost:5000 -- **Nginx**: http://localhost:80 +- **Main Application (via Nginx)**: http://localhost (or http://YOUR_SERVER_IP) +- **Backend API**: http://localhost/api (or http://YOUR_SERVER_IP/api) +- **Admin Dashboard**: http://localhost/admin (or http://YOUR_SERVER_IP/admin) - **Database**: localhost:5432 +**Note**: All services are accessible through Nginx on port 80. The frontend is built during Docker build and served as static files through Nginx. + ## Default Credentials **Application Users** (seeded automatically): @@ -97,20 +99,38 @@ docker compose -f docker/docker-compose.yml up -d --build backend ## First Time Setup -1. Build and start: +1. Clone the repository (if not already cloned): ```bash - docker compose -f docker/docker-compose.yml up -d --build + git clone + cd ProjectHub ``` -2. Wait for database seeding (automatic on first startup) - - Creates admin user and test users - - Seeds projects, tasks, comments, messages (100 messages with 50 templates, spanning 6 months) - - Creates document records - -3. Access the application at http://localhost:3000 +2. Build and start all services: + ```bash + docker compose -f docker/docker-compose.yml up -d --build + ``` + + **That's it!** This single command will: + - Build the frontend (production build happens automatically) + - Build the backend container + - Start all services (database, backend, frontend, nginx) + - Automatically seed the database with test data on first startup + +3. Wait a few seconds for services to initialize, then access the application: + - **Local**: http://localhost + - **Remote Server**: http://YOUR_SERVER_IP (replace with your server's IP address) + + The application is served through Nginx on port 80, which proxies: + - Frontend static files (automatically built) + - Backend API requests to `/api/*` 4. Login with any of the default credentials (see above) +**Optional**: Check logs to verify services are ready: +```bash +docker compose -f docker/docker-compose.yml logs -f +``` + ## Re-seeding Database To reset and re-seed the database (⚠️ deletes all data): @@ -143,9 +163,15 @@ docker compose -f docker/docker-compose.yml up -d - Check backend logs: `docker compose -f docker/docker-compose.yml logs backend` - Verify database is accessible -**Frontend not loading?** +**Frontend not loading (403 Forbidden)?** +- Wait a few seconds for the frontend build to complete (check logs: `docker compose -f docker/docker-compose.yml logs frontend`) +- Verify nginx can access the build files: `docker compose -f docker/docker-compose.yml exec nginx ls -la /usr/share/nginx/html` +- Rebuild if needed: `docker compose -f docker/docker-compose.yml up -d --build` + +**Nginx 403 errors?** +- Frontend build may still be in progress. Wait 30-60 seconds and refresh - Check frontend logs: `docker compose -f docker/docker-compose.yml logs frontend` -- Ensure frontend compiled successfully (look for "Compiled successfully!") +- Check nginx logs: `docker compose -f docker/docker-compose.yml logs nginx` **Database connection errors?** - Ensure database container is running diff --git a/README.md b/README.md index 3324ba6..c3eb34b 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,13 @@ docker compose -f docker/docker-compose.yml build --no-cache docker compose -f docker/docker-compose.yml up -d ``` -**Build and start in one command:** +**Build and start in one command (recommended):** ```bash docker compose -f docker/docker-compose.yml up -d --build ``` +**Note**: The frontend is automatically built during the Docker build process. The production build is created in `frontend/build/` and served by Nginx on port 80. + #### Shutdown **Stop all services (keeps containers):** @@ -148,15 +150,20 @@ cd ProjectHub docker compose -f docker/docker-compose.yml up -d --build ``` -3. Wait for services to initialize (database seeding happens automatically on first startup) + **That's it!** The command will: + - Build the frontend (production build) + - Build the backend container + - Start all services (database, backend, frontend, nginx) + - Automatically seed the database with test data + +3. Wait a few seconds for services to initialize, then access the application: + - **Main Application (via Nginx)**: http://localhost (or http://YOUR_SERVER_IP) + - **Backend API**: http://localhost/api (or http://YOUR_SERVER_IP/api) + - **Admin Dashboard**: http://localhost/admin (or http://YOUR_SERVER_IP/admin) + - **API Health Check**: http://localhost/api/health (or http://YOUR_SERVER_IP/api/health) + - **Database**: localhost:5432 -4. Access the application: -- **Frontend**: http://localhost:3000 -- **Backend API**: http://localhost:5000 -- **Admin Dashboard**: http://localhost:5000/admin -- **API Health Check**: http://localhost:5000/api/health -- **Nginx (Production)**: http://localhost:80 -- **Database**: localhost:5432 + **Note**: The frontend is automatically built and served through Nginx on port 80. No additional build steps required. ### Database Seeding diff --git a/backend/routes/analytics.py b/backend/routes/analytics.py index c4dc9c0..2b1b8c8 100644 --- a/backend/routes/analytics.py +++ b/backend/routes/analytics.py @@ -24,7 +24,7 @@ def get_stats(): timestamp = get_utc_timestamp() ctx = get_request_context() - req_id = request_id if request_id else 'N/A' + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else 'N/A' stats = { 'user_count': query_helper.count_users(), @@ -54,13 +54,15 @@ def search(): query_helper = QueryHelper() search_time = get_utc_now() + ctx = get_request_context() + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else 'N/A' results = { 'query': search_term, 'users': [u.to_dict() for u in query_helper.search_users(search_term)], 'projects': [p.to_dict() for p in query_helper.search_projects(search_term)], 'search_time': search_time.isoformat(), - 'request_id': request_id if request_id else 'N/A' + 'request_id': req_id } log_user_action(user.id, 'analytics_search', {'term': search_term}) @@ -78,10 +80,12 @@ def get_user(user_id): return jsonify({'error': 'User not found'}), 404 fetched_at = get_utc_now() + ctx = get_request_context() + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else 'N/A' return jsonify({ 'user': user.to_dict(), 'fetched_at': fetched_at.isoformat(), - 'request_id': request_id if request_id else 'N/A' + 'request_id': req_id }) diff --git a/backend/routes/api.py b/backend/routes/api.py index 2e814e0..0062101 100644 --- a/backend/routes/api.py +++ b/backend/routes/api.py @@ -19,7 +19,7 @@ def get_users(): """Get all users""" # Access request context ctx = get_request_context() - req_id = request_id if request_id else 'N/A' + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else 'N/A' ip_address = get_request_metadata('ip_address', 'unknown') search = request.args.get('search', '') diff --git a/backend/utils/logger.py b/backend/utils/logger.py index dbbe8ad..e691349 100644 --- a/backend/utils/logger.py +++ b/backend/utils/logger.py @@ -50,7 +50,8 @@ def log_user_action(user_id, action, details=None): logger = logging.getLogger('projecthub') # Get request ID - req_id = request_id if request_id else "N/A" + ctx = get_request_context() + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else "N/A" duration = get_request_duration() duration_str = f" ({duration:.3f}s)" if duration else "" @@ -65,8 +66,8 @@ def log_login_attempt(username, password, success=False): logger = logging.getLogger('projecthub') # Get request ID - req_id = request_id if request_id else "N/A" ctx = get_request_context() + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else "N/A" ip_address = ctx.request.remote_addr if ctx and hasattr(ctx, 'request') and ctx.request else "unknown" status = "SUCCESS" if success else "FAILED" @@ -77,7 +78,8 @@ def log_api_request(user_id, endpoint, request_data): logger = logging.getLogger('projecthub') # Get request ID - req_id = request_id if request_id else "N/A" + ctx = get_request_context() + req_id = ctx.request_id if ctx and hasattr(ctx, 'request_id') else "N/A" duration = get_request_duration() duration_str = f" ({duration:.3f}s)" if duration else "" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3080d50..e82178a 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,8 +49,15 @@ services: volumes: - ../frontend:/app - /app/node_modules + - frontend_build:/build-output depends_on: - backend + healthcheck: + test: ["CMD", "test", "-f", "/build-output/.ready", "-a", "-f", "/build-output/index.html"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 10s networks: - app-network @@ -60,9 +67,10 @@ services: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - - ../frontend/build:/usr/share/nginx/html + - frontend_build:/usr/share/nginx/html depends_on: - - frontend + frontend: + condition: service_healthy networks: - app-network @@ -70,6 +78,7 @@ volumes: postgres_data: uploads: logs: + frontend_build: networks: app-network: diff --git a/docker/nginx.conf b/docker/nginx.conf index 8473694..a84fae9 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -20,15 +20,27 @@ http { autoindex on; } - # Frontend - location / { - root /usr/share/nginx/html; - index index.html; - try_files $uri $uri/ /index.html; + # Admin dashboard (must come before /api/ to avoid matching /api/admin) + location /admin { + proxy_pass http://backend:5000/admin; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Backend API location /api/ { + # Handle CORS preflight requests + if ($request_method = 'OPTIONS') { + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; + add_header Access-Control-Max-Age 1728000; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + proxy_pass http://backend:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -39,6 +51,13 @@ http { add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; } + # Frontend (must come last as catch-all) + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + location /uploads/ { alias /app/uploads/; autoindex on; diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 88ccfcd..2551143 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -11,8 +11,12 @@ RUN npm install # Copy application code COPY . . +# Copy entrypoint script +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh + # Expose port EXPOSE 3000 -CMD ["npm", "start"] +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/frontend/docker-entrypoint.sh b/frontend/docker-entrypoint.sh new file mode 100644 index 0000000..d659e52 --- /dev/null +++ b/frontend/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +# Build the frontend +echo "Building frontend..." +npm run build + +# Copy build files to shared volume if it exists +if [ -d "/build-output" ]; then + echo "Copying build files to shared volume..." + # Clear destination and copy all files including hidden ones + rm -rf /build-output/* /build-output/.[!.]* /build-output/..?* 2>/dev/null || true + cp -r /app/build/. /build-output/ + echo "Build files copied successfully" + + # Create a ready file to signal that build is complete + touch /build-output/.ready + echo "Build ready signal created" +else + echo "Warning: /build-output directory not found, build files not copied to shared volume" +fi + +# Start the development server (for hot-reload during development) +# In production, this container could exit after building +exec npm start + diff --git a/frontend/src/components/UserManagement.js b/frontend/src/components/UserManagement.js index 44c45db..706590a 100644 --- a/frontend/src/components/UserManagement.js +++ b/frontend/src/components/UserManagement.js @@ -26,7 +26,12 @@ function UserManagement({ user }) { setUsers(response.data.users || []); } catch (err) { console.error('Error loading users:', err); - setError('Failed to load users'); + const errorMessage = err.response?.data?.error || + err.response?.statusText || + err.message || + 'Failed to load users'; + const statusCode = err.response?.status; + setError(`Failed to load users${statusCode ? ` (${statusCode})` : ''}: ${errorMessage}`); } finally { setLoading(false); } diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 0280035..b8beafc 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -5,9 +5,9 @@ const getApiBaseUrl = () => { if (process.env.REACT_APP_API_URL) { return process.env.REACT_APP_API_URL; } - // Use the same host as the frontend, but port 5000 - const hostname = window.location.hostname; - return `http://${hostname}:5000/api`; + // Use relative path - nginx will proxy to backend + // This works because frontend is served through nginx on port 80 + return '/api'; }; const API_BASE_URL = getApiBaseUrl();