Files
Temp_MSSPLASHPage/splash_master_update.md

27 KiB

SplashPage Updates para OSB Master Integration

Resumen

Este documento describe las modificaciones necesarias en las instancias de SplashPage para integrarse con el sistema OSB Master.


Fase 1: Modificaciones en SplashPage Backend

1.1 Crear API de Métricas

InstanceMetricsController.cs

Ubicación: src/SplashPage.Web.Host/Controllers/InstanceMetricsController.cs

Endpoints a implementar:

[Route("api/[controller]")]
public class InstanceMetricsController : SplashPageControllerBase
{
    private readonly IRepository<SplashUser> _userRepo;
    private readonly IRepository<SplashUserConnection> _connectionRepo;
    private readonly IRepository<SplashMerakiNetwork> _networkRepo;
    private readonly IRepository<SplashEmailTemplate> _emailTemplateRepo;
    private readonly IRepository<SplashScheduledEmail> _scheduledEmailRepo;
    private readonly ITenantCache _tenantCache;

    [HttpGet("health")]
    [AbpAllowAnonymous]
    public async Task<IActionResult> GetHealth()
    {
        // Basic health check - no auth required
        return Ok(new {
            status = "healthy",
            timestamp = DateTime.UtcNow,
            version = AppVersionHelper.Version
        });
    }

    [HttpGet("metrics")]
    public async Task<InstanceMetricsDto> GetMetrics(
        [FromHeader(Name = "X-Master-Api-Key")] string apiKey)
    {
        // Validate API key
        var validKey = Environment.GetEnvironmentVariable("MASTER_API_KEY");
        if (string.IsNullOrEmpty(apiKey) || apiKey != validKey)
            throw new AbpAuthorizationException("Invalid API key");

        var tenantId = AbpSession.TenantId ?? 0;

        // Gather metrics (use service layer)
        var metrics = await _metricsService.GetInstanceMetrics(tenantId);

        return metrics;
    }
}

1.2 Crear DTO de Métricas

InstanceMetricsDto.cs

Ubicación: src/SplashPage.Application/Metrics/Dto/InstanceMetricsDto.cs

public class InstanceMetricsDto
{
    // Instance Info
    public string InstanceId { get; set; }
    public string TenantName { get; set; }
    public int TenantId { get; set; }
    public DateTime LastChecked { get; set; }
    public string AppVersion { get; set; }
    public string Environment { get; set; }

    // User Metrics
    public int TotalUsers { get; set; }
    public int ActiveUsersLast24Hours { get; set; }
    public int ActiveUsersLast7Days { get; set; }
    public int OnlineUsersNow { get; set; }
    public DateTime? LastUserLogin { get; set; }
    public string LastUserEmail { get; set; }

    // Admin Access
    public DateTime? LastAdminAccess { get; set; }
    public string LastAdminEmail { get; set; }

    // Device/Network Metrics
    public int TotalNetworks { get; set; }
    public int TotalAccessPoints { get; set; }
    public int MerakiOrganizationCount { get; set; }

    // Email Metrics
    public int EmailTemplatesCount { get; set; }
    public int PendingScheduledEmails { get; set; }
    public int EmailsSentLast24Hours { get; set; }
    public DateTime? LastEmailSent { get; set; }

    // System Health
    public bool DatabaseConnected { get; set; }
    public bool MerakiApiHealthy { get; set; }
    public double DatabaseResponseTimeMs { get; set; }
}

1.3 Implementar Service de Métricas

InstanceMetricsAppService.cs

Ubicación: src/SplashPage.Application/Metrics/InstanceMetricsAppService.cs

public class InstanceMetricsAppService : SplashPageAppServiceBase
{
    private readonly IRepository<SplashUser> _userRepo;
    private readonly IRepository<SplashUserConnection> _connectionRepo;
    private readonly IRepository<SplashMerakiNetwork> _networkRepo;
    private readonly IRepository<SplashMerakiDevice> _deviceRepo;
    private readonly IRepository<SplashMerakiOrganization> _orgRepo;
    private readonly IRepository<SplashEmailTemplate> _emailTemplateRepo;
    private readonly IRepository<SplashScheduledEmail> _scheduledEmailRepo;
    private readonly ITenantCache _tenantCache;
    private readonly IMemoryCache _cache;

