first commit
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
# Variables de Entorno para Prueba de Producción Local
|
||||
|
||||
# Database
|
||||
POSTGRES_DB=checklist_db
|
||||
POSTGRES_USER=checklist_user
|
||||
POSTGRES_PASSWORD=checklist_pass_2024_prod
|
||||
|
||||
# Backend
|
||||
SECRET_KEY=production-secret-key-super-segura-minimo-32-caracteres-123456
|
||||
OPENAI_API_KEY=
|
||||
ENVIRONMENT=production
|
||||
|
||||
# CORS - Para prueba local
|
||||
ALLOWED_ORIGINS=http://localhost,http://localhost:80,http://127.0.0.1
|
||||
|
||||
# Frontend - Para prueba local (apunta al backend en puerto 8000)
|
||||
VITE_API_URL=http://localhost:8000
|
||||
@@ -1,17 +1,22 @@
|
||||
# Variables de Entorno para Producción - Dockploy
|
||||
# Producción - CAMBIAR TODOS ESTOS VALORES
|
||||
POSTGRES_DB=syntria_db
|
||||
POSTGRES_USER=syntria_user
|
||||
POSTGRES_PASSWORD=syntria_db
|
||||
|
||||
# Database
|
||||
POSTGRES_DB=checklist_db
|
||||
POSTGRES_USER=checklist_user
|
||||
POSTGRES_PASSWORD=CAMBIAR-PASSWORD-SUPER-SEGURA-AQUI
|
||||
# Secret key para JWT (mínimo 32 caracteres)
|
||||
SECRET_KEY=CHANGE_THIS_TO_RANDOM_STRING_MIN_32_CHARS_PRODUCTION
|
||||
|
||||
# Backend
|
||||
SECRET_KEY=CAMBIAR-CLAVE-SECRETA-MINIMO-32-CARACTERES-SUPER-SEGURA
|
||||
OPENAI_API_KEY=tu-openai-api-key-aqui
|
||||
# OpenAI API Key (opcional, para IA)
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# Docker Hub username
|
||||
DOCKER_USERNAME=dymai
|
||||
|
||||
# API URL para el frontend
|
||||
API_URL=http://your-domain.com:8000
|
||||
|
||||
# Orígenes permitidos (CORS)
|
||||
ALLOWED_ORIGINS=http://your-domain.com,https://your-domain.com
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
|
||||
# CORS - URL del FRONTEND (de donde vienen las peticiones)
|
||||
ALLOWED_ORIGINS=http://checklist-frontend-n5eten-9cb24a-72-61-106-199.traefik.me
|
||||
|
||||
# Frontend - Vacío para usar URL relativa con proxy de Nginx
|
||||
VITE_API_URL=
|
||||
|
||||
170
AI_FUNCTIONALITY.md
Normal file
170
AI_FUNCTIONALITY.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Sistema de IA para Checklists Inteligentes
|
||||
|
||||
## Modos de IA
|
||||
|
||||
### 1. **OFF (Sin IA)**
|
||||
- El mecánico completa manualmente todas las respuestas
|
||||
- Sin sugerencias ni asistencia automática
|
||||
- Control total del usuario
|
||||
|
||||
### 2. **ASSISTED (IA Asistida)**
|
||||
**Funcionalidades:**
|
||||
- **Análisis de fotos**: Cuando el mecánico sube una foto, la IA analiza la imagen y sugiere:
|
||||
- Estado del componente (bueno/malo)
|
||||
- Nivel de criticidad (ok/minor/critical)
|
||||
- Observaciones automáticas basadas en lo que detecta
|
||||
- **Sugerencias contextuales**: Basándose en respuestas previas
|
||||
- **Detección de anomalías**: Si detecta algo crítico, lo marca automáticamente
|
||||
- **El mecánico puede aceptar o rechazar** las sugerencias
|
||||
|
||||
**Ejemplo de flujo:**
|
||||
```
|
||||
1. Mecánico sube foto de pastillas de freno
|
||||
2. IA analiza: "Desgaste del 85%, menos de 2mm de material"
|
||||
3. Sugiere: Estado=Crítico, Requiere reemplazo inmediato
|
||||
4. Mecánico revisa y confirma o modifica
|
||||
```
|
||||
|
||||
### 3. **FULL (IA Completa - Copilot)**
|
||||
**Funcionalidades:**
|
||||
- **Inspección automática por fotos**: El mecánico solo toma fotos
|
||||
- **Análisis completo**: La IA responde todas las preguntas automáticamente
|
||||
- **Generación de informe**: Crea observaciones y recomendaciones
|
||||
- **Detección de problemas**: Marca automáticamente items críticos
|
||||
- **El mecánico solo revisa y firma** al final
|
||||
|
||||
**Ejemplo de flujo:**
|
||||
```
|
||||
1. Mecánico toma 20 fotos del vehículo
|
||||
2. IA procesa todas las fotos
|
||||
3. Responde las 99 preguntas automáticamente
|
||||
4. Genera observaciones detalladas
|
||||
5. Marca 3 items como críticos
|
||||
6. Mecánico revisa el informe completo
|
||||
7. Ajusta si es necesario
|
||||
8. Firma y completa
|
||||
```
|
||||
|
||||
## Implementación Técnica
|
||||
|
||||
### Análisis de Imágenes con OpenAI Vision
|
||||
```javascript
|
||||
// Frontend: Subir foto
|
||||
const analyzeImage = async (imageFile, questionType) => {
|
||||
const formData = new FormData()
|
||||
formData.append('image', imageFile)
|
||||
formData.append('question_id', questionId)
|
||||
|
||||
const response = await fetch('/api/analyze-image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
return response.json() // { status, observations, confidence }
|
||||
}
|
||||
```
|
||||
|
||||
### Backend: Procesamiento con IA
|
||||
```python
|
||||
# Backend: Analizar imagen con OpenAI GPT-4 Vision
|
||||
import openai
|
||||
from PIL import Image
|
||||
|
||||
async def analyze_vehicle_component(image_path: str, question: Question):
|
||||
# Cargar imagen
|
||||
with open(image_path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
|
||||
# Prompt especializado según tipo de pregunta
|
||||
prompts = {
|
||||
"brakes": "Analiza el estado de las pastillas de freno. Indica desgaste, grietas, material restante.",
|
||||
"tires": "Evalúa la banda de rodadura, presión aparente, desgaste irregular.",
|
||||
"lights": "Verifica funcionamiento de luces, roturas, opacidad.",
|
||||
# ... más prompts especializados
|
||||
}
|
||||
|
||||
# Llamada a OpenAI Vision API
|
||||
response = openai.ChatCompletion.create(
|
||||
model="gpt-4-vision-preview",
|
||||
messages=[{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": prompts.get(question.section, "Analiza este componente")},
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_data}"}}
|
||||
]
|
||||
}],
|
||||
max_tokens=300
|
||||
)
|
||||
|
||||
# Parsear respuesta
|
||||
ai_analysis = response.choices[0].message.content
|
||||
|
||||
# Determinar status según análisis
|
||||
if "crítico" in ai_analysis.lower() or "falla" in ai_analysis.lower():
|
||||
status = "critical"
|
||||
elif "menor" in ai_analysis.lower() or "atención" in ai_analysis.lower():
|
||||
status = "minor"
|
||||
else:
|
||||
status = "ok"
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"observations": ai_analysis,
|
||||
"confidence": 0.85,
|
||||
"ai_analysis": {
|
||||
"raw_response": ai_analysis,
|
||||
"model": "gpt-4-vision",
|
||||
"timestamp": datetime.now()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Ventajas de cada modo
|
||||
|
||||
### OFF
|
||||
✅ Control total del mecánico
|
||||
✅ Sin dependencia de conectividad
|
||||
✅ Sin costos de API
|
||||
|
||||
### ASSISTED
|
||||
✅ Ayuda al mecánico a no olvidar detalles
|
||||
✅ Documenta mejor con análisis de fotos
|
||||
✅ Reduce errores humanos
|
||||
✅ Aprende patrones comunes
|
||||
⚠️ Requiere internet y API key
|
||||
|
||||
### FULL
|
||||
✅ Rapidez extrema (5-10 min vs 30-40 min)
|
||||
✅ Consistencia en evaluaciones
|
||||
✅ Ideal para inspecciones masivas
|
||||
✅ Genera informes detallados automáticamente
|
||||
⚠️ Requiere validación del mecánico
|
||||
⚠️ Mayor costo de API
|
||||
|
||||
## Casos de Uso Recomendados
|
||||
|
||||
**OFF**:
|
||||
- Talleres sin internet estable
|
||||
- Inspecciones básicas
|
||||
- Presupuesto limitado
|
||||
|
||||
**ASSISTED**:
|
||||
- Talleres medianos/grandes
|
||||
- Inspecciones preventivas
|
||||
- Documentación detallada requerida
|
||||
|
||||
**FULL**:
|
||||
- Flotas de vehículos
|
||||
- Inspecciones pre-compra masivas
|
||||
- Talleres de alto volumen
|
||||
- Empresas de rent-a-car
|
||||
|
||||
## Próximos pasos de implementación
|
||||
|
||||
1. ✅ Estructura de base de datos preparada
|
||||
2. ⏳ Endpoint `/api/analyze-image` para análisis
|
||||
3. ⏳ Integración con OpenAI Vision API
|
||||
4. ⏳ UI para mostrar sugerencias de IA
|
||||
5. ⏳ Sistema de confianza (confidence score)
|
||||
6. ⏳ Historial de sugerencias aceptadas/rechazadas
|
||||
7. ⏳ Fine-tuning del modelo con datos reales
|
||||
193
DEPLOYMENT.md
Normal file
193
DEPLOYMENT.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Syntria - Despliegue en Producción
|
||||
|
||||
## 📋 Requisitos Previos
|
||||
|
||||
- Docker y Docker Compose instalados
|
||||
- Cuenta en Docker Hub
|
||||
- Servidor Linux/Windows con puertos 80, 8000 y 5432 disponibles
|
||||
|
||||
## 🚀 Despliegue Rápido
|
||||
|
||||
### 1. Construir y Publicar Imágenes
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\build-and-push.ps1
|
||||
```
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
chmod +x build-and-push.sh
|
||||
./build-and-push.sh
|
||||
```
|
||||
|
||||
### 2. Configurar Producción
|
||||
|
||||
Edita `.env.production` con tus valores:
|
||||
|
||||
```env
|
||||
POSTGRES_PASSWORD=tu_password_seguro_aqui
|
||||
SECRET_KEY=clave_secreta_minimo_32_caracteres
|
||||
DOCKER_USERNAME=tu_usuario_dockerhub
|
||||
API_URL=http://tu-dominio.com:8000
|
||||
ALLOWED_ORIGINS=http://tu-dominio.com,https://tu-dominio.com
|
||||
```
|
||||
|
||||
### 3. Desplegar en Servidor
|
||||
|
||||
En tu servidor de producción:
|
||||
|
||||
```bash
|
||||
# Clonar o subir archivos necesarios
|
||||
# - docker-compose.prod.yml
|
||||
# - .env.production
|
||||
# - init-db.sh
|
||||
|
||||
# Desplegar
|
||||
docker-compose -f docker-compose.prod.yml --env-file .env.production up -d
|
||||
|
||||
# Verificar estado
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Ver logs
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
```
|
||||
|
||||
### 4. Inicializar Base de Datos
|
||||
|
||||
```bash
|
||||
# Crear usuarios iniciales
|
||||
docker exec -it syntria-backend-prod python init_db.py
|
||||
```
|
||||
|
||||
## 🔧 Mantenimiento
|
||||
|
||||
### Ver Logs
|
||||
```bash
|
||||
# Todos los servicios
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
# Solo backend
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
|
||||
# Solo frontend
|
||||
docker-compose -f docker-compose.prod.yml logs -f frontend
|
||||
```
|
||||
|
||||
### Actualizar Imágenes
|
||||
```bash
|
||||
# Pull nuevas versiones
|
||||
docker-compose -f docker-compose.prod.yml pull
|
||||
|
||||
# Reiniciar servicios
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### Backup Base de Datos
|
||||
```bash
|
||||
# Crear backup
|
||||
docker exec syntria-db-prod pg_dump -U syntria_user syntria_db > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Restaurar backup
|
||||
docker exec -i syntria-db-prod psql -U syntria_user syntria_db < backup_20241118.sql
|
||||
```
|
||||
|
||||
## 🌐 Acceso
|
||||
|
||||
- **Frontend:** http://tu-servidor
|
||||
- **Backend API:** http://tu-servidor:8000
|
||||
- **Docs API:** http://tu-servidor:8000/docs
|
||||
|
||||
## 🔒 Seguridad
|
||||
|
||||
### Recomendaciones:
|
||||
|
||||
1. **Cambiar passwords por defecto**
|
||||
- PostgreSQL password
|
||||
- SECRET_KEY (mínimo 32 caracteres aleatorios)
|
||||
|
||||
2. **Usar HTTPS**
|
||||
- Configurar certificado SSL
|
||||
- Usar reverse proxy (Nginx/Traefik)
|
||||
|
||||
3. **Firewall**
|
||||
- Abrir solo puertos necesarios (80, 443)
|
||||
- Cerrar puerto 5432 al público
|
||||
|
||||
4. **Backups automáticos**
|
||||
- Configurar cron job para backups diarios
|
||||
|
||||
## 📊 Monitoreo
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Frontend
|
||||
curl http://localhost
|
||||
|
||||
# Database
|
||||
docker exec syntria-db-prod pg_isready -U syntria_user
|
||||
```
|
||||
|
||||
## 🐳 Imágenes Docker Hub
|
||||
|
||||
Las imágenes están disponibles en:
|
||||
- `usuario/syntria-backend:latest`
|
||||
- `usuario/syntria-frontend:latest`
|
||||
|
||||
## ⚙️ Variables de Entorno
|
||||
|
||||
### Backend
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `SECRET_KEY` - JWT secret key (32+ chars)
|
||||
- `OPENAI_API_KEY` - OpenAI API key (opcional)
|
||||
- `ENVIRONMENT` - `production`
|
||||
- `ALLOWED_ORIGINS` - CORS origins
|
||||
|
||||
### Frontend
|
||||
- `VITE_API_URL` - Backend API URL
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Error: Connection refused
|
||||
```bash
|
||||
# Verificar que todos los servicios estén corriendo
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# Verificar logs
|
||||
docker-compose -f docker-compose.prod.yml logs backend
|
||||
```
|
||||
|
||||
### Error: Database connection
|
||||
```bash
|
||||
# Verificar variables de entorno
|
||||
docker exec syntria-backend-prod env | grep DATABASE_URL
|
||||
|
||||
# Verificar que postgres esté healthy
|
||||
docker inspect syntria-db-prod | grep Health
|
||||
```
|
||||
|
||||
### Error: CORS
|
||||
- Actualizar `ALLOWED_ORIGINS` en `.env.production`
|
||||
- Incluir http:// y https:// si usas ambos
|
||||
|
||||
## 📝 Usuarios por Defecto
|
||||
|
||||
Después de `init_db.py`:
|
||||
- **Admin:** admin / admin123
|
||||
- **Mecánico:** mechanic / mechanic123
|
||||
|
||||
⚠️ **Cambiar passwords en producción**
|
||||
|
||||
## 🔄 Actualización de Versiones
|
||||
|
||||
1. Hacer cambios en código
|
||||
2. Ejecutar `build-and-push.ps1` con nueva versión
|
||||
3. En servidor: `docker-compose pull && docker-compose up -d`
|
||||
|
||||
## 📞 Soporte
|
||||
|
||||
Para issues y preguntas: GitHub Issues
|
||||
@@ -1,256 +0,0 @@
|
||||
# Guía de Deployment en Dockploy
|
||||
|
||||
## Arquitectura de Conexión
|
||||
|
||||
### Desarrollo (Docker Compose Local)
|
||||
```
|
||||
Frontend (localhost:5173) → Backend (localhost:8000) → PostgreSQL (localhost:5432)
|
||||
```
|
||||
|
||||
### Producción (Dockploy)
|
||||
```
|
||||
Frontend (tudominio.com) → Backend (api.tudominio.com) → PostgreSQL (interno)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Pasos para Deploy en Dockploy
|
||||
|
||||
### 1. Preparar Variables de Entorno
|
||||
|
||||
**Backend (.env)**
|
||||
```bash
|
||||
DATABASE_URL=postgresql://user:pass@postgres:5432/checklist_db
|
||||
SECRET_KEY=tu-clave-secreta-minimo-32-caracteres-super-segura
|
||||
OPENAI_API_KEY=tu-api-key-de-openai
|
||||
ENVIRONMENT=production
|
||||
ALLOWED_ORIGINS=https://tudominio.com,https://www.tudominio.com
|
||||
```
|
||||
|
||||
**Frontend (.env)**
|
||||
```bash
|
||||
VITE_API_URL=https://api.tudominio.com
|
||||
```
|
||||
|
||||
### 2. Actualizar CORS en Backend
|
||||
|
||||
Editar `backend/app/main.py`:
|
||||
```python
|
||||
import os
|
||||
|
||||
# CORS
|
||||
allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Crear Dockerfile de Producción para Frontend
|
||||
|
||||
Crear `frontend/Dockerfile.prod`:
|
||||
```dockerfile
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
COPY . .
|
||||
ARG VITE_API_URL
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Servidor Nginx para servir archivos estáticos
|
||||
FROM nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
Crear `frontend/nginx.conf`:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Configuración de cache para assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Estructura Recomendada en Dockploy
|
||||
|
||||
**Opción A: Servicios Separados (Recomendado)**
|
||||
- **Servicio 1**: PostgreSQL (Base de datos interna)
|
||||
- **Servicio 2**: Backend API (api.tudominio.com)
|
||||
- **Servicio 3**: Frontend (tudominio.com)
|
||||
|
||||
**Opción B: Docker Compose en Dockploy**
|
||||
Usar `docker-compose.prod.yml`:
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ENVIRONMENT: production
|
||||
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- postgres
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.prod
|
||||
args:
|
||||
VITE_API_URL: ${VITE_API_URL}
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
### 5. Configuración de Dominios en Dockploy
|
||||
|
||||
1. **Backend API**:
|
||||
- Dominio: `api.tudominio.com`
|
||||
- Puerto interno: `8000`
|
||||
- Habilitar SSL/TLS
|
||||
|
||||
2. **Frontend**:
|
||||
- Dominio: `tudominio.com`
|
||||
- Puerto interno: `80`
|
||||
- Habilitar SSL/TLS
|
||||
|
||||
3. **PostgreSQL**:
|
||||
- Solo acceso interno (no exponer públicamente)
|
||||
|
||||
### 6. Scripts de Migración
|
||||
|
||||
Crear `backend/migrate.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Ejecutar migraciones en producción
|
||||
python -c "from app.core.database import Base, engine; Base.metadata.create_all(bind=engine)"
|
||||
```
|
||||
|
||||
### 7. Health Checks
|
||||
|
||||
El backend ya tiene el endpoint de health. Dockploy puede monitorearlo:
|
||||
```
|
||||
GET https://api.tudominio.com/docs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Seguridad en Producción
|
||||
|
||||
1. **Variables de Entorno**:
|
||||
- Usar secretos de Dockploy, NO hardcodear
|
||||
- Cambiar `SECRET_KEY` a algo seguro (min 32 caracteres)
|
||||
- No exponer `DATABASE_URL` públicamente
|
||||
|
||||
2. **HTTPS**:
|
||||
- Dockploy maneja SSL automáticamente con Let's Encrypt
|
||||
- Asegurar que CORS solo acepte HTTPS en producción
|
||||
|
||||
3. **Database**:
|
||||
- NO exponer puerto 5432 públicamente
|
||||
- Usar credenciales fuertes
|
||||
- Configurar backups automáticos
|
||||
|
||||
---
|
||||
|
||||
## 📝 Checklist Pre-Deploy
|
||||
|
||||
- [ ] Cambiar `SECRET_KEY` en variables de entorno
|
||||
- [ ] Configurar `VITE_API_URL` con dominio de producción
|
||||
- [ ] Actualizar CORS con dominios de producción
|
||||
- [ ] Crear Dockerfile.prod para frontend
|
||||
- [ ] Configurar nginx.conf
|
||||
- [ ] Probar build local: `docker-compose -f docker-compose.prod.yml up`
|
||||
- [ ] Configurar dominios en Dockploy
|
||||
- [ ] Habilitar SSL/TLS en ambos servicios
|
||||
- [ ] Crear usuario admin inicial en producción
|
||||
- [ ] Configurar backups de PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Comandos Útiles
|
||||
|
||||
**Build local de producción:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml build
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
**Ver logs:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs -f backend
|
||||
docker-compose -f docker-compose.prod.yml logs -f frontend
|
||||
```
|
||||
|
||||
**Crear usuario admin en producción:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml exec backend python -c "
|
||||
from app.core.database import SessionLocal
|
||||
from app.core.security import get_password_hash
|
||||
from app.models import User
|
||||
|
||||
db = SessionLocal()
|
||||
admin = User(
|
||||
username='admin',
|
||||
email='admin@tudominio.com',
|
||||
full_name='Administrador',
|
||||
hashed_password=get_password_hash('tu-password-segura'),
|
||||
role='admin',
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
print('Admin creado exitosamente')
|
||||
"
|
||||
```
|
||||
24
backend/.dockerignore
Normal file
24
backend/.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
||||
# Backend .dockerignore
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
.venv/
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.log
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.prod
|
||||
README.md
|
||||
.env
|
||||
.env.*
|
||||
*.md
|
||||
27
backend/Dockerfile.prod
Normal file
27
backend/Dockerfile.prod
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias del sistema
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
postgresql-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copiar requirements
|
||||
COPY requirements.txt .
|
||||
|
||||
# Instalar dependencias de Python
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copiar código
|
||||
COPY . .
|
||||
|
||||
# Crear directorio para uploads
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
# Exponer puerto
|
||||
EXPOSE 8000
|
||||
|
||||
# Comando de producción (múltiples workers)
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
||||
@@ -1,22 +1,39 @@
|
||||
"""
|
||||
Script para agregar preguntas a un checklist basado en el formato del PDF
|
||||
Ejecutar: docker cp add_questions.py checklist-backend:/app/ && docker-compose exec -T backend python /app/add_questions.py
|
||||
Ejecutar: docker-compose exec -T backend python /app/add_questions.py
|
||||
"""
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.models import Checklist, Question
|
||||
|
||||
# ID del checklist al que quieres agregar preguntas
|
||||
CHECKLIST_ID = 2 # Cambia este número según el ID de tu checklist
|
||||
from app.models import Checklist, Question, User
|
||||
|
||||
db = SessionLocal()
|
||||
|
||||
# Verificar que el checklist existe
|
||||
checklist = db.query(Checklist).filter(Checklist.id == CHECKLIST_ID).first()
|
||||
if not checklist:
|
||||
print(f"❌ Checklist con ID {CHECKLIST_ID} no encontrado")
|
||||
# Obtener el usuario admin
|
||||
admin = db.query(User).filter(User.username == "admin").first()
|
||||
if not admin:
|
||||
print("❌ Usuario admin no encontrado")
|
||||
exit(1)
|
||||
|
||||
# Verificar si ya existe un checklist, si no, crearlo
|
||||
checklist = db.query(Checklist).filter(Checklist.name == "Inspección Preventiva de Vehículos").first()
|
||||
if not checklist:
|
||||
print("📋 Creando checklist...")
|
||||
checklist = Checklist(
|
||||
name="Inspección Preventiva de Vehículos",
|
||||
description="Checklist completo para inspección preventiva de vehículos",
|
||||
ai_mode="off",
|
||||
scoring_enabled=True,
|
||||
created_by=admin.id
|
||||
)
|
||||
db.add(checklist)
|
||||
db.commit()
|
||||
db.refresh(checklist)
|
||||
print(f"✅ Checklist creado con ID: {checklist.id}")
|
||||
else:
|
||||
print(f"✅ Usando checklist existente: {checklist.name} (ID: {checklist.id})")
|
||||
|
||||
CHECKLIST_ID = checklist.id
|
||||
|
||||
print(f"✅ Agregando preguntas al checklist: {checklist.name}")
|
||||
|
||||
# Definir todas las preguntas por sección
|
||||
|
||||
@@ -9,7 +9,6 @@ from datetime import datetime, timedelta
|
||||
|
||||
from app.core.database import engine, get_db, Base
|
||||
from app.core.security import verify_password, get_password_hash, create_access_token, decode_access_token
|
||||
from app.core.config import settings
|
||||
from app import models, schemas
|
||||
|
||||
# Crear tablas
|
||||
@@ -17,18 +16,15 @@ Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI(title="Checklist Inteligente API", version="1.0.0")
|
||||
|
||||
# CORS - Usar configuración de settings
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Log para debug
|
||||
print(f"🌐 CORS configured for origins: {settings.cors_origins}")
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
# Dependency para obtener usuario actual
|
||||
@@ -451,6 +447,428 @@ async def upload_photo(
|
||||
return media_file
|
||||
|
||||
|
||||
# ============= AI ANALYSIS =============
|
||||
@app.get("/api/ai/models", response_model=List[schemas.AIModelInfo])
|
||||
def get_available_ai_models(current_user: models.User = Depends(get_current_user)):
|
||||
"""Obtener lista de modelos de IA disponibles"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden ver modelos de IA")
|
||||
|
||||
models_list = [
|
||||
# OpenAI Models
|
||||
{
|
||||
"id": "gpt-4o",
|
||||
"name": "GPT-4o (Recomendado)",
|
||||
"provider": "openai",
|
||||
"description": "Modelo multimodal más avanzado de OpenAI, rápido y preciso para análisis de imágenes"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4o-mini",
|
||||
"name": "GPT-4o Mini",
|
||||
"provider": "openai",
|
||||
"description": "Versión compacta y económica de GPT-4o, ideal para análisis rápidos"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-turbo",
|
||||
"name": "GPT-4 Turbo",
|
||||
"provider": "openai",
|
||||
"description": "Modelo potente con capacidades de visión y contexto amplio"
|
||||
},
|
||||
{
|
||||
"id": "gpt-4-vision-preview",
|
||||
"name": "GPT-4 Vision (Preview)",
|
||||
"provider": "openai",
|
||||
"description": "Modelo específico para análisis de imágenes (versión previa)"
|
||||
},
|
||||
# Gemini Models - Actualizados a versiones 2.0, 2.5 y 3.0
|
||||
{
|
||||
"id": "gemini-3-pro-preview",
|
||||
"name": "Gemini 3 Pro Preview (Último)",
|
||||
"provider": "gemini",
|
||||
"description": "Modelo de próxima generación en preview, máxima capacidad de análisis"
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-pro",
|
||||
"name": "Gemini 2.5 Pro (Recomendado)",
|
||||
"provider": "gemini",
|
||||
"description": "Último modelo estable con excelente análisis visual y razonamiento avanzado"
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-flash",
|
||||
"name": "Gemini 2.5 Flash",
|
||||
"provider": "gemini",
|
||||
"description": "Versión rápida del 2.5, ideal para inspecciones en tiempo real"
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash",
|
||||
"name": "Gemini 2.0 Flash",
|
||||
"provider": "gemini",
|
||||
"description": "Modelo rápido y eficiente de la generación 2.0"
|
||||
},
|
||||
{
|
||||
"id": "gemini-1.5-pro-latest",
|
||||
"name": "Gemini 1.5 Pro Latest",
|
||||
"provider": "gemini",
|
||||
"description": "Versión estable 1.5 con contexto de 2M tokens"
|
||||
},
|
||||
{
|
||||
"id": "gemini-1.5-flash-latest",
|
||||
"name": "Gemini 1.5 Flash Latest",
|
||||
"provider": "gemini",
|
||||
"description": "Modelo 1.5 rápido para análisis básicos"
|
||||
}
|
||||
]
|
||||
|
||||
return models_list
|
||||
|
||||
|
||||
@app.get("/api/ai/configuration", response_model=schemas.AIConfiguration)
|
||||
def get_ai_configuration(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""Obtener configuración de IA actual"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden ver configuración de IA")
|
||||
|
||||
config = db.query(models.AIConfiguration).filter(
|
||||
models.AIConfiguration.is_active == True
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="No hay configuración de IA activa")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@app.post("/api/ai/configuration", response_model=schemas.AIConfiguration)
|
||||
def create_ai_configuration(
|
||||
config: schemas.AIConfigurationCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""Crear o actualizar configuración de IA"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden configurar IA")
|
||||
|
||||
# Desactivar configuraciones anteriores
|
||||
db.query(models.AIConfiguration).update({"is_active": False})
|
||||
|
||||
# Crear nueva configuración
|
||||
new_config = models.AIConfiguration(
|
||||
provider=config.provider,
|
||||
api_key=config.api_key,
|
||||
model_name=config.model_name,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(new_config)
|
||||
db.commit()
|
||||
db.refresh(new_config)
|
||||
|
||||
return new_config
|
||||
|
||||
|
||||
@app.put("/api/ai/configuration/{config_id}", response_model=schemas.AIConfiguration)
|
||||
def update_ai_configuration(
|
||||
config_id: int,
|
||||
config_update: schemas.AIConfigurationUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""Actualizar configuración de IA existente"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden actualizar configuración de IA")
|
||||
|
||||
config = db.query(models.AIConfiguration).filter(
|
||||
models.AIConfiguration.id == config_id
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuración no encontrada")
|
||||
|
||||
# Actualizar campos
|
||||
for key, value in config_update.dict(exclude_unset=True).items():
|
||||
setattr(config, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@app.delete("/api/ai/configuration/{config_id}")
|
||||
def delete_ai_configuration(
|
||||
config_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""Eliminar configuración de IA"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="Solo administradores pueden eliminar configuración de IA")
|
||||
|
||||
config = db.query(models.AIConfiguration).filter(
|
||||
models.AIConfiguration.id == config_id
|
||||
).first()
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuración no encontrada")
|
||||
|
||||
db.delete(config)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Configuración eliminada correctamente"}
|
||||
|
||||
|
||||
@app.post("/api/analyze-image")
|
||||
async def analyze_image(
|
||||
file: UploadFile = File(...),
|
||||
question_id: int = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: models.User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Analiza una imagen usando IA para sugerir respuestas
|
||||
Usa la configuración de IA activa (OpenAI o Gemini)
|
||||
"""
|
||||
# Obtener configuración de IA activa
|
||||
ai_config = db.query(models.AIConfiguration).filter(
|
||||
models.AIConfiguration.is_active == True
|
||||
).first()
|
||||
|
||||
if not ai_config:
|
||||
return {
|
||||
"status": "disabled",
|
||||
"message": "No hay configuración de IA activa. Configure en Settings."
|
||||
}
|
||||
|
||||
# Guardar imagen temporalmente
|
||||
import base64
|
||||
|
||||
contents = await file.read()
|
||||
image_b64 = base64.b64encode(contents).decode('utf-8')
|
||||
|
||||
# Obtener contexto de la pregunta si se proporciona
|
||||
question_obj = None
|
||||
if question_id:
|
||||
question_obj = db.query(models.Question).filter(models.Question.id == question_id).first()
|
||||
|
||||
try:
|
||||
# Construir prompt dinámico basado en la pregunta específica
|
||||
if question_obj:
|
||||
# Prompt altamente específico para la pregunta
|
||||
question_text = question_obj.text
|
||||
question_type = question_obj.type
|
||||
section = question_obj.section
|
||||
|
||||
system_prompt = f"""Eres un mecánico experto realizando una inspección vehicular.
|
||||
|
||||
PREGUNTA ESPECÍFICA A RESPONDER: "{question_text}"
|
||||
Sección: {section}
|
||||
|
||||
Analiza la imagen ÚNICAMENTE para responder esta pregunta específica.
|
||||
Sé directo y enfócate solo en lo que la pregunta solicita.
|
||||
|
||||
Responde en formato JSON:
|
||||
{{
|
||||
"status": "ok|minor|critical",
|
||||
"observations": "Respuesta específica a: {question_text}",
|
||||
"recommendation": "Acción si aplica",
|
||||
"confidence": 0.0-1.0
|
||||
}}
|
||||
|
||||
IMPORTANTE:
|
||||
- Responde SOLO lo que la pregunta pide
|
||||
- No des información genérica del vehículo
|
||||
- Sé específico y técnico
|
||||
- Si la pregunta es pass/fail, indica claramente si pasa o falla
|
||||
- Si la pregunta es bueno/regular/malo, indica el estado específico del componente"""
|
||||
|
||||
user_message = f"Inspecciona la imagen y responde específicamente: {question_text}"
|
||||
else:
|
||||
# Fallback para análisis general
|
||||
system_prompt = """Eres un experto mecánico automotriz. Analiza la imagen y proporciona:
|
||||
1. Estado del componente (bueno/regular/malo)
|
||||
2. Nivel de criticidad (ok/minor/critical)
|
||||
3. Observaciones técnicas breves
|
||||
4. Recomendación de acción
|
||||
|
||||
Responde en formato JSON:
|
||||
{
|
||||
"status": "ok|minor|critical",
|
||||
"observations": "descripción técnica",
|
||||
"recommendation": "acción sugerida",
|
||||
"confidence": 0.0-1.0
|
||||
}"""
|
||||
user_message = "Analiza este componente del vehículo para la inspección general."
|
||||
|
||||
if ai_config.provider == "openai":
|
||||
import openai
|
||||
openai.api_key = ai_config.api_key
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model=ai_config.model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": user_message
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
ai_response = response.choices[0].message.content
|
||||
|
||||
elif ai_config.provider == "gemini":
|
||||
import google.generativeai as genai
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
genai.configure(api_key=ai_config.api_key)
|
||||
model = genai.GenerativeModel(ai_config.model_name)
|
||||
|
||||
# Convertir base64 a imagen PIL
|
||||
image = Image.open(BytesIO(contents))
|
||||
|
||||
prompt = f"{system_prompt}\n\n{user_message}"
|
||||
response = model.generate_content([prompt, image])
|
||||
ai_response = response.text
|
||||
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Provider {ai_config.provider} no soportado"
|
||||
}
|
||||
|
||||
# Intentar parsear como JSON, si falla, usar texto plano
|
||||
try:
|
||||
import json
|
||||
import re
|
||||
|
||||
# Limpiar markdown code blocks si existen
|
||||
cleaned_response = ai_response.strip()
|
||||
|
||||
# Remover ```json ... ``` si existe
|
||||
if cleaned_response.startswith('```'):
|
||||
# Extraer contenido entre ``` markers
|
||||
match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', cleaned_response, re.DOTALL)
|
||||
if match:
|
||||
cleaned_response = match.group(1).strip()
|
||||
|
||||
analysis = json.loads(cleaned_response)
|
||||
except:
|
||||
# Si no es JSON válido, crear estructura básica
|
||||
analysis = {
|
||||
"status": "ok",
|
||||
"observations": ai_response,
|
||||
"recommendation": "Revisar manualmente",
|
||||
"confidence": 0.7
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"analysis": analysis,
|
||||
"raw_response": ai_response,
|
||||
"model": ai_config.model_name,
|
||||
"provider": ai_config.provider
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error en análisis AI: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "Error analyzing image with AI. Please check AI configuration in Settings."
|
||||
}
|
||||
|
||||
try:
|
||||
import openai
|
||||
openai.api_key = settings.OPENAI_API_KEY
|
||||
|
||||
# Prompt especializado para inspección vehicular
|
||||
system_prompt = """Eres un experto mecánico automotriz. Analiza la imagen y proporciona:
|
||||
1. Estado del componente (bueno/regular/malo)
|
||||
2. Nivel de criticidad (ok/minor/critical)
|
||||
3. Observaciones técnicas breves
|
||||
4. Recomendación de acción
|
||||
|
||||
Responde en formato JSON:
|
||||
{
|
||||
"status": "ok|minor|critical",
|
||||
"observations": "descripción técnica",
|
||||
"recommendation": "acción sugerida",
|
||||
"confidence": 0.0-1.0
|
||||
}"""
|
||||
|
||||
response = openai.ChatCompletion.create(
|
||||
model="gpt-4-vision-preview" if "gpt-4" in str(settings.OPENAI_API_KEY) else "gpt-4o",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": system_prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Analiza este componente del vehículo.\n{question_context}"
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": f"data:image/jpeg;base64,{image_b64}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
ai_response = response.choices[0].message.content
|
||||
|
||||
# Intentar parsear como JSON, si falla, usar texto plano
|
||||
try:
|
||||
import json
|
||||
analysis = json.loads(ai_response)
|
||||
except:
|
||||
# Si no es JSON válido, crear estructura básica
|
||||
analysis = {
|
||||
"status": "ok",
|
||||
"observations": ai_response,
|
||||
"recommendation": "Revisar manualmente",
|
||||
"confidence": 0.7
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"analysis": analysis,
|
||||
"raw_response": ai_response,
|
||||
"model": "gpt-4-vision"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error en análisis AI: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "Error analyzing image with AI"
|
||||
}
|
||||
|
||||
|
||||
# ============= HEALTH CHECK =============
|
||||
@app.get("/")
|
||||
def root():
|
||||
|
||||
@@ -144,3 +144,15 @@ class MediaFile(Base):
|
||||
|
||||
# Relationships
|
||||
answer = relationship("Answer", back_populates="media_files")
|
||||
|
||||
|
||||
class AIConfiguration(Base):
|
||||
__tablename__ = "ai_configurations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
provider = Column(String(50), nullable=False) # openai, gemini
|
||||
api_key = Column(Text, nullable=False)
|
||||
model_name = Column(String(100), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
14
backend/app/models_ai.py
Normal file
14
backend/app/models_ai.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
|
||||
class AIConfiguration(Base):
|
||||
__tablename__ = "ai_configurations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
provider = Column(String(50), nullable=False) # openai, gemini
|
||||
api_key = Column(Text, nullable=False)
|
||||
model_name = Column(String(100), nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
@@ -175,3 +175,33 @@ class InspectionDetail(Inspection):
|
||||
checklist: ChecklistWithQuestions
|
||||
mechanic: User
|
||||
answers: List[AnswerWithMedia] = []
|
||||
|
||||
|
||||
# AI Configuration Schemas
|
||||
class AIConfigurationBase(BaseModel):
|
||||
provider: str # openai, gemini
|
||||
api_key: str
|
||||
model_name: str
|
||||
|
||||
class AIConfigurationCreate(AIConfigurationBase):
|
||||
pass
|
||||
|
||||
class AIConfigurationUpdate(BaseModel):
|
||||
provider: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
model_name: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class AIConfiguration(AIConfigurationBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class AIModelInfo(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
provider: str
|
||||
description: Optional[str] = None
|
||||
|
||||
61
backend/init_db.py
Normal file
61
backend/init_db.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script para inicializar la base de datos con usuarios de prueba
|
||||
"""
|
||||
from app.core.database import SessionLocal, engine, Base
|
||||
from app.core.security import get_password_hash
|
||||
from app.models import User
|
||||
from sqlalchemy import text
|
||||
|
||||
def init_db():
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Verificar conexión
|
||||
db.execute(text("SELECT 1"))
|
||||
print("✓ Conexión a la base de datos exitosa")
|
||||
|
||||
# Verificar si ya existen usuarios
|
||||
existing_users = db.query(User).count()
|
||||
if existing_users > 0:
|
||||
print(f"⚠ Ya existen {existing_users} usuario(s) en la base de datos")
|
||||
return
|
||||
|
||||
# Crear usuario administrador
|
||||
admin = User(
|
||||
username="admin",
|
||||
email="admin@checklist.com",
|
||||
full_name="Administrador",
|
||||
password_hash=get_password_hash("admin123"),
|
||||
role="admin",
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
|
||||
# Crear usuario mecánico de prueba
|
||||
mechanic = User(
|
||||
username="mechanic",
|
||||
email="mechanic@checklist.com",
|
||||
full_name="Mecánico de Prueba",
|
||||
password_hash=get_password_hash("mechanic123"),
|
||||
role="mechanic",
|
||||
is_active=True
|
||||
)
|
||||
db.add(mechanic)
|
||||
|
||||
db.commit()
|
||||
|
||||
print("✓ Usuarios creados exitosamente:")
|
||||
print(" - Admin: username='admin', password='admin123'")
|
||||
print(" - Mechanic: username='mechanic', password='mechanic123'")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error al inicializar la base de datos: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Inicializando base de datos...")
|
||||
init_db()
|
||||
print("¡Inicialización completada!")
|
||||
@@ -11,6 +11,7 @@ passlib==1.7.4
|
||||
bcrypt==4.0.1
|
||||
python-multipart==0.0.6
|
||||
openai==1.10.0
|
||||
google-generativeai==0.3.2
|
||||
Pillow==10.2.0
|
||||
reportlab==4.0.9
|
||||
python-dotenv==1.0.0
|
||||
|
||||
87
build-and-push.ps1
Normal file
87
build-and-push.ps1
Normal file
@@ -0,0 +1,87 @@
|
||||
# Script PowerShell para construir y publicar imagenes de Syntria a Docker Hub
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "🚀 Syntria - Build & Push to Docker Hub" -ForegroundColor Green
|
||||
Write-Host "==========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Verificar que estamos en el directorio correcto
|
||||
if (-not (Test-Path "docker-compose.yml")) {
|
||||
Write-Host "❌ Error: Ejecuta este script desde el directorio raiz del proyecto" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Leer variables de entorno
|
||||
if (Test-Path ".env.production") {
|
||||
Get-Content ".env.production" | ForEach-Object {
|
||||
if ($_ -match "^([^#][^=]+)=(.*)$") {
|
||||
Set-Item -Path "env:$($matches[1])" -Value $matches[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verificar Docker Hub username
|
||||
if (-not $env:DOCKER_USERNAME) {
|
||||
$env:DOCKER_USERNAME = Read-Host "Ingresa tu username de Docker Hub"
|
||||
}
|
||||
|
||||
# Verificar version
|
||||
$version = Read-Host "Version a construir (default: latest)"
|
||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
||||
$version = "latest"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Configuracion:" -ForegroundColor Green
|
||||
Write-Host " Docker Hub User: $env:DOCKER_USERNAME"
|
||||
Write-Host " Version: $version"
|
||||
Write-Host ""
|
||||
|
||||
# Confirmar
|
||||
$confirm = Read-Host "Continuar? (y/n)"
|
||||
if ($confirm -ne "y") {
|
||||
Write-Host "Cancelado" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Login a Docker Hub
|
||||
Write-Host ""
|
||||
Write-Host "📦 Iniciando sesion en Docker Hub..." -ForegroundColor Green
|
||||
docker login
|
||||
|
||||
# Build Backend
|
||||
Write-Host ""
|
||||
Write-Host "🔨 Construyendo Backend..." -ForegroundColor Green
|
||||
docker build -t "$env:DOCKER_USERNAME/syntria-backend:$version" -f backend/Dockerfile.prod backend/
|
||||
docker tag "$env:DOCKER_USERNAME/syntria-backend:$version" "$env:DOCKER_USERNAME/syntria-backend:latest"
|
||||
|
||||
# Build Frontend
|
||||
Write-Host ""
|
||||
Write-Host "🔨 Construyendo Frontend..." -ForegroundColor Green
|
||||
docker build -t "$env:DOCKER_USERNAME/syntria-frontend:$version" -f frontend/Dockerfile.prod frontend/
|
||||
docker tag "$env:DOCKER_USERNAME/syntria-frontend:$version" "$env:DOCKER_USERNAME/syntria-frontend:latest"
|
||||
|
||||
# Push images
|
||||
Write-Host ""
|
||||
Write-Host "⬆️ Subiendo imagenes a Docker Hub..." -ForegroundColor Green
|
||||
docker push "$env:DOCKER_USERNAME/syntria-backend:$version"
|
||||
docker push "$env:DOCKER_USERNAME/syntria-backend:latest"
|
||||
docker push "$env:DOCKER_USERNAME/syntria-frontend:$version"
|
||||
docker push "$env:DOCKER_USERNAME/syntria-frontend:latest"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✅ Imagenes publicadas exitosamente!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Imagenes disponibles:"
|
||||
Write-Host " 🐳 $env:DOCKER_USERNAME/syntria-backend:$version"
|
||||
Write-Host " 🐳 $env:DOCKER_USERNAME/syntria-backend:latest"
|
||||
Write-Host " 🐳 $env:DOCKER_USERNAME/syntria-frontend:$version"
|
||||
Write-Host " 🐳 $env:DOCKER_USERNAME/syntria-frontend:latest"
|
||||
Write-Host ""
|
||||
Write-Host "Proximos pasos:" -ForegroundColor Yellow
|
||||
Write-Host "1. Actualiza DOCKER_USERNAME en .env.production"
|
||||
Write-Host "2. En tu servidor de produccion, ejecuta:"
|
||||
Write-Host " docker-compose -f docker-compose.prod.yml --env-file .env.production up -d"
|
||||
|
||||
|
||||
88
build-and-push.sh
Normal file
88
build-and-push.sh
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script para construir y publicar imágenes de Syntria a Docker Hub
|
||||
|
||||
set -e
|
||||
|
||||
# Colores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}🚀 Syntria - Build & Push to Docker Hub${NC}"
|
||||
echo "=========================================="
|
||||
|
||||
# Verificar que estamos en el directorio correcto
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
echo -e "${RED}❌ Error: Ejecuta este script desde el directorio raíz del proyecto${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Leer variables de entorno
|
||||
if [ -f ".env.production" ]; then
|
||||
export $(cat .env.production | grep -v '^#' | xargs)
|
||||
fi
|
||||
|
||||
# Verificar Docker Hub username
|
||||
if [ -z "$DOCKER_USERNAME" ]; then
|
||||
echo -e "${YELLOW}⚠️ DOCKER_USERNAME no está configurado en .env.production${NC}"
|
||||
read -p "Ingresa tu username de Docker Hub: " DOCKER_USERNAME
|
||||
fi
|
||||
|
||||
# Verificar versión
|
||||
read -p "Versión a construir (default: latest): " VERSION
|
||||
VERSION=${VERSION:-latest}
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Configuración:${NC}"
|
||||
echo " Docker Hub User: $DOCKER_USERNAME"
|
||||
echo " Versión: $VERSION"
|
||||
echo ""
|
||||
|
||||
# Confirmar
|
||||
read -p "¿Continuar? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}Cancelado${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Login a Docker Hub
|
||||
echo ""
|
||||
echo -e "${GREEN}📦 Iniciando sesión en Docker Hub...${NC}"
|
||||
docker login
|
||||
|
||||
# Build Backend
|
||||
echo ""
|
||||
echo -e "${GREEN}🔨 Construyendo Backend...${NC}"
|
||||
docker build -t $DOCKER_USERNAME/syntria-backend:$VERSION -f backend/Dockerfile.prod backend/
|
||||
docker tag $DOCKER_USERNAME/syntria-backend:$VERSION $DOCKER_USERNAME/syntria-backend:latest
|
||||
|
||||
# Build Frontend
|
||||
echo ""
|
||||
echo -e "${GREEN}🔨 Construyendo Frontend...${NC}"
|
||||
docker build -t $DOCKER_USERNAME/syntria-frontend:$VERSION -f frontend/Dockerfile.prod frontend/
|
||||
docker tag $DOCKER_USERNAME/syntria-frontend:$VERSION $DOCKER_USERNAME/syntria-frontend:latest
|
||||
|
||||
# Push images
|
||||
echo ""
|
||||
echo -e "${GREEN}⬆️ Subiendo imágenes a Docker Hub...${NC}"
|
||||
docker push $DOCKER_USERNAME/syntria-backend:$VERSION
|
||||
docker push $DOCKER_USERNAME/syntria-backend:latest
|
||||
docker push $DOCKER_USERNAME/syntria-frontend:$VERSION
|
||||
docker push $DOCKER_USERNAME/syntria-frontend:latest
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ ¡Imágenes publicadas exitosamente!${NC}"
|
||||
echo ""
|
||||
echo "Imágenes disponibles:"
|
||||
echo " 🐳 $DOCKER_USERNAME/syntria-backend:$VERSION"
|
||||
echo " 🐳 $DOCKER_USERNAME/syntria-backend:latest"
|
||||
echo " 🐳 $DOCKER_USERNAME/syntria-frontend:$VERSION"
|
||||
echo " 🐳 $DOCKER_USERNAME/syntria-frontend:latest"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Próximos pasos:${NC}"
|
||||
echo "1. Actualiza DOCKER_USERNAME en .env.production"
|
||||
echo "2. En tu servidor de producción, ejecuta:"
|
||||
echo " docker-compose -f docker-compose.prod.yml --env-file .env.production up -d"
|
||||
56
docker-compose.hub.yml
Normal file
56
docker-compose.hub.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: syntria-db-prod
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: syntria_db
|
||||
POSTGRES_USER: syntria_user
|
||||
POSTGRES_PASSWORD: syntria_secure_2024
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- syntria-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U syntria_user -d syntria_db"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
image: dymai/syntria-backend:latest
|
||||
container_name: syntria-backend-prod
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://syntria_user:syntria_secure_2024@db:5432/syntria_db
|
||||
SECRET_KEY: tu_clave_secreta_super_segura_minimo_32_caracteres_prod
|
||||
OPENAI_API_KEY: tu_api_key_de_openai
|
||||
GEMINI_API_KEY: tu_api_key_de_gemini
|
||||
ENVIRONMENT: production
|
||||
ALLOWED_ORIGINS: http://localhost,http://localhost:80
|
||||
networks:
|
||||
- syntria-network
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
|
||||
frontend:
|
||||
image: dymai/syntria-frontend:latest
|
||||
container_name: syntria-frontend-prod
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "80:80"
|
||||
networks:
|
||||
- syntria-network
|
||||
|
||||
networks:
|
||||
syntria-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -1,64 +1,63 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: checklist-db-prod
|
||||
container_name: syntria-db-prod
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-checklist_db}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-checklist_user}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-syntria_db}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-syntria_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-checklist_user}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-syntria_user} -d ${POSTGRES_DB:-syntria_db}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: always
|
||||
networks:
|
||||
- syntria-network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: checklist-backend-prod
|
||||
image: ${DOCKER_USERNAME}/syntria-backend:latest
|
||||
container_name: syntria-backend-prod
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-checklist_user}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-checklist_db}
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-syntria_user}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-syntria_db}
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ENVIRONMENT: production
|
||||
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
|
||||
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost,http://localhost:5173}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- uploads_data:/app/uploads
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
networks:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
- syntria-network
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.prod
|
||||
args:
|
||||
VITE_API_URL: ${VITE_API_URL}
|
||||
container_name: checklist-frontend-prod
|
||||
image: ${DOCKER_USERNAME}/syntria-frontend:latest
|
||||
container_name: syntria-frontend-prod
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
- VITE_API_URL=${API_URL:-http://localhost:8000}
|
||||
restart: always
|
||||
networks:
|
||||
- syntria-network
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
uploads_data:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
syntria-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: checklist-db
|
||||
environment:
|
||||
POSTGRES_DB: checklist_db
|
||||
POSTGRES_USER: checklist_user
|
||||
POSTGRES_PASSWORD: checklist_pass_2024
|
||||
POSTGRES_DB: ${POSTGRES_DB:-checklist_db}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-checklist_user}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-checklist_pass_2024}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U checklist_user"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-checklist_user} -d ${POSTGRES_DB:-checklist_db}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -22,10 +21,10 @@ services:
|
||||
build: ./backend
|
||||
container_name: checklist-backend
|
||||
environment:
|
||||
DATABASE_URL: postgresql://checklist_user:checklist_pass_2024@postgres:5432/checklist_db
|
||||
DATABASE_URL: postgresql://${POSTGRES_USER:-checklist_user}:${POSTGRES_PASSWORD:-checklist_pass_2024}@postgres:5432/${POSTGRES_DB:-checklist_db}
|
||||
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production-min-32-chars}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
ENVIRONMENT: development
|
||||
ENVIRONMENT: ${ENVIRONMENT:-development}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
|
||||
99
docker-stack.yml
Normal file
99
docker-stack.yml
Normal file
@@ -0,0 +1,99 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: syntria_db
|
||||
POSTGRES_USER: syntria_user
|
||||
POSTGRES_PASSWORD: syntria_secure_2024
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- syntria_network
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints: [node.role == manager]
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U syntria_user -d syntria_db"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
image: dymai/syntria-backend:latest
|
||||
environment:
|
||||
DATABASE_URL: postgresql://syntria_user:syntria_secure_2024@db:5432/syntria_db
|
||||
SECRET_KEY: tu_clave_secreta_super_segura_minimo_32_caracteres_prod
|
||||
OPENAI_API_KEY: tu_api_key_de_openai
|
||||
GEMINI_API_KEY: tu_api_key_de_gemini
|
||||
ENVIRONMENT: production
|
||||
ALLOWED_ORIGINS: http://localhost,https://syntria.tudominio.com
|
||||
networks:
|
||||
- syntria_network
|
||||
- network_public
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 2
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=network_public"
|
||||
- "traefik.http.routers.syntria-api.rule=Host(`syntria.tudominio.com`) && PathPrefix(`/api`)"
|
||||
- "traefik.http.routers.syntria-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.syntria-api.tls.certresolver=letsencryptresolver"
|
||||
- "traefik.http.routers.syntria-api.service=syntria-api"
|
||||
- "traefik.http.services.syntria-api.loadbalancer.server.port=8000"
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||
|
||||
frontend:
|
||||
image: dymai/syntria-frontend:latest
|
||||
networks:
|
||||
- syntria_network
|
||||
- network_public
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 2
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
order: start-first
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=network_public"
|
||||
- "traefik.http.routers.syntria-web.rule=Host(`syntria.tudominio.com`)"
|
||||
- "traefik.http.routers.syntria-web.entrypoints=websecure"
|
||||
- "traefik.http.routers.syntria-web.priority=1"
|
||||
- "traefik.http.routers.syntria-web.tls.certresolver=letsencryptresolver"
|
||||
- "traefik.http.routers.syntria-web.service=syntria-web"
|
||||
- "traefik.http.services.syntria-web.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
syntria_network:
|
||||
driver: overlay
|
||||
attachable: true
|
||||
network_public:
|
||||
external: true
|
||||
attachable: true
|
||||
name: network_public
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
16
frontend/.dockerignore
Normal file
16
frontend/.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
||||
# Frontend .dockerignore
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
Dockerfile.prod
|
||||
README.md
|
||||
.env
|
||||
.env.*
|
||||
*.md
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
@@ -1,3 +1,4 @@
|
||||
# Build stage
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
@@ -5,26 +6,19 @@ WORKDIR /app
|
||||
# Copiar package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Instalar todas las dependencias (necesitamos las dev para el build)
|
||||
# Instalar dependencias
|
||||
RUN npm install
|
||||
|
||||
# Copiar código
|
||||
# Copiar código fuente
|
||||
COPY . .
|
||||
|
||||
# Build argument para la URL de la API
|
||||
ARG VITE_API_URL=http://checklist-rons-0e8a3a-63dbc4-72-61-106-199.traefik.me
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
|
||||
# Mostrar la URL que se está usando (para debug)
|
||||
RUN echo "Building with VITE_API_URL=${VITE_API_URL}"
|
||||
|
||||
# Construir la aplicación
|
||||
# Build de producción
|
||||
RUN npm run build
|
||||
|
||||
# Etapa de producción con Nginx
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copiar archivos construidos
|
||||
# Copiar build al nginx
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copiar configuración de nginx
|
||||
@@ -33,5 +27,5 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
# Exponer puerto 80
|
||||
EXPOSE 80
|
||||
|
||||
# Comando para iniciar nginx
|
||||
# Comando por defecto
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Checklist Inteligente - Sistema de Inspecciones</title>
|
||||
<title>Syntria - Sistema Inteligente de Inspecciones</title>
|
||||
<meta name="description" content="Syntria: Sistema avanzado de inspecciones vehiculares con inteligencia artificial" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
@@ -11,24 +11,30 @@ server {
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
||||
|
||||
# SPA routing - todas las rutas van a index.html
|
||||
# Proxy al backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
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_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache para assets estáticos
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# No cache para index.html
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
1036
frontend/src/App.jsx
1036
frontend/src/App.jsx
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,19 @@
|
||||
export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) {
|
||||
return (
|
||||
<aside className={`bg-gray-900 text-white transition-all duration-300 ${sidebarOpen ? 'w-64' : 'w-16'} flex flex-col fixed h-full z-10`}>
|
||||
<aside className={`bg-gradient-to-b from-gray-900 via-indigo-950 to-purple-950 text-white transition-all duration-300 ${sidebarOpen ? 'w-64' : 'w-16'} flex flex-col fixed h-full z-10 shadow-2xl`}>
|
||||
{/* Sidebar Header */}
|
||||
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-gray-700`}>
|
||||
{sidebarOpen && <h2 className="text-xl font-bold">Sistema</h2>}
|
||||
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-indigo-800/50`}>
|
||||
{sidebarOpen && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">S</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Syntria</h2>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-2 rounded-lg hover:bg-gray-800 transition"
|
||||
className="p-2 rounded-lg hover:bg-indigo-800/50 transition"
|
||||
title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'}
|
||||
>
|
||||
{sidebarOpen ? '☰' : '☰'}
|
||||
@@ -21,8 +28,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
onClick={() => setActiveTab('checklists')}
|
||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||
activeTab === 'checklists'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-800'
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||
}`}
|
||||
title={!sidebarOpen ? 'Checklists' : ''}
|
||||
>
|
||||
@@ -35,8 +42,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
onClick={() => setActiveTab('inspections')}
|
||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||
activeTab === 'inspections'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-800'
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||
}`}
|
||||
title={!sidebarOpen ? 'Inspecciones' : ''}
|
||||
>
|
||||
@@ -51,8 +58,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
onClick={() => setActiveTab('users')}
|
||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||
activeTab === 'users'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-800'
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||
}`}
|
||||
title={!sidebarOpen ? 'Usuarios' : ''}
|
||||
>
|
||||
@@ -65,8 +72,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
onClick={() => setActiveTab('reports')}
|
||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||
activeTab === 'reports'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-800'
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||
}`}
|
||||
title={!sidebarOpen ? 'Reportes' : ''}
|
||||
>
|
||||
@@ -74,27 +81,41 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
{sidebarOpen && <span>Reportes</span>}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
onClick={() => setActiveTab('settings')}
|
||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||
activeTab === 'settings'
|
||||
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||
}`}
|
||||
title={!sidebarOpen ? 'Configuración' : ''}
|
||||
>
|
||||
<span className="text-xl">⚙️</span>
|
||||
{sidebarOpen && <span>Configuración</span>}
|
||||
</button>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="p-4 border-t border-gray-700">
|
||||
<div className="p-4 border-t border-indigo-800/50">
|
||||
<div className={`flex items-center gap-3 ${!sidebarOpen && 'justify-center'}`}>
|
||||
<div className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-white font-bold flex-shrink-0 shadow-lg">
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{user.full_name || user.username}</p>
|
||||
<p className="text-xs text-gray-400">{user.role === 'admin' ? 'Admin' : 'Mecánico'}</p>
|
||||
<p className="text-sm font-medium truncate text-white">{user.full_name || user.username}</p>
|
||||
<p className="text-xs text-indigo-300">{user.role === 'admin' ? '👑 Admin' : '🔧 Mecánico'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className={`mt-3 w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition flex items-center justify-center gap-2`}
|
||||
className={`mt-3 w-full px-4 py-2 bg-gradient-to-r from-red-500 to-pink-500 text-white rounded-lg hover:from-red-600 hover:to-pink-600 transition-all transform hover:scale-105 flex items-center justify-center gap-2 shadow-lg`}
|
||||
title={!sidebarOpen ? 'Cerrar Sesión' : ''}
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
|
||||
12
init-db.sh
Normal file
12
init-db.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
-- Verificar que la base de datos existe
|
||||
SELECT 'Database is ready!' as status;
|
||||
|
||||
-- Crear extensiones si son necesarias
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
EOSQL
|
||||
|
||||
echo "Database initialization completed successfully!"
|
||||
74
init_users.py
Normal file
74
init_users.py
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script para inicializar la base de datos de Syntria
|
||||
Ejecutar desde Portainer > Stack > Servicios > backend > Console
|
||||
"""
|
||||
|
||||
from app.core.database import SessionLocal, engine, Base
|
||||
from app.models import User
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
def init_database():
|
||||
"""Inicializar base de datos y crear usuarios"""
|
||||
|
||||
print("🗄️ Creando tablas...")
|
||||
# Crear todas las tablas
|
||||
Base.metadata.create_all(bind=engine)
|
||||
print("✅ Tablas creadas")
|
||||
|
||||
# Crear sesión
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Verificar si ya existen usuarios
|
||||
existing_users = db.query(User).count()
|
||||
if existing_users > 0:
|
||||
print(f"⚠️ Ya existen {existing_users} usuarios en la base de datos")
|
||||
response = input("¿Deseas crear usuarios de todas formas? (s/n): ")
|
||||
if response.lower() != 's':
|
||||
print("❌ Operación cancelada")
|
||||
return
|
||||
|
||||
print("\n👥 Creando usuarios...")
|
||||
|
||||
# Usuario Admin
|
||||
admin = User(
|
||||
username='admin',
|
||||
password_hash=get_password_hash('admin123'),
|
||||
role='admin',
|
||||
full_name='Administrador',
|
||||
email='admin@syntria.com',
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
print("✅ Usuario 'admin' creado (password: admin123)")
|
||||
|
||||
# Usuario Mecánico
|
||||
mecanico = User(
|
||||
username='mecanico',
|
||||
password_hash=get_password_hash('mec123'),
|
||||
role='mecanico',
|
||||
full_name='Mecánico',
|
||||
email='mecanico@syntria.com',
|
||||
is_active=True
|
||||
)
|
||||
db.add(mecanico)
|
||||
print("✅ Usuario 'mecanico' creado (password: mec123)")
|
||||
|
||||
# Guardar cambios
|
||||
db.commit()
|
||||
|
||||
print("\n✨ Base de datos inicializada correctamente!")
|
||||
print("\n📋 Usuarios creados:")
|
||||
print(" - admin / admin123 (Administrador)")
|
||||
print(" - mecanico / mec123 (Mecánico)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_database()
|
||||
Reference in New Issue
Block a user