diff --git a/.gitignore b/.gitignore index 3dbafaf..90d5441 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,6 @@ htmlcov/ *.temp .cache/ -celerybeat-schedule \ No newline at end of file +celerybeat-schedule + +backend/app/static \ No newline at end of file diff --git a/Makefile b/Makefile index 1840953..ea6911b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,15 @@ .PHONY: help install dev-services dev-stop-services dev-backend dev-frontend dev build test lint clean up down restart logs celery-worker celery-beat celery-flower celery-shell +IMAGE_NAME = my-flask-react-app + +# Git Remotes +ORIGIN_REMOTE = origin +DEPLOY_REMOTE = deploy + +# Get current git commit hash +GIT_HASH = $(shell git rev-parse --short HEAD) +GIT_BRANCH = $(shell git rev-parse --abbrev-ref HEAD) + help: ## Show this help message @echo "Available commands:" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' @@ -52,12 +62,6 @@ restart: ## Restart all services logs: ## Show logs from all services docker compose logs -f -logs-backend: ## Show backend logs - docker compose logs -f backend - -logs-frontend: ## Show frontend logs - docker compose logs -f frontend - test: ## Run all tests @echo "Running backend tests..." cd backend && . venv/bin/activate && pytest @@ -170,3 +174,14 @@ logs-celery-beat: ## Show Celery Beat logs logs-flower: ## Show Flower logs docker compose logs -f flower + +docker-build: ## Show Flower logs + docker build -f docker/Dockerfile -t $(IMAGE_NAME):$(GIT_HASH) -t $(IMAGE_NAME):latest . + +docker-run: ## Show Flower logs + docker run -p 8001:8000 $(IMAGE_NAME):latest + +# Push to deploy remote (triggers deployment) +push-deploy: + @echo "Pushing to deploy remote ($(GIT_BRANCH))..." + git push $(DEPLOY_REMOTE) $(GIT_BRANCH) \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index fc87c4f..7b4fc6c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -20,11 +20,11 @@ RUN useradd -m appuser && chown -R appuser:appuser /app USER appuser # Expose port -EXPOSE 5000 +EXPOSE 8000 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5000/health/ || exit 1 + CMD curl -f http://localhost:8000/health/ || exit 1 # Run with gunicorn -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "wsgi:app"] \ No newline at end of file +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "wsgi:app"] \ No newline at end of file diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 4c4f203..3d98468 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -48,11 +48,12 @@ def create_app(config_name=None): # Import models (required for migrations) # Register blueprints - from app.routes import api_bp, health_bp + from app.routes import api_bp, health_bp, home_bp from app.routes.kanban import kanban_bp app.register_blueprint(api_bp, url_prefix="/api") - app.register_blueprint(health_bp) + app.register_blueprint(health_bp, url_prefix="/health") + app.register_blueprint(home_bp) app.register_blueprint(kanban_bp, url_prefix="/api") # Global error handlers diff --git a/backend/app/routes/__init__.py b/backend/app/routes/__init__.py index 439d3ac..b305b47 100644 --- a/backend/app/routes/__init__.py +++ b/backend/app/routes/__init__.py @@ -1,4 +1,5 @@ from .api import api_bp from .health import health_bp +from .home import home_bp -__all__ = ["api_bp", "health_bp"] +__all__ = ["api_bp", "health_bp", "home_bp"] diff --git a/backend/app/routes/home.py b/backend/app/routes/home.py new file mode 100644 index 0000000..6da8cb3 --- /dev/null +++ b/backend/app/routes/home.py @@ -0,0 +1,16 @@ +import os + +from flask import Blueprint, jsonify, send_from_directory, current_app as app + +home_bp = Blueprint("home", __name__) + + +@home_bp.route('/') +def serve_root(): + return send_from_directory(app.static_folder, 'index.html') + +@home_bp.route('/') +def serve_spa(path): + if os.path.exists(os.path.join(app.static_folder, path)): + return send_from_directory(app.static_folder, path) + return send_from_directory(app.static_folder, 'index.html') diff --git a/backend/requirements/dev.txt b/backend/requirements/dev.txt index af83bc9..598079c 100644 --- a/backend/requirements/dev.txt +++ b/backend/requirements/dev.txt @@ -11,3 +11,4 @@ faker==20.1.0 # Celery monitoring flower==2.0.1 +gunicorn==21.2.0 \ No newline at end of file diff --git a/backend/wsgi.py b/backend/wsgi.py index f4150a8..489fcd0 100644 --- a/backend/wsgi.py +++ b/backend/wsgi.py @@ -5,4 +5,4 @@ env = os.environ.get('FLASK_ENV', 'dev') app = create_app(env) if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) \ No newline at end of file + app.run(host='0.0.0.0', port=8000) \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..9367914 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,46 @@ +# --------------------------------------------------------- +# Stage 1: Build the React Frontend +# --------------------------------------------------------- +FROM node:18-alpine AS frontend-build + +WORKDIR /app/frontend + +# Copy package files first for better caching +COPY frontend/package*.json ./ +RUN npm ci + +# Copy source code and build +COPY frontend/ ./ +RUN npm run build + +# --------------------------------------------------------- +# Stage 2: Build the Python Backend & Assemble +# --------------------------------------------------------- +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies (if needed for python packages) +# RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY backend/requirements ./requirements/ +RUN pip install --no-cache-dir -r requirements/dev.txt + +# Copy Flask application code +COPY backend/ ./ + +# Copy the built React files from Stage 1 into Flask's static folder +# Adjust 'build' to 'dist' if you are using Vite +COPY --from=frontend-build /app/frontend/dist ./static + +# Create a non-root user for security +RUN useradd -m appuser +USER appuser + +# Expose port +EXPOSE 8000 + +# Run with Gunicorn (Production WSGI Server) +# --bind 0.0.0.0 makes it accessible outside the container +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "wsgi:app"] \ No newline at end of file diff --git a/frontend/src/components/kanban/DeleteListModal.tsx b/frontend/src/components/kanban/DeleteListModal.tsx index a10889e..a077ffe 100644 --- a/frontend/src/components/kanban/DeleteListModal.tsx +++ b/frontend/src/components/kanban/DeleteListModal.tsx @@ -1,4 +1,5 @@ import { ModalContentProps } from '../../types'; +import { useToast } from '../../context/toasts/useToast'; import Trash2Icon from '../icons/Trash2Icon'; interface DeleteListModalProps extends ModalContentProps { @@ -7,12 +8,22 @@ interface DeleteListModalProps extends ModalContentProps { } export function DeleteListModal({ onClose, onDelete, listName }: DeleteListModalProps) { + const { addNotification } = useToast(); + const handleDelete = async () => { try { await onDelete(); + addNotification({ + type: 'success', + title: 'List deleted successfully', + }); onClose(); } catch (err) { console.error('Failed to delete list:', err); + addNotification({ + type: 'error', + title: 'Failed to delete list', + }); } }; diff --git a/frontend/src/hooks/useChecklistMutations.ts b/frontend/src/hooks/useChecklistMutations.ts index 8793e8d..9230438 100644 --- a/frontend/src/hooks/useChecklistMutations.ts +++ b/frontend/src/hooks/useChecklistMutations.ts @@ -84,7 +84,6 @@ export function useChecklistMutations(cardId: number, onUpdate: () => void) { }; const toggleCheckItem = async (item: CheckItem, currentState: 'incomplete' | 'complete') => { - console.log('item', item); try { const newState = currentState === 'incomplete' ? 'complete' : 'incomplete'; await withLoader(