diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03405d4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# ── .NET ────────────────────────────────────────────────────────────────────── +**/bin/ +**/obj/ +**/.vs/ +**/*.user +**/logs/ +**/*.log + +# ── Frontend ────────────────────────────────────────────────────────────────── +frontend/node_modules/ +frontend/dist/ +frontend/.cache/ + +# ── Git / CI ────────────────────────────────────────────────────────────────── +.git/ +.github/ +.gitignore + +# ── Documentação / Exemplos (não necessários na imagem) ─────────────────────── +README.md +AGENTS.md +examples/ +database/ + +# ── Variáveis de ambiente (nunca incluir segredos na imagem) ────────────────── +.env +.env.* +!.env.example diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..016e9be --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,136 @@ +name: CI/CD Pipeline + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + API_IMAGE: ${{ github.repository }}/api + FRONTEND_IMAGE: ${{ github.repository }}/frontend + +jobs: + # ============================================================================ + # JOB 1 — Build & Lint da API .NET + # ============================================================================ + build-api: + name: Build API (.NET 9) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "9.0.x" + + - name: Restore dependencies + run: dotnet restore EtlMonitoring.sln + + - name: Build + run: dotnet build EtlMonitoring.sln --no-restore -c Release + + # ============================================================================ + # JOB 2 — Lint & Build do Frontend React + # ============================================================================ + build-frontend: + name: Build Frontend (Node 22) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Lint + working-directory: frontend + run: npm run lint + + - name: Build + working-directory: frontend + run: npm run build + + # ============================================================================ + # JOB 3 — Publicar imagens Docker no GHCR (apenas na branch main) + # ============================================================================ + publish: + name: Publish Docker Images + runs-on: ubuntu-latest + needs: [build-api, build-frontend] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login no GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + # ── API ──────────────────────────────────────────────────────────────── + - name: Metadata da imagem (API) + id: meta-api + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.API_IMAGE }} + tags: | + type=sha,prefix=sha- + type=raw,value=latest + + - name: Build e push da imagem (API) + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta-api.outputs.tags }} + labels: ${{ steps.meta-api.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # ── Frontend ────────────────────────────────────────────────────────── + - name: Metadata da imagem (Frontend) + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }} + tags: | + type=sha,prefix=sha- + type=raw,value=latest + + - name: Build e push da imagem (Frontend) + uses: docker/build-push-action@v6 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VITE_API_URL=/api diff --git a/.gitignore b/.gitignore index 8a30d25..b480056 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,5 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.env +.env.example \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6bfea5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# ============================================================================= +# Stage 1: Build +# ============================================================================= +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copiar arquivos de projeto primeiro para aproveitar cache de camadas +COPY EtlMonitoring.sln . +COPY src/EtlMonitoring.Api/EtlMonitoring.Api.csproj src/EtlMonitoring.Api/ +COPY src/EtlMonitoring.Core/EtlMonitoring.Core.csproj src/EtlMonitoring.Core/ +COPY src/EtlMonitoring.Infrastructure/EtlMonitoring.Infrastructure.csproj src/EtlMonitoring.Infrastructure/ + +RUN dotnet restore EtlMonitoring.sln + +# Copiar restante do código-fonte +COPY src/ src/ + +RUN dotnet publish src/EtlMonitoring.Api/EtlMonitoring.Api.csproj \ + -c Release \ + -o /app/publish \ + --no-restore + +# ============================================================================= +# Stage 2: Runtime +# ============================================================================= +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app + +# Criar usuário não-root para segurança +RUN groupadd -r appgroup && useradd -r -g appgroup appuser + +# Instalar curl para healthcheck +RUN apt-get update && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app/publish . + +# Criar diretório de logs com permissões corretas +RUN mkdir -p logs && chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 8080 + +ENV ASPNETCORE_URLS=http://+:8080 \ + ASPNETCORE_ENVIRONMENT=Production + +ENTRYPOINT ["dotnet", "EtlMonitoring.Api.dll"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9c5b59f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +name: datapulsecm + +services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + ACCEPT_EULA: "Y" + SA_PASSWORD: "${SA_PASSWORD:-DataPulseCM@2024!}" + MSSQL_PID: Express + ports: + - "1433:1433" + volumes: + - sqlserver_data:/var/opt/mssql + healthcheck: + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"${SA_PASSWORD:-DataPulseCM@2024!}\" -Q 'SELECT 1' -No"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + restart: unless-stopped + + # --------------------------------------------------------------------------- + # API .NET 9 + # --------------------------------------------------------------------------- + api: + build: + context: . + dockerfile: Dockerfile + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__DefaultConnection: "Server=sqlserver,1433;Database=DataPulseCM;User Id=sa;Password=${SA_PASSWORD:-DataPulseCM@2024!};TrustServerCertificate=True;" + SEQ_URL: "${SEQ_URL:-}" + SEQ_API_KEY: "${SEQ_API_KEY:-}" + ports: + - "5105:8080" + depends_on: + sqlserver: + condition: service_healthy + volumes: + - api_logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/live || exit 1"] + interval: 30s + timeout: 30s + retries: 5 + start_period: 30s + + # --------------------------------------------------------------------------- + # Frontend React (nginx) + # --------------------------------------------------------------------------- + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: /api # nginx faz proxy /api → api:8080 + ports: + - "80:80" + depends_on: + api: + condition: service_healthy + restart: unless-stopped + +volumes: + sqlserver_data: + api_logs: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..edc15fc --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,29 @@ +# ============================================================================= +# Stage 1: Build +# ============================================================================= +FROM node:22-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts + +COPY . . + +# VITE_API_URL é resolvido em build-time pelo Vite +# Em docker-compose, passe --build-arg VITE_API_URL=/api (nginx faz proxy) +ARG VITE_API_URL=/api +ENV VITE_API_URL=$VITE_API_URL + +RUN npm run build + +# ============================================================================= +# Stage 2: Serve com nginx +# ============================================================================= +FROM nginx:1.27-alpine AS final + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..1c0f780 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,39 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # SPA fallback — React Router funciona corretamente + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy reverso para a API dentro do docker-compose + location /api/ { + proxy_pass http://api:8080/api/; + proxy_http_version 1.1; + 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 para o Swagger + location /swagger/ { + proxy_pass http://api:8080/swagger/; + proxy_http_version 1.1; + 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; + } + + # Health check endpoint (evita log excessivo do nginx) + location /health { + proxy_pass http://api:8080/health; + proxy_http_version 1.1; + access_log off; + } +} diff --git a/frontend/src/components/Common/StatusBadge.tsx b/frontend/src/components/Common/StatusBadge.tsx index 70bba15..e06bba9 100644 --- a/frontend/src/components/Common/StatusBadge.tsx +++ b/frontend/src/components/Common/StatusBadge.tsx @@ -1,3 +1,4 @@ +import type { ReactElement } from 'react'; import { Chip } from '@mui/material'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import ErrorIcon from '@mui/icons-material/Error'; @@ -12,7 +13,7 @@ interface StatusBadgeProps { export default function StatusBadge({ status }: StatusBadgeProps) { const normalizedStatus = status?.toUpperCase() || ''; - let config: { color: 'success' | 'error' | 'warning' | 'info' | 'default'; icon: JSX.Element; label: string }; + let config: { color: 'success' | 'error' | 'warning' | 'info' | 'default'; icon: ReactElement; label: string }; if (normalizedStatus.includes('SUCESSO') || normalizedStatus === 'SUCCESS') { config = { color: 'success', icon: , label: 'Sucesso' }; diff --git a/frontend/src/components/DashBoard/StatusChart.tsx b/frontend/src/components/DashBoard/StatusChart.tsx index f253c3d..6f48ef8 100644 --- a/frontend/src/components/DashBoard/StatusChart.tsx +++ b/frontend/src/components/DashBoard/StatusChart.tsx @@ -43,8 +43,8 @@ export default function StatusChart({ byStatus }: StatusChartProps) { [value, 'Execuções']} - labelFormatter={(label: string) => `Status: ${label}`} + formatter={(value: number | undefined) => [value ?? 0, 'Execuções'] as [number, string]} + labelFormatter={(label: unknown) => `Status: ${String(label)}`} /> {chartData.map((entry, index) => ( diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 26fd7eb..728e8c0 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import type { JobExecution, Statistics, JobExecutionDetail } from '../types/job.types'; -const API_BASE = 'http://localhost:5105/api'; +const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:5105/api'; const api = axios.create({ baseURL: API_BASE, diff --git a/src/EtlMonitoring.Api/Program.cs b/src/EtlMonitoring.Api/Program.cs index 65ff04f..819b727 100644 --- a/src/EtlMonitoring.Api/Program.cs +++ b/src/EtlMonitoring.Api/Program.cs @@ -27,7 +27,9 @@ outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj} {Properties:j}{NewLine}{Exception}", retainedFileCountLimit: 30, fileSizeLimitBytes: 10_485_760) // 10MB - .WriteTo.Seq("http://localhost:5341", apiKey: "your-seq-api-key-here") // Opcional: Seq para visualização avançada + .WriteTo.Seq( + serverUrl: Environment.GetEnvironmentVariable("SEQ_URL") ?? "http://localhost:5341", + apiKey: Environment.GetEnvironmentVariable("SEQ_API_KEY")) // Configure SEQ_URL e SEQ_API_KEY como variáveis de ambiente .CreateLogger(); try @@ -127,6 +129,14 @@ } // Mapear Health Checks +// /health/live → apenas verifica se o processo está vivo (sem checar DB) — usado pelo docker healthcheck +app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions +{ + Predicate = _ => false // não executa nenhum check, retorna sempre 200 +}); +// /health/ready → checa todas as dependências (DB, etc.) +app.MapHealthChecks("/health/ready"); +// /health → compatibilidade retroativa app.MapHealthChecks("/health"); app.UseAuthorization();