changes: TODO: Remove this config files from tracking
This commit is contained in:
439
cp_draft_image.md
Normal file
439
cp_draft_image.md
Normal file
@@ -0,0 +1,439 @@
|
||||
# Plan: Path-based Image Versioning para Captive Portal
|
||||
|
||||
## Problema Identificado
|
||||
|
||||
Las imágenes eliminadas en modo preview/draft se borran permanentemente de S3/MinIO, rompiendo la configuración productiva que aún las referencia.
|
||||
|
||||
### Causa Raíz
|
||||
- **Configuración**: Tiene separación entre draft (`Configuration`) y production (`ProdConfiguration`)
|
||||
- **Imágenes**: NO tienen separación - todas comparten el mismo path en S3
|
||||
- **Resultado**: Eliminar una imagen durante testing la borra para TODOS, incluyendo producción
|
||||
|
||||
### Flujo Actual Problemático
|
||||
1. Admin prueba nueva imagen en preview mode
|
||||
2. Admin elimina imagen antigua durante testing
|
||||
3. Backend ejecuta: `MinIO.DeleteObject("captive-portal/{id}/Logo/old-image.png")`
|
||||
4. ⚠️ Imagen se borra permanentemente de S3
|
||||
5. Si `ProdConfiguration` referenciaba esa imagen → **producción se rompe**
|
||||
|
||||
## Solución Propuesta: Path-based Versioning con Copy-on-Publish
|
||||
|
||||
### Estrategia
|
||||
Separar imágenes draft y production en paths distintos de S3/MinIO:
|
||||
- **Draft**: `captive-portal/{id}/draft/{imageType}/{filename}`
|
||||
- **Production**: `captive-portal/{id}/production/{imageType}/{filename}`
|
||||
|
||||
### Arquitectura de la Solución
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ S3/MinIO Storage │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ captive-portal/123/ │
|
||||
│ ├── draft/ │
|
||||
│ │ ├── Logo/ │
|
||||
│ │ │ ├── logo-v1.png ← Testing zone │
|
||||
│ │ │ └── logo-v2.png ← Safe to delete │
|
||||
│ │ └── Background/ │
|
||||
│ │ └── bg-new.jpg │
|
||||
│ │ │
|
||||
│ └── production/ │
|
||||
│ ├── Logo/ │
|
||||
│ │ └── logo-v1.png ← Protected │
|
||||
│ └── Background/ ← Never deleted during │
|
||||
│ └── bg-current.jpg draft operations │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Database: SplashCaptivePortal (id=123)
|
||||
├── Configuration: "...draft/Logo/logo-v2.png..." ← References draft
|
||||
└── ProdConfiguration: "...production/Logo/logo-v1.png..." ← References production
|
||||
```
|
||||
|
||||
## Cambios Requeridos
|
||||
|
||||
### 1. Backend (C#) - `CaptivePortalAppService.cs`
|
||||
|
||||
#### Archivo Principal
|
||||
**Path**: `src/SplashPage.Application/Perzonalization/CaptivePortalAppService.cs`
|
||||
|
||||
#### Modificaciones Necesarias
|
||||
|
||||
##### A. `UploadImageAsync` (Línea ~766)
|
||||
**Cambio**: Subir a path `/draft/` en lugar de raíz
|
||||
|
||||
```csharp
|
||||
// ANTES:
|
||||
string path = $"captive-portal/{id}/{imageType}/{uniqueFileName}";
|
||||
|
||||
// DESPUÉS:
|
||||
string path = $"captive-portal/{id}/draft/{imageType}/{uniqueFileName}";
|
||||
```
|
||||
|
||||
##### B. `GetImagesAsync` (Línea ~909)
|
||||
**Cambio**: Listar desde path `/draft/` para configuración en edición
|
||||
|
||||
```csharp
|
||||
// ANTES:
|
||||
string prefix = $"captive-portal/{id}/{imageType}/";
|
||||
|
||||
// DESPUÉS:
|
||||
string prefix = $"captive-portal/{id}/draft/{imageType}/";
|
||||
```
|
||||
|
||||
##### C. `DeleteImageAsync` (Línea ~852)
|
||||
**Cambio**: Solo borrar de `/draft/`, NUNCA de `/production/`
|
||||
|
||||
```csharp
|
||||
// ANTES:
|
||||
await _minioStorageService.DeleteObjectAsync(bucket, pathToDelete);
|
||||
|
||||
// DESPUÉS:
|
||||
// Asegurar que solo se borran imágenes draft
|
||||
if (!pathToDelete.Contains("/draft/"))
|
||||
{
|
||||
throw new UserFriendlyException("Solo se pueden eliminar imágenes en modo draft");
|
||||
}
|
||||
await _minioStorageService.DeleteObjectAsync(bucket, pathToDelete);
|
||||
```
|
||||
|
||||
##### D. `PublishConfigurationAsync` (Línea ~749) - **CAMBIO CRÍTICO**
|
||||
**Cambio**: Implementar copy-on-publish de imágenes
|
||||
|
||||
```csharp
|
||||
public async Task PublishConfigurationAsync(int Id)
|
||||
{
|
||||
var portalEntity = await _captivePortalRepository.GetAsync(Id);
|
||||
|
||||
// 1. Parsear configuración draft
|
||||
var draftConfig = JsonConvert.DeserializeObject<CaptivePortalCfgDto>(
|
||||
portalEntity.Configuration
|
||||
);
|
||||
|
||||
// 2. Extraer todas las rutas de imágenes del draft
|
||||
var imagePathsToCopy = new List<string>();
|
||||
if (!string.IsNullOrEmpty(draftConfig.SelectedLogoImagePath))
|
||||
imagePathsToCopy.Add(draftConfig.SelectedLogoImagePath);
|
||||
if (!string.IsNullOrEmpty(draftConfig.SelectedBackgroundImagePath))
|
||||
imagePathsToCopy.Add(draftConfig.SelectedBackgroundImagePath);
|
||||
// TODO: Agregar más campos de imágenes si existen
|
||||
|
||||
// 3. Copiar imágenes de draft a production
|
||||
string bucket = GetBucketName(); // Método existente
|
||||
var productionConfig = draftConfig; // Clonar
|
||||
|
||||
foreach (var draftPath in imagePathsToCopy)
|
||||
{
|
||||
if (string.IsNullOrEmpty(draftPath)) continue;
|
||||
|
||||
// Convertir path: draft/Logo/image.png → production/Logo/image.png
|
||||
string productionPath = draftPath.Replace("/draft/", "/production/");
|
||||
|
||||
// Copiar en MinIO
|
||||
await _minioStorageService.CopyObjectAsync(
|
||||
sourceBucket: bucket,
|
||||
sourceObject: draftPath,
|
||||
destBucket: bucket,
|
||||
destObject: productionPath
|
||||
);
|
||||
|
||||
// Actualizar referencias en config de producción
|
||||
productionConfig = UpdateImagePathInConfig(productionConfig, draftPath, productionPath);
|
||||
}
|
||||
|
||||
// 4. Serializar y guardar
|
||||
portalEntity.ProdConfiguration = JsonConvert.SerializeObject(
|
||||
productionConfig,
|
||||
Formatting.Indented
|
||||
);
|
||||
|
||||
await _captivePortalRepository.UpdateAsync(portalEntity);
|
||||
|
||||
Logger.Info($"Configuración publicada para portal {Id} con {imagePathsToCopy.Count} imágenes copiadas");
|
||||
}
|
||||
|
||||
// Método helper para actualizar paths en el objeto de configuración
|
||||
private CaptivePortalCfgDto UpdateImagePathInConfig(
|
||||
CaptivePortalCfgDto config,
|
||||
string oldPath,
|
||||
string newPath)
|
||||
{
|
||||
if (config.SelectedLogoImagePath == oldPath)
|
||||
config.SelectedLogoImagePath = newPath;
|
||||
if (config.SelectedBackgroundImagePath == oldPath)
|
||||
config.SelectedBackgroundImagePath = newPath;
|
||||
|
||||
return config;
|
||||
}
|
||||
```
|
||||
|
||||
#### E. Verificar `MinioStorageService` tiene método `CopyObjectAsync`
|
||||
|
||||
**Path**: `src/SplashPage.Application/Storage/MinioStorageService.cs`
|
||||
|
||||
Si no existe, implementar:
|
||||
|
||||
```csharp
|
||||
public async Task CopyObjectAsync(
|
||||
string sourceBucket,
|
||||
string sourceObject,
|
||||
string destBucket,
|
||||
string destObject)
|
||||
{
|
||||
var args = new CopyObjectArgs()
|
||||
.WithBucket(destBucket)
|
||||
.WithObject(destObject)
|
||||
.WithCopyObjectSource(new CopySourceObjectArgs()
|
||||
.WithBucket(sourceBucket)
|
||||
.WithObject(sourceObject));
|
||||
|
||||
await _minioClient.CopyObjectAsync(args);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Frontend (Next.js) - Verificación
|
||||
|
||||
#### Archivos a Revisar
|
||||
1. **`src/SplashPage.Web.Ui/src/app/dashboard/settings/captive-portal/[id]/page.tsx`**
|
||||
- ✅ Ya usa `GetPortalConfiguration` para draft (correcto)
|
||||
- ✅ Preview mode ya apunta a draft config
|
||||
|
||||
2. **`src/SplashPage.Web.Ui/src/app/CaptivePortal/Portal/[id]/page.tsx`**
|
||||
- ✅ Ya usa `GetPortalProdConfiguration` para producción (correcto)
|
||||
- ✅ No requiere cambios
|
||||
|
||||
3. **`src/SplashPage.Web.Ui/src/hooks/captive-portal/use-portal-images.ts`**
|
||||
- ✅ Ya funciona correctamente con la API
|
||||
- ✅ No requiere cambios (paths manejados por backend)
|
||||
|
||||
**Conclusión Frontend**: No se requieren cambios, solo verificar que todo siga funcionando después de los cambios del backend.
|
||||
|
||||
## Flujo Completo Después de la Implementación
|
||||
|
||||
### Escenario: Admin Configura Nuevo Logo
|
||||
|
||||
```
|
||||
1. Admin sube logo-new.png
|
||||
→ Backend: Guarda en captive-portal/123/draft/Logo/logo-new-uuid.png
|
||||
→ Draft config actualizado: "draft/Logo/logo-new-uuid.png"
|
||||
→ Production config sin cambios: "production/Logo/logo-old-uuid.png"
|
||||
|
||||
2. Admin prueba en Preview Mode
|
||||
→ Preview carga desde GetPortalConfiguration (draft)
|
||||
→ Muestra logo-new.png
|
||||
→ Portal público sigue mostrando logo-old.png ✅
|
||||
|
||||
3. Admin decide eliminar logo anterior del draft
|
||||
→ DELETE /api/.../DeleteImage
|
||||
→ Backend borra: captive-portal/123/draft/Logo/logo-old-uuid.png
|
||||
→ Production NO afectada (tiene su propia copia) ✅
|
||||
|
||||
4. Admin hace "Guardar"
|
||||
→ SaveConfiguration actualiza campo Configuration
|
||||
→ Production sigue sin cambios ✅
|
||||
|
||||
5. Admin hace "Publicar"
|
||||
→ PublishConfigurationAsync:
|
||||
a. Lee Configuration (draft)
|
||||
b. Encuentra: "draft/Logo/logo-new-uuid.png"
|
||||
c. Copia MinIO: draft/Logo/logo-new-uuid.png → production/Logo/logo-new-uuid.png
|
||||
d. Actualiza ProdConfiguration: "production/Logo/logo-new-uuid.png"
|
||||
e. Guarda en BD
|
||||
→ Ahora portal público muestra logo nuevo ✅
|
||||
→ Logo viejo en production puede limpiarse después (garbage collection)
|
||||
```
|
||||
|
||||
## Beneficios de Esta Solución
|
||||
|
||||
### Técnicos
|
||||
- ✅ **Zero Downtime**: No afecta producción actual durante implementación
|
||||
- ✅ **Aislamiento Completo**: Draft y production totalmente independientes
|
||||
- ✅ **Rollback Simple**: Feature "Restore from Production" sigue funcionando
|
||||
- ✅ **No Requiere Migración DB**: Solo cambios en lógica de aplicación
|
||||
- ✅ **Backward Compatible**: No rompe configuraciones existentes
|
||||
|
||||
### Operacionales
|
||||
- ✅ **Testing Seguro**: Admins pueden probar sin riesgo
|
||||
- ✅ **Preview Confiable**: Preview siempre refleja draft real
|
||||
- ✅ **Publicación Atómica**: Publish copia todo junto
|
||||
- ✅ **Auditoría Simple**: Path indica estado (draft vs production)
|
||||
|
||||
### De Negocio
|
||||
- ✅ **Previene Pérdida de Datos**: Producción protegida
|
||||
- ✅ **Reduce Errores**: No más portales rotos por testing
|
||||
- ✅ **Workflow Claro**: Draft → Test → Publish
|
||||
- ✅ **Sin Costos Adicionales**: Storage es barato
|
||||
|
||||
## Mejoras Futuras (Post-MVP)
|
||||
|
||||
### 1. Garbage Collection (Opcional)
|
||||
Implementar job nocturno que limpie:
|
||||
- Imágenes draft no referenciadas por más de 30 días
|
||||
- Imágenes production huérfanas después de publish
|
||||
|
||||
```csharp
|
||||
// HangFire recurring job
|
||||
public async Task CleanupOrphanedImagesAsync()
|
||||
{
|
||||
// Lógica de limpieza
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Validación Pre-Delete (Opcional)
|
||||
Agregar warning en UI si se intenta eliminar imagen que existe en production:
|
||||
|
||||
```csharp
|
||||
public async Task<ImageDeleteWarning> CheckImageBeforeDeleteAsync(string imagePath)
|
||||
{
|
||||
// Verificar si existe versión production
|
||||
string productionPath = imagePath.Replace("/draft/", "/production/");
|
||||
bool existsInProduction = await _minioStorageService.ObjectExistsAsync(bucket, productionPath);
|
||||
|
||||
return new ImageDeleteWarning
|
||||
{
|
||||
CanDelete = true,
|
||||
Warning = existsInProduction
|
||||
? "Esta imagen tiene una versión publicada en producción"
|
||||
: null
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Historial de Versiones (Futuro)
|
||||
Si necesitan auditoría completa en el futuro:
|
||||
- Agregar tabla `CaptivePortalImageHistory`
|
||||
- Trackear: `PortalId`, `ImagePath`, `Action`, `User`, `Timestamp`
|
||||
|
||||
## Archivos Impactados
|
||||
|
||||
### Backend
|
||||
1. ✏️ `src/SplashPage.Application/Perzonalization/CaptivePortalAppService.cs` (principal)
|
||||
2. ✏️ `src/SplashPage.Application/Storage/MinioStorageService.cs` (si falta CopyObject)
|
||||
3. 📖 `src/SplashPage.Application/Perzonalization/ICaptivePortalAppService.cs` (revisar interface)
|
||||
|
||||
### Frontend
|
||||
1. 👀 `src/SplashPage.Web.Ui/src/app/dashboard/settings/captive-portal/[id]/page.tsx` (verificar)
|
||||
2. 👀 `src/SplashPage.Web.Ui/src/app/CaptivePortal/Portal/[id]/page.tsx` (verificar)
|
||||
3. 👀 `src/SplashPage.Web.Ui/src/hooks/captive-portal/use-portal-images.ts` (verificar)
|
||||
|
||||
**Leyenda**: ✏️ Modificar | 👀 Verificar (no modificar)
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### 1. Testing Manual (Crítico)
|
||||
- [ ] Subir imagen nueva en draft
|
||||
- [ ] Verificar preview muestra imagen nueva
|
||||
- [ ] Verificar producción sigue mostrando imagen vieja
|
||||
- [ ] Eliminar imagen en draft
|
||||
- [ ] Verificar producción NO se rompe
|
||||
- [ ] Publicar configuración
|
||||
- [ ] Verificar imagen copiada a production
|
||||
- [ ] Verificar producción muestra imagen nueva
|
||||
- [ ] Probar "Restore from Production"
|
||||
- [ ] Probar con múltiples imágenes (logo + background)
|
||||
|
||||
### 2. Testing de Edge Cases
|
||||
- [ ] Publicar sin cambios de imágenes
|
||||
- [ ] Publicar con imagen ya existente en production (sobrescribir)
|
||||
- [ ] Eliminar imagen y subir otra con mismo nombre
|
||||
- [ ] Verificar errores si MinIO falla (copy error)
|
||||
|
||||
### 3. Testing de Regresión
|
||||
- [ ] Portal existente sigue funcionando
|
||||
- [ ] Preview mode sigue funcionando
|
||||
- [ ] Upload/delete/select desde UI funcionan
|
||||
- [ ] API endpoints existentes no rotos
|
||||
|
||||
## Estimación de Tiempo
|
||||
|
||||
| Tarea | Tiempo Estimado |
|
||||
|-------|-----------------|
|
||||
| Modificar `UploadImageAsync` | 15 min |
|
||||
| Modificar `GetImagesAsync` | 15 min |
|
||||
| Modificar `DeleteImageAsync` | 20 min |
|
||||
| Implementar `CopyObjectAsync` en MinioStorageService | 30 min |
|
||||
| Refactorizar `PublishConfigurationAsync` | 90 min |
|
||||
| Testing manual completo | 60 min |
|
||||
| Testing edge cases | 30 min |
|
||||
| Testing de regresión | 30 min |
|
||||
| **TOTAL** | **~4.5 horas** |
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
| Riesgo | Probabilidad | Impacto | Mitigación |
|
||||
|--------|--------------|---------|------------|
|
||||
| MinIO CopyObject falla | Baja | Alto | Try-catch con rollback, logging detallado |
|
||||
| Portales existentes rompen | Media | Alto | Deploy en staging primero, backup DB |
|
||||
| Performance de copy en publish | Baja | Bajo | Es async, no bloquea. Futuro: job background |
|
||||
| Espacio duplicado en S3 | Baja | Bajo | Storage barato, futuro: garbage collection |
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Si algo falla después del deploy:
|
||||
|
||||
1. **Inmediato**: Revertir código a commit anterior
|
||||
2. **Database**: No hay cambios en schema, solo datos en JSON (revertir si necesario)
|
||||
3. **S3/MinIO**: Archivos draft/production coexisten, no hay pérdida de datos
|
||||
4. **Monitoreo**: Revisar logs de `PublishConfigurationAsync` para errores
|
||||
|
||||
## Notas Adicionales
|
||||
|
||||
### Por Qué Esta Solución (vs Alternativas)
|
||||
|
||||
#### ❌ Reference Counting
|
||||
- Más complejo: requiere analizar ambas configs en cada delete
|
||||
- Más lento: queries adicionales en cada operación
|
||||
- Más frágil: bugs si el conteo se desincroniza
|
||||
|
||||
#### ❌ Soft Delete / Immutable Storage
|
||||
- Overhead de storage innecesario para bajo volumen
|
||||
- Complejidad en queries (filtrar deleted)
|
||||
- Necesita garbage collection desde día 1
|
||||
|
||||
#### ✅ Path-based Versioning (Seleccionada)
|
||||
- **Simple**: Solo cambios en paths
|
||||
- **Rápida**: Implementación en 1-2 días
|
||||
- **Robusta**: Aislamiento físico garantizado
|
||||
- **Escalable**: Funciona igual con 10 o 10,000 imágenes
|
||||
- **Clara**: Path indica estado (draft vs production)
|
||||
|
||||
### Consideraciones de Migración
|
||||
|
||||
**¿Qué pasa con portales existentes que ya tienen imágenes?**
|
||||
|
||||
**Respuesta**: No requieren migración forzosa:
|
||||
1. Imágenes existentes quedan en paths antiguos (`captive-portal/{id}/{type}/`)
|
||||
2. Backend las trata como production implícitamente
|
||||
3. Próximo upload va a `/draft/`
|
||||
4. Próximo publish copia a `/production/`
|
||||
5. Después de primer ciclo draft→publish, portal está en nuevo esquema
|
||||
|
||||
**Migración opcional (recomendado pero no crítico)**:
|
||||
```sql
|
||||
-- Script one-time para mover imágenes existentes a /production/
|
||||
-- Ejecutar en MinIO o via script C#
|
||||
```
|
||||
|
||||
## Aprobación y Próximos Pasos
|
||||
|
||||
### Para Aprobar Este Plan
|
||||
- [ ] Revisión técnica del equipo
|
||||
- [ ] Aprobación de arquitectura
|
||||
- [ ] Confirmación de ventana de deploy
|
||||
|
||||
### Para Implementar
|
||||
1. Crear branch: `feature/captive-portal-draft-images`
|
||||
2. Implementar cambios backend según este documento
|
||||
3. Testing en ambiente dev/staging
|
||||
4. Code review
|
||||
5. Deploy a producción
|
||||
6. Monitoreo post-deploy (24h)
|
||||
7. Documentar en changelog.MD
|
||||
|
||||
---
|
||||
|
||||
**Documento Creado**: 2025-10-30
|
||||
**Autor**: Claude Code (basado en análisis del codebase)
|
||||
**Estado**: Pendiente de Aprobación
|
||||
**Prioridad**: Alta (previene pérdida de datos en producción)
|
||||
@@ -12,7 +12,7 @@
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"SPLASH_CUSTOMER": "LC",
|
||||
"SPLASH_CUSTOMER": "TEST",
|
||||
"SPLASH_SKIP_WORKER": "true",
|
||||
"SPLASH_APP_NAME": "OSB"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"ConnectionStrings": {
|
||||
//"Default": "Server=172.21.4.113;Database=SplashPageBase;User=Jordi;Password=SqlS3rv3r;Encrypt=True;TrustServerCertificate=True;",
|
||||
//"Default": "Server=45.168.234.22;Port=5000;Database=SULTANES_db_Splash;Password=Bpusr001;User=root",
|
||||
"Default": "User ID=mysql;Password=Bpusr001;Host=45.168.234.22;Port=5001;Database=lc_db_splash;Pooling=true;Minimum Pool Size=20;Maximum Pool Size=150;Connection Idle Lifetime=300;Connection Pruning Interval=5;Timeout=30;Command Timeout=360;"
|
||||
"Default": "User ID=mysql;Password=Bpusr001;Host=45.168.234.22;Port=5001;Database=onboarding;Pooling=true;Minimum Pool Size=20;Maximum Pool Size=150;Connection Idle Lifetime=300;Connection Pruning Interval=5;Timeout=30;Command Timeout=360;"
|
||||
//"Default": "User ID=mysql;Password=Bpusr001;Host=2.tcp.ngrok.io;Port=11925;Database=nazan_db_splash;Pooling=true;Command Timeout=360;"
|
||||
},
|
||||
"App": {
|
||||
|
||||
Reference in New Issue
Block a user