changes: TODO: Remove this config files from tracking

This commit is contained in:
2025-10-31 07:07:59 -06:00
parent 311587606e
commit 2fafdaf689
3 changed files with 441 additions and 2 deletions

439
cp_draft_image.md Normal file
View 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)

View File

@@ -12,7 +12,7 @@
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"SPLASH_CUSTOMER": "LC",
"SPLASH_CUSTOMER": "TEST",
"SPLASH_SKIP_WORKER": "true",
"SPLASH_APP_NAME": "OSB"
}

View File

@@ -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": {