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();