    public async Task<InstanceMetricsDto> GetInstanceMetrics(int tenantId)
    {
        // Check cache first (1 minute cache)
        var cacheKey = $"InstanceMetrics_{tenantId}";
        if (_cache.TryGetValue(cacheKey, out InstanceMetricsDto cachedMetrics))
        {
            return cachedMetrics;
        }

        var metrics = new InstanceMetricsDto
        {
            InstanceId = Environment.GetEnvironmentVariable("INSTANCE_ID") ?? "unknown",
            TenantId = tenantId,
            TenantName = _tenantCache.Get(tenantId)?.TenancyName ?? "Unknown",
            LastChecked = DateTime.UtcNow,
            AppVersion = AppVersionHelper.Version,
            Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"
        };

        // Run queries in parallel for better performance
        var tasks = new[]
        {
            GetUserMetricsAsync(tenantId, metrics),
            GetDeviceMetricsAsync(tenantId, metrics),
            GetEmailMetricsAsync(tenantId, metrics),
            GetSystemHealthAsync(metrics)
        };

        await Task.WhenAll(tasks);

        // Cache for 1 minute
        _cache.Set(cacheKey, metrics, TimeSpan.FromMinutes(1));

        return metrics;
    }

    private async Task GetUserMetricsAsync(int tenantId, InstanceMetricsDto metrics)
    {
        // Total users
        metrics.TotalUsers = await _userRepo.CountAsync(u => u.TenantId == tenantId);

        // Active users last 24 hours
        var last24h = DateTime.UtcNow.AddHours(-24);
        metrics.ActiveUsersLast24Hours = await _connectionRepo.CountAsync(
            c => c.TenantId == tenantId && c.LastSeen >= last24h
        );

        // Active users last 7 days
        var last7d = DateTime.UtcNow.AddDays(-7);
        metrics.ActiveUsersLast7Days = await _connectionRepo.CountAsync(
            c => c.TenantId == tenantId && c.LastSeen >= last7d
        );

        // Online users now
        metrics.OnlineUsersNow = await _connectionRepo.CountAsync(
            c => c.TenantId == tenantId && c.Status == "Online"
        );

        // Last user login
        var lastConnection = await _connectionRepo.GetAll()
            .Where(c => c.TenantId == tenantId)
            .OrderByDescending(c => c.LastSeen)
            .FirstOrDefaultAsync();

        if (lastConnection != null)
        {
            metrics.LastUserLogin = lastConnection.LastSeen;
            var user = await _userRepo.GetAsync(lastConnection.SplashUserId);
            metrics.LastUserEmail = user?.Email;
        }

        // Last admin access - from AbpUserLoginAttempts or AuditLogs
        // This would need to query the ABP audit logs for admin users
        // Implementation depends on your specific audit logging setup
    }

    private async Task GetDeviceMetricsAsync(int tenantId, InstanceMetricsDto metrics)
    {
        metrics.TotalNetworks = await _networkRepo.CountAsync(n => n.TenantId == tenantId);
        metrics.TotalAccessPoints = await _deviceRepo.CountAsync(
            d => d.TenantId == tenantId && d.Model.Contains("AP")
        );
        metrics.MerakiOrganizationCount = await _orgRepo.CountAsync(o => o.TenantId == tenantId);
    }

    private async Task GetEmailMetricsAsync(int tenantId, InstanceMetricsDto metrics)
    {
        metrics.EmailTemplatesCount = await _emailTemplateRepo.CountAsync(
            t => t.TenantId == tenantId && t.IsActive
        );

        metrics.PendingScheduledEmails = await _scheduledEmailRepo.CountAsync(
            e => e.TenantId == tenantId && e.Status == "Pending"
        );

        var last24h = DateTime.UtcNow.AddHours(-24);
        metrics.EmailsSentLast24Hours = await _scheduledEmailRepo.CountAsync(
            e => e.TenantId == tenantId &&
            e.Status == "Sent" &&
            e.SentDate >= last24h
        );

        var lastEmail = await _scheduledEmailRepo.GetAll()
            .Where(e => e.TenantId == tenantId && e.Status == "Sent")
            .OrderByDescending(e => e.SentDate)
            .FirstOrDefaultAsync();

        metrics.LastEmailSent = lastEmail?.SentDate;
    }

    private async Task GetSystemHealthAsync(InstanceMetricsDto metrics)
    {
        try
        {
            var sw = Stopwatch.StartNew();
            await _userRepo.GetAll().AnyAsync();
            sw.Stop();

            metrics.DatabaseConnected = true;
            metrics.DatabaseResponseTimeMs = sw.ElapsedMilliseconds;
        }
        catch
        {
            metrics.DatabaseConnected = false;
            metrics.DatabaseResponseTimeMs = -1;
        }

        // Check Meraki API health by trying to fetch org info
        // Implementation depends on your Meraki service
        metrics.MerakiApiHealthy = true; // Placeholder
    }
}

1.4 Módulo de Buzón de Sugerencias

1.4.1 Entidad - SuggestionBox.cs

Ubicación: src/SplashPage.Core/Feedback/SuggestionBox.cs

[Table("SuggestionBoxes")]
public class SuggestionBox : FullAuditedEntity, IMustHaveTenant
{
    public int TenantId { get; set; }

    public long UserId { get; set; }
    [ForeignKey("UserId")]
    public virtual User User { get; set; }

    [Required]
    [StringLength(50)]
    public string Category { get; set; } // Bug, Feature, Improvement, Performance, UI/UX, Documentation, Other

    [Required]
    [StringLength(200)]
    public string Subject { get; set; }

    [Required]
    [StringLength(5000)]
    public string Description { get; set; }

    [StringLength(50)]
    public string Status { get; set; } // Pending, InReview, Resolved, Rejected

    [StringLength(20)]
    public string Priority { get; set; } // Low, Medium, High

    public DateTime? ResolvedDate { get; set; }

    [StringLength(2000)]
    public string AdminResponse { get; set; }

    public long? ResolvedBy { get; set; }

    public bool SentToMaster { get; set; }
    public DateTime? SentToMasterDate { get; set; }
}

1.4.2 DbContext - Agregar DbSet

Ubicación: src/SplashPage.EntityFrameworkCore/EntityFrameworkCore/SplashPageDbContext.cs

public DbSet<SuggestionBox> SuggestionBoxes { get; set; }

1.4.3 Application Service - SuggestionBoxAppService.cs

Ubicación: src/SplashPage.Application/Feedback/SuggestionBoxAppService.cs

public class SuggestionBoxAppService : AsyncCrudAppService<
    SuggestionBox,
    SuggestionBoxDto,
    int,
    PagedSuggestionBoxResultRequestDto,
    CreateSuggestionBoxDto,
    SuggestionBoxDto>
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SuggestionBoxAppService(
        IRepository<SuggestionBox> repository,
        IHttpClientFactory httpClientFactory) : base(repository)
    {
        _httpClientFactory = httpClientFactory;
    }

    [AbpAuthorize]
    public override async Task<SuggestionBoxDto> CreateAsync(CreateSuggestionBoxDto input)
    {
        // Create locally
        var suggestion = ObjectMapper.Map<SuggestionBox>(input);
        suggestion.UserId = AbpSession.GetUserId();
        suggestion.Status = "Pending";
        suggestion.Priority = "Medium"; // Default
        suggestion.SentToMaster = false;

        var id = await Repository.InsertAndGetIdAsync(suggestion);
        await CurrentUnitOfWork.SaveChangesAsync();

        var created = await Repository.GetAsync(id);

        // Send to Master via webhook (fire and forget)
        _ = SendToMasterAsync(created);

        return MapToEntityDto(created);
    }

    [AbpAuthorize]
    public async Task<PagedResultDto<SuggestionBoxDto>> GetMySuggestions(
        PagedSuggestionBoxResultRequestDto input)
    {
        var userId = AbpSession.GetUserId();
        var query = Repository.GetAll()
            .Where(s => s.UserId == userId);

        var totalCount = await query.CountAsync();
        var items = await query
            .OrderByDescending(s => s.CreationTime)
            .PageBy(input)
            .ToListAsync();

        return new PagedResultDto<SuggestionBoxDto>(
            totalCount,
            ObjectMapper.Map<List<SuggestionBoxDto>>(items)
        );
    }

    [AbpAuthorize("Pages.Administration")]
    public async Task<PagedResultDto<SuggestionBoxDto>> GetAllSuggestions(
        PagedSuggestionBoxResultRequestDto input)
    {
        var query = Repository.GetAll()
            .WhereIf(!string.IsNullOrEmpty(input.Status), s => s.Status == input.Status)
            .WhereIf(!string.IsNullOrEmpty(input.Category), s => s.Category == input.Category);

        var totalCount = await query.CountAsync();
        var items = await query
            .OrderByDescending(s => s.CreationTime)
            .PageBy(input)
            .ToListAsync();

        return new PagedResultDto<SuggestionBoxDto>(
            totalCount,
            ObjectMapper.Map<List<SuggestionBoxDto>>(items)
        );
    }

    private async Task SendToMasterAsync(SuggestionBox suggestion)
    {
        try
        {
            var masterUrl = Environment.GetEnvironmentVariable("MASTER_URL");
            if (string.IsNullOrEmpty(masterUrl))
                return;

            var instanceId = Environment.GetEnvironmentVariable("INSTANCE_ID");
            var apiKey = Environment.GetEnvironmentVariable("MASTER_API_KEY");

            var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.Add("X-Master-Api-Key", apiKey);

            var payload = new
            {
                instanceId = instanceId,
                tenantName = suggestion.TenantId, // Get tenant name from cache
                userId = suggestion.UserId,
                userEmail = suggestion.User?.EmailAddress,
                userName = suggestion.User?.FullName,
                category = suggestion.Category,
                subject = suggestion.Subject,
                description = suggestion.Description,
                timestamp = suggestion.CreationTime
            };

            var response = await client.PostAsJsonAsync(
                $"{masterUrl}/api/suggestions/submit",
                payload
            );

            if (response.IsSuccessStatusCode)
            {
                suggestion.SentToMaster = true;
                suggestion.SentToMasterDate = DateTime.UtcNow;
                await Repository.UpdateAsync(suggestion);
            }
        }
        catch (Exception ex)
        {
            Logger.Error("Failed to send suggestion to Master", ex);
            // Don't throw - this is a background operation
        }
    }
}

1.4.4 DTOs

Ubicación: src/SplashPage.Application/Feedback/Dto/

public class SuggestionBoxDto : EntityDto
{
    public int TenantId { get; set; }
    public long UserId { get; set; }
    public string UserName { get; set; }
    public string UserEmail { get; set; }
    public string Category { get; set; }
    public string Subject { get; set; }
    public string Description { get; set; }
    public string Status { get; set; }
    public string Priority { get; set; }
    public DateTime? ResolvedDate { get; set; }
    public string AdminResponse { get; set; }
    public DateTime CreationTime { get; set; }
}

public class CreateSuggestionBoxDto
{
    [Required]
    public string Category { get; set; }

    [Required]
    [StringLength(200)]
    public string Subject { get; set; }

    [Required]
    [StringLength(5000)]
    public string Description { get; set; }
}

public class PagedSuggestionBoxResultRequestDto : PagedResultRequestDto
{
    public string Status { get; set; }
    public string Category { get; set; }
}

1.5 UI del Buzón de Sugerencias (Next.js)

1.5.1 Ruta de Feedback

Ubicación: src/SplashPage.Web.Ui/src/app/dashboard/feedback/page.tsx

'use client';

import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { useCreateSuggestionMutation, useGetMySuggestionsQuery } from '@/api/hooks';

const formSchema = z.object({
  category: z.string().min(1, 'Categoría requerida'),
  subject: z.string().min(5, 'Mínimo 5 caracteres').max(200),
  description: z.string().min(20, 'Mínimo 20 caracteres').max(5000),
});

export default function FeedbackPage() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      category: '',
      subject: '',
      description: '',
    },
  });

  const createMutation = useCreateSuggestionMutation();
  const { data: mySuggestions, refetch } = useGetMySuggestionsQuery({
    maxResultCount: 20,
    skipCount: 0,
  });

  const onSubmit = async (values: z.infer<typeof formSchema>) => {
    try {
      await createMutation.mutateAsync(values);
      form.reset();
      refetch();
    } catch (error) {
      console.error('Error creating suggestion:', error);
    }
  };

  const categories = [
    'Bug',
    'Feature Request',
    'Improvement',
    'Performance',
    'UI/UX',
    'Documentation',
    'Other',
  ];

  const getStatusColor = (status: string) => {
    switch (status) {
      case 'Pending':
        return 'bg-yellow-500';
      case 'InReview':
        return 'bg-blue-500';
      case 'Resolved':
        return 'bg-green-500';
      case 'Rejected':
        return 'bg-red-500';
      default:
        return 'bg-gray-500';
    }
  };

  return (
    <div className="container mx-auto p-6 space-y-6">
      <h1 className="text-3xl font-bold">Buzón de Sugerencias</h1>

      {/* Form */}
      <Card>
        <CardHeader>
          <CardTitle>Nueva Sugerencia</CardTitle>
        </CardHeader>
        <CardContent>
          <Form {...form}>
            <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
              <FormField
                control={form.control}
                name="category"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Categoría</FormLabel>
                    <Select onValueChange={field.onChange} defaultValue={field.value}>
                      <FormControl>
                        <SelectTrigger>
                          <SelectValue placeholder="Selecciona una categoría" />
                        </SelectTrigger>
                      </FormControl>
                      <SelectContent>
                        {categories.map((cat) => (
                          <SelectItem key={cat} value={cat}>
                            {cat}
                          </SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="subject"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Asunto</FormLabel>
                    <FormControl>
                      <Input placeholder="Breve descripción del tema" {...field} />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <FormField
                control={form.control}
                name="description"
                render={({ field }) => (
                  <FormItem>
                    <FormLabel>Descripción</FormLabel>
                    <FormControl>
                      <Textarea
                        placeholder="Describe tu sugerencia en detalle..."
                        rows={6}
                        {...field}
                      />
                    </FormControl>
                    <FormMessage />
                  </FormItem>
                )}
              />

              <Button type="submit" disabled={createMutation.isPending}>
                {createMutation.isPending ? 'Enviando...' : 'Enviar Sugerencia'}
              </Button>
            </form>
          </Form>
        </CardContent>
      </Card>

      {/* History */}
      <Card>
        <CardHeader>
          <CardTitle>Mis Sugerencias</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="space-y-4">
            {mySuggestions?.items?.map((suggestion) => (
              <div
                key={suggestion.id}
                className="border rounded-lg p-4 space-y-2"
              >
                <div className="flex items-center justify-between">
                  <h3 className="font-semibold">{suggestion.subject}</h3>
                  <Badge className={getStatusColor(suggestion.status)}>
                    {suggestion.status}
                  </Badge>
                </div>
                <p className="text-sm text-gray-600">{suggestion.description}</p>
                <div className="flex items-center gap-2 text-xs text-gray-500">
                  <span>{suggestion.category}</span>
                  <span></span>
                  <span>{new Date(suggestion.creationTime).toLocaleDateString()}</span>
                </div>
                {suggestion.adminResponse && (
                  <div className="mt-2 p-2 bg-blue-50 rounded">
                    <p className="text-sm font-semibold">Respuesta del Admin:</p>
                    <p className="text-sm">{suggestion.adminResponse}</p>
                  </div>
                )}
              </div>
            ))}
          </div>
        </CardContent>
      </Card>
    </div>
  );
}

1.5.2 Botón Flotante (Componente Global)

Ubicación: src/SplashPage.Web.Ui/src/components/feedback-button.tsx

'use client';

import { MessageSquarePlus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useRouter } from 'next/navigation';

export function FeedbackButton() {
  const router = useRouter();

  return (
    <Button
      onClick={() => router.push('/dashboard/feedback')}
      className="fixed bottom-6 right-6 rounded-full w-14 h-14 shadow-lg"
      size="icon"
      title="Enviar sugerencia"
    >
      <MessageSquarePlus className="h-6 w-6" />
    </Button>
  );
}

Agregar a Layout: src/SplashPage.Web.Ui/src/app/dashboard/layout.tsx

import { FeedbackButton } from '@/components/feedback-button';

export default function DashboardLayout({ children }) {
  return (
    <>
      {children}
      <FeedbackButton />
    </>
  );
}

1.6 Variables de Entorno

Ubicación: Agregar a appsettings.json o variables de entorno del servidor

{
  "Master": {
    "ApiKey": "your-secure-api-key-here",
    "InstanceId": "lc-prod",
    "MasterUrl": "https://master.osb.beprime.mx",
    "ReportingEnabled": true
  }
}

Variables de Entorno:

MASTER_API_KEY=your-secure-api-key-here
INSTANCE_ID=lc-prod
MASTER_URL=https://master.osb.beprime.mx

1.7 Auto-Enrollment Worker (Opcional)

Ubicación: src/SplashPage.Application/BackgroundWorkers/MasterEnrollmentWorker.cs

public class MasterEnrollmentWorker : PeriodicBackgroundWorkerBase
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ITenantCache _tenantCache;

    public MasterEnrollmentWorker(
        AbpTimer timer,
        IHttpClientFactory httpClientFactory,
        ITenantCache tenantCache) : base(timer)
    {
        Timer.Period = 3600000; // Run once per hour
        _httpClientFactory = httpClientFactory;
        _tenantCache = tenantCache;
    }

    protected override async Task DoWorkAsync()
    {
        var masterUrl = Environment.GetEnvironmentVariable("MASTER_URL");
        if (string.IsNullOrEmpty(masterUrl))
            return;

        try
        {
            var instanceId = Environment.GetEnvironmentVariable("INSTANCE_ID");
            var apiKey = Environment.GetEnvironmentVariable("MASTER_API_KEY");
            var tenantId = AbpSession.TenantId ?? 1;
            var tenant = _tenantCache.Get(tenantId);

            var client = _httpClientFactory.CreateClient();
            client.DefaultRequestHeaders.Add("X-Master-Api-Key", apiKey);

            var payload = new
            {
                instanceId = instanceId,
                baseUrl = "https://" + Environment.GetEnvironmentVariable("DOMAIN"),
                tenantId = tenantId,
                tenantName = tenant?.TenancyName,
                environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
                appVersion = AppVersionHelper.Version,
                contactEmail = tenant?.AdminEmailAddress
            };

            var response = await client.PostAsJsonAsync(
                $"{masterUrl}/api/onboarding/enroll",
                payload
            );

            if (response.IsSuccessStatusCode)
            {
                Logger.Info("Successfully enrolled/updated with Master");
            }
        }
        catch (Exception ex)
        {
            Logger.Error("Failed to enroll with Master", ex);
        }
    }
}

Migración de Base de Datos

Crear Migración

cd src/SplashPage.EntityFrameworkCore
dotnet ef migrations add AddSuggestionBox --startup-project ../SplashPage.Web.Host

Aplicar Migración

cd src/SplashPage.Migrator
dotnet run

O manualmente:

cd src/SplashPage.EntityFrameworkCore
dotnet ef database update --startup-project ../SplashPage.Web.Host

Testing

1. Test Health Endpoint

curl https://your-instance.com/api/InstanceMetrics/health

Expected: { "status": "healthy", "timestamp": "..." }

2. Test Metrics Endpoint

curl -H "X-Master-Api-Key: your-api-key" \
     https://your-instance.com/api/InstanceMetrics/metrics

Expected: Full metrics JSON

3. Test Suggestion Creation

  • Login to dashboard
  • Navigate to /dashboard/feedback
  • Fill form and submit
  • Check database for new record
  • Verify webhook was sent to Master (check logs)

Checklist de Implementación

  • Crear InstanceMetricsController
  • Crear InstanceMetricsDto
  • Implementar InstanceMetricsAppService
  • Crear entidad SuggestionBox
  • Agregar DbSet a DbContext
  • Crear SuggestionBoxAppService
  • Crear DTOs de Suggestion
  • Crear migración de base de datos
  • Aplicar migración
  • Crear UI de feedback en Next.js
  • Agregar botón flotante al layout
  • Configurar variables de entorno
  • (Opcional) Implementar MasterEnrollmentWorker
  • Probar health endpoint
  • Probar metrics endpoint con API key
  • Probar creación de sugerencias
  • Verificar webhook a Master

Notas de Seguridad

  1. API Key: Nunca commitear el API key en el código. Usar variables de entorno o Azure Key Vault.
  2. HTTPS: Asegurar que todas las comunicaciones con Master usen HTTPS.
  3. Rate Limiting: Considerar agregar rate limiting al metrics endpoint.
  4. Validation: Validar todos los inputs en SuggestionBox para prevenir XSS.

Soporte

Para preguntas sobre la implementación, contactar al equipo de desarrollo de OSB Master.