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
|
# Secret key para JWT (mínimo 32 caracteres)
|
||||||
POSTGRES_DB=checklist_db
|
SECRET_KEY=CHANGE_THIS_TO_RANDOM_STRING_MIN_32_CHARS_PRODUCTION
|
||||||
POSTGRES_USER=checklist_user
|
|
||||||
POSTGRES_PASSWORD=CAMBIAR-PASSWORD-SUPER-SEGURA-AQUI
|
|
||||||
|
|
||||||
# Backend
|
# OpenAI API Key (opcional, para IA)
|
||||||
SECRET_KEY=CAMBIAR-CLAVE-SECRETA-MINIMO-32-CARACTERES-SUPER-SEGURA
|
OPENAI_API_KEY=
|
||||||
OPENAI_API_KEY=tu-openai-api-key-aqui
|
|
||||||
|
# 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
|
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
|
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.core.database import SessionLocal
|
||||||
from app.models import Checklist, Question
|
from app.models import Checklist, Question, User
|
||||||
|
|
||||||
# ID del checklist al que quieres agregar preguntas
|
|
||||||
CHECKLIST_ID = 2 # Cambia este número según el ID de tu checklist
|
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|
||||||
# Verificar que el checklist existe
|
# Obtener el usuario admin
|
||||||
checklist = db.query(Checklist).filter(Checklist.id == CHECKLIST_ID).first()
|
admin = db.query(User).filter(User.username == "admin").first()
|
||||||
if not checklist:
|
if not admin:
|
||||||
print(f"❌ Checklist con ID {CHECKLIST_ID} no encontrado")
|
print("❌ Usuario admin no encontrado")
|
||||||
exit(1)
|
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}")
|
print(f"✅ Agregando preguntas al checklist: {checklist.name}")
|
||||||
|
|
||||||
# Definir todas las preguntas por sección
|
# 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.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.security import verify_password, get_password_hash, create_access_token, decode_access_token
|
||||||
from app.core.config import settings
|
|
||||||
from app import models, schemas
|
from app import models, schemas
|
||||||
|
|
||||||
# Crear tablas
|
# Crear tablas
|
||||||
@@ -17,18 +16,15 @@ Base.metadata.create_all(bind=engine)
|
|||||||
|
|
||||||
app = FastAPI(title="Checklist Inteligente API", version="1.0.0")
|
app = FastAPI(title="Checklist Inteligente API", version="1.0.0")
|
||||||
|
|
||||||
# CORS - Usar configuración de settings
|
# CORS
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.cors_origins,
|
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Log para debug
|
|
||||||
print(f"🌐 CORS configured for origins: {settings.cors_origins}")
|
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|
||||||
# Dependency para obtener usuario actual
|
# Dependency para obtener usuario actual
|
||||||
@@ -451,6 +447,428 @@ async def upload_photo(
|
|||||||
return media_file
|
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 =============
|
# ============= HEALTH CHECK =============
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
|
|||||||
@@ -144,3 +144,15 @@ class MediaFile(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
answer = relationship("Answer", back_populates="media_files")
|
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
|
checklist: ChecklistWithQuestions
|
||||||
mechanic: User
|
mechanic: User
|
||||||
answers: List[AnswerWithMedia] = []
|
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
|
bcrypt==4.0.1
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
openai==1.10.0
|
openai==1.10.0
|
||||||
|
google-generativeai==0.3.2
|
||||||
Pillow==10.2.0
|
Pillow==10.2.0
|
||||||
reportlab==4.0.9
|
reportlab==4.0.9
|
||||||
python-dotenv==1.0.0
|
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:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: checklist-db-prod
|
container_name: syntria-db-prod
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-checklist_db}
|
POSTGRES_DB: ${POSTGRES_DB:-syntria_db}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-checklist_user}
|
POSTGRES_USER: ${POSTGRES_USER:-syntria_user}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
networks:
|
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||||
- app-network
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
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
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- syntria-network
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
image: ${DOCKER_USERNAME}/syntria-backend:latest
|
||||||
context: ./backend
|
container_name: syntria-backend-prod
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: checklist-backend-prod
|
|
||||||
environment:
|
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}
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
ENVIRONMENT: production
|
ENVIRONMENT: production
|
||||||
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
|
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost,http://localhost:5173}
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./uploads:/app/uploads
|
- uploads_data:/app/uploads
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- syntria-network
|
||||||
restart: unless-stopped
|
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
image: ${DOCKER_USERNAME}/syntria-frontend:latest
|
||||||
context: ./frontend
|
container_name: syntria-frontend-prod
|
||||||
dockerfile: Dockerfile.prod
|
|
||||||
args:
|
|
||||||
VITE_API_URL: ${VITE_API_URL}
|
|
||||||
container_name: checklist-frontend-prod
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=${API_URL:-http://localhost:8000}
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- syntria-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
uploads_data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
syntria-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: checklist-db
|
container_name: checklist-db
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: checklist_db
|
POSTGRES_DB: ${POSTGRES_DB:-checklist_db}
|
||||||
POSTGRES_USER: checklist_user
|
POSTGRES_USER: ${POSTGRES_USER:-checklist_user}
|
||||||
POSTGRES_PASSWORD: checklist_pass_2024
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-checklist_pass_2024}
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
|
||||||
healthcheck:
|
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
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -22,10 +21,10 @@ services:
|
|||||||
build: ./backend
|
build: ./backend
|
||||||
container_name: checklist-backend
|
container_name: checklist-backend
|
||||||
environment:
|
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}
|
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production-min-32-chars}
|
||||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
ENVIRONMENT: development
|
ENVIRONMENT: ${ENVIRONMENT:-development}
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
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
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -5,26 +6,19 @@ WORKDIR /app
|
|||||||
# Copiar package files
|
# Copiar package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Instalar todas las dependencias (necesitamos las dev para el build)
|
# Instalar dependencias
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copiar código
|
# Copiar código fuente
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build argument para la URL de la API
|
# Build de producción
|
||||||
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
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Etapa de producción con Nginx
|
# Production stage
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
# Copiar archivos construidos
|
# Copiar build al nginx
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# Copiar configuración de nginx
|
# Copiar configuración de nginx
|
||||||
@@ -33,5 +27,5 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|||||||
# Exponer puerto 80
|
# Exponer puerto 80
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
# Comando para iniciar nginx
|
# Comando por defecto
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
@@ -11,24 +11,30 @@ server {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
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 / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
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)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
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
|
# Security headers
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" 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 }) {
|
export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, setSidebarOpen, onLogout }) {
|
||||||
return (
|
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 */}
|
{/* Sidebar Header */}
|
||||||
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-gray-700`}>
|
<div className={`p-4 flex items-center ${sidebarOpen ? 'justify-between' : 'justify-center'} border-b border-indigo-800/50`}>
|
||||||
{sidebarOpen && <h2 className="text-xl font-bold">Sistema</h2>}
|
{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
|
<button
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
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'}
|
title={sidebarOpen ? 'Ocultar sidebar' : 'Mostrar sidebar'}
|
||||||
>
|
>
|
||||||
{sidebarOpen ? '☰' : '☰'}
|
{sidebarOpen ? '☰' : '☰'}
|
||||||
@@ -21,8 +28,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
onClick={() => setActiveTab('checklists')}
|
onClick={() => setActiveTab('checklists')}
|
||||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
activeTab === 'checklists'
|
activeTab === 'checklists'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||||
: 'text-gray-300 hover:bg-gray-800'
|
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||||
}`}
|
}`}
|
||||||
title={!sidebarOpen ? 'Checklists' : ''}
|
title={!sidebarOpen ? 'Checklists' : ''}
|
||||||
>
|
>
|
||||||
@@ -35,8 +42,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
onClick={() => setActiveTab('inspections')}
|
onClick={() => setActiveTab('inspections')}
|
||||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
activeTab === 'inspections'
|
activeTab === 'inspections'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||||
: 'text-gray-300 hover:bg-gray-800'
|
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||||
}`}
|
}`}
|
||||||
title={!sidebarOpen ? 'Inspecciones' : ''}
|
title={!sidebarOpen ? 'Inspecciones' : ''}
|
||||||
>
|
>
|
||||||
@@ -51,8 +58,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
onClick={() => setActiveTab('users')}
|
onClick={() => setActiveTab('users')}
|
||||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
activeTab === 'users'
|
activeTab === 'users'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||||
: 'text-gray-300 hover:bg-gray-800'
|
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||||
}`}
|
}`}
|
||||||
title={!sidebarOpen ? 'Usuarios' : ''}
|
title={!sidebarOpen ? 'Usuarios' : ''}
|
||||||
>
|
>
|
||||||
@@ -65,8 +72,8 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
onClick={() => setActiveTab('reports')}
|
onClick={() => setActiveTab('reports')}
|
||||||
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
className={`w-full flex items-center ${sidebarOpen ? 'gap-3 px-4' : 'justify-center px-2'} py-3 rounded-lg transition ${
|
||||||
activeTab === 'reports'
|
activeTab === 'reports'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg'
|
||||||
: 'text-gray-300 hover:bg-gray-800'
|
: 'text-indigo-200 hover:bg-indigo-900/50'
|
||||||
}`}
|
}`}
|
||||||
title={!sidebarOpen ? 'Reportes' : ''}
|
title={!sidebarOpen ? 'Reportes' : ''}
|
||||||
>
|
>
|
||||||
@@ -74,27 +81,41 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
|||||||
{sidebarOpen && <span>Reportes</span>}
|
{sidebarOpen && <span>Reportes</span>}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* User Info */}
|
{/* 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={`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()}
|
{user.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
{sidebarOpen && (
|
{sidebarOpen && (
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium truncate">{user.full_name || user.username}</p>
|
<p className="text-sm font-medium truncate text-white">{user.full_name || user.username}</p>
|
||||||
<p className="text-xs text-gray-400">{user.role === 'admin' ? 'Admin' : 'Mecánico'}</p>
|
<p className="text-xs text-indigo-300">{user.role === 'admin' ? '👑 Admin' : '🔧 Mecánico'}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onLogout}
|
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' : ''}
|
title={!sidebarOpen ? 'Cerrar Sesión' : ''}
|
||||||
>
|
>
|
||||||
{sidebarOpen ? (
|
{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