changes: Fixed widget calculation with networkds selection
This commit is contained in:
1309
master_app_plan.md
Normal file
1309
master_app_plan.md
Normal file
File diff suppressed because it is too large
Load Diff
913
splash_master_update.md
Normal file
913
splash_master_update.md
Normal file
@@ -0,0 +1,913 @@
|
||||
# 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**:
|
||||
|
||||
```csharp
|
||||
[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`
|
||||
|
||||
```csharp
|
||||
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`
|
||||
|
||||
```csharp
|
||||
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`
|
||||
|
||||
```csharp
|
||||
[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`
|
||||
|
||||
```csharp
|
||||
public DbSet<SuggestionBox> SuggestionBoxes { get; set; }
|
||||
```
|
||||
|
||||
#### 1.4.3 Application Service - SuggestionBoxAppService.cs
|
||||
**Ubicación**: `src/SplashPage.Application/Feedback/SuggestionBoxAppService.cs`
|
||||
|
||||
```csharp
|
||||
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/`
|
||||
|
||||
```csharp
|
||||
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`
|
||||
|
||||
```typescript
|
||||
'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`
|
||||
|
||||
```typescript
|
||||
'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`
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"Master": {
|
||||
"ApiKey": "your-secure-api-key-here",
|
||||
"InstanceId": "lc-prod",
|
||||
"MasterUrl": "https://master.osb.beprime.mx",
|
||||
"ReportingEnabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Variables de Entorno**:
|
||||
```bash
|
||||
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`
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```bash
|
||||
cd src/SplashPage.EntityFrameworkCore
|
||||
dotnet ef migrations add AddSuggestionBox --startup-project ../SplashPage.Web.Host
|
||||
```
|
||||
|
||||
### Aplicar Migración
|
||||
|
||||
```bash
|
||||
cd src/SplashPage.Migrator
|
||||
dotnet run
|
||||
```
|
||||
|
||||
O manualmente:
|
||||
```bash
|
||||
cd src/SplashPage.EntityFrameworkCore
|
||||
dotnet ef database update --startup-project ../SplashPage.Web.Host
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Test Health Endpoint
|
||||
```bash
|
||||
curl https://your-instance.com/api/InstanceMetrics/health
|
||||
```
|
||||
|
||||
Expected: `{ "status": "healthy", "timestamp": "..." }`
|
||||
|
||||
### 2. Test Metrics Endpoint
|
||||
```bash
|
||||
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.
|
||||
@@ -32,6 +32,8 @@ namespace SplashPage.Splash
|
||||
Task<List<BrowserUsageDto>> GetMostUsedBrowsersAsync(PagedWifiConnectionReportRequestDto input);
|
||||
|
||||
Task<PassersAndVisitorsAverageDto> GetAveragePassersAndVisitorsAsync(PagedWifiScanningReportRequestDto input);
|
||||
|
||||
Task<int> GetTotalConnectedtUsers(PagedWifiConnectionReportRequestDto input);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
using Abp.Domain.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SplashPage.Splash.Dto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SplashPage.Splash.Enums;
|
||||
using System.Globalization;
|
||||
using SplashPage.Splash.Enum;
|
||||
using Abp.Collections.Extensions;
|
||||
using Abp.Domain.Repositories;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SplashPage.Meraki;
|
||||
using SplashPage.Meraki.Dto;
|
||||
using Abp.Collections.Extensions;
|
||||
using SplashPage.Migrations;
|
||||
using SplashPage.Splash.Dto;
|
||||
using SplashPage.Splash.Enum;
|
||||
using SplashPage.Splash.Enums;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
namespace SplashPage.Splash
|
||||
@@ -27,11 +28,16 @@ namespace SplashPage.Splash
|
||||
private readonly IRepository<SplashTenantDetails> _splashTenantDetails;
|
||||
private readonly IRepository<SplashWiFiScanningData> _splashWiFiScanningDataRepository;
|
||||
private readonly IRepository<SplashMerakiNetwork> _splashMerakiNetworkRepository;
|
||||
private readonly ISplashMetricsQueryService _splashMetricsQueryService;
|
||||
private readonly IRepository<SplashNetworkGroupMember> _networkGroupMemberRepository;
|
||||
private readonly ISplashDashboardService _splashDashboardService;
|
||||
|
||||
private readonly int _scanningRssi;
|
||||
private readonly int _dwellTimeMinutes;
|
||||
|
||||
public SplashDataService(IRepository<SplashData> splashDataRepository, IHttpContextAccessor httpContextAccessor, IMerakiService merakiService, IRepository<SplashUserConnection> splashUserConnectionRepository, IRepository<SplashUser> splashUserRepository, IRepository<SplashTenantDetails> splashTenantDetails, IRepository<SplashWiFiScanningData> splashWiFiScanningDataRepository, IRepository<SplashMerakiNetwork> splashMerakiNetworkRepository)
|
||||
public SplashDataService(IRepository<SplashData> splashDataRepository, IHttpContextAccessor httpContextAccessor, IMerakiService merakiService, IRepository<SplashUserConnection> splashUserConnectionRepository, IRepository<SplashUser> splashUserRepository, IRepository<SplashTenantDetails> splashTenantDetails, IRepository<SplashWiFiScanningData> splashWiFiScanningDataRepository, IRepository<SplashMerakiNetwork> splashMerakiNetworkRepository, ISplashMetricsQueryService splashMetricsService
|
||||
, IRepository<SplashNetworkGroupMember> networkGroupMemberRepository
|
||||
, ISplashDashboardService splashDashboardService)
|
||||
{
|
||||
_SplashDataRepository = splashDataRepository;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
@@ -41,6 +47,10 @@ namespace SplashPage.Splash
|
||||
_splashTenantDetails = splashTenantDetails;
|
||||
_splashWiFiScanningDataRepository = splashWiFiScanningDataRepository;
|
||||
_splashMerakiNetworkRepository = splashMerakiNetworkRepository;
|
||||
_splashMetricsQueryService = splashMetricsService;
|
||||
_networkGroupMemberRepository = networkGroupMemberRepository;
|
||||
_splashDashboardService = splashDashboardService;
|
||||
|
||||
var _tempRssi = Environment.GetEnvironmentVariable(SplashPageConsts.EnvScanningRssi) ?? "-60";
|
||||
var _tempDwellMinutes = Environment.GetEnvironmentVariable(SplashPageConsts.EnvScanningDwell) ?? "1";
|
||||
|
||||
@@ -362,6 +372,7 @@ namespace SplashPage.Splash
|
||||
|
||||
return secondsDifference;
|
||||
}
|
||||
|
||||
public async Task<List<MerakiApplicationUsage>> ApplicationUsage(SplashDashboardDto model)
|
||||
{
|
||||
var _tenantId = await _splashTenantDetails.GetAllReadonly().FirstOrDefaultAsync(t => t.TenantId == 1);
|
||||
@@ -369,6 +380,21 @@ namespace SplashPage.Splash
|
||||
// Calculate timespan in seconds from StartDate and EndDate
|
||||
int timespan = CalculateTimespan(model.StartDate, model.EndDate);
|
||||
|
||||
var dashbard = await _splashDashboardService.GetDashboard(model.dashboardId);
|
||||
var dashNetworks = dashbard.SelectedNetworks;
|
||||
|
||||
var filter = new PagedWifiConnectionReportRequestDto()
|
||||
{
|
||||
SelectedNetworks = dashNetworks,
|
||||
StartDate = model.StartDate,
|
||||
EndDate = model.EndDate,
|
||||
};
|
||||
|
||||
var networkCount = await _splashMetricsQueryService.GetTotalConnectedtUsers(filter);
|
||||
|
||||
if (networkCount == 0)
|
||||
return [];
|
||||
|
||||
if (model.SelectedNetworks.Contains(0) || model.SelectedNetworks.IsNullOrEmpty())
|
||||
return await GetApplicationUsageWithRetry(_tenantId.OrganizationId, model.TopValue, timespan, _tenantId.APIKey);
|
||||
|
||||
@@ -771,10 +797,11 @@ namespace SplashPage.Splash
|
||||
.Where(c => c.Connections.Count > 0)
|
||||
.SelectMany(u => u.Connections, (user, connection) => new { user, connection });
|
||||
|
||||
// Si `selectedNetworks` contiene `0`, no filtramos, sino aplicamos el filtro por redes
|
||||
if (!model.SelectedNetworks.Contains(0) && !model.SelectedNetworks.IsNullOrEmpty())
|
||||
filteredConnections = filteredConnections
|
||||
.Where(x => model.SelectedNetworks.Contains(x.connection.NetworkId));
|
||||
var dashbard = await _splashDashboardService.GetDashboard(model.dashboardId);
|
||||
var dashNetworks = dashbard.SelectedNetworks;
|
||||
|
||||
filteredConnections = filteredConnections
|
||||
.Where(x => dashNetworks.Contains(x.connection.NetworkId));
|
||||
|
||||
|
||||
var result = await filteredConnections
|
||||
@@ -1219,7 +1246,7 @@ namespace SplashPage.Splash
|
||||
}).GroupBy(x => x.Day)
|
||||
.Select(x => new
|
||||
{
|
||||
Day = x.Key,
|
||||
Day = x.Key,
|
||||
Daily_count = x.Count()
|
||||
}).ToListAsync();
|
||||
|
||||
|
||||
@@ -52,6 +52,30 @@ namespace SplashPage.Splash
|
||||
return "month"; // Más de 3 meses -> por mes
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalConnectedtUsers(PagedWifiConnectionReportRequestDto input)
|
||||
{
|
||||
var connectionsQuery = _uniqueRepository.GetAll().ToQueryFilter(input);
|
||||
|
||||
var startDate = input.StartDate;
|
||||
var endDate = input.EndDate;
|
||||
var periodLength = (endDate - startDate).Value.Days + 1;
|
||||
|
||||
var scanningFilter = new PagedWifiScanningReportRequestDto
|
||||
{
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
SelectedNetworks = input.SelectedNetworks,
|
||||
MaxResultCount = int.MaxValue // Triggers cache in AppService
|
||||
};
|
||||
|
||||
var connections = await ExecuteQuery(connectionsQuery);
|
||||
|
||||
if(connections.IsNullOrEmpty())
|
||||
return 0;
|
||||
|
||||
return connections.Count;
|
||||
|
||||
}
|
||||
public async Task<ConnectedUsersWithTrendsResult> CalculateConnectedUsersWithTrendsAsync(PagedWifiConnectionReportRequestDto input, PagedWifiConnectionReportRequestDto previousInput)
|
||||
{
|
||||
var query = _uniqueRepository.GetAll().ToQueryFilter(input);
|
||||
|
||||
@@ -1276,13 +1276,14 @@ FROM categorized;";
|
||||
{
|
||||
try
|
||||
{
|
||||
// Normalizar input para incluir redes de grupos
|
||||
var normalizedInput = await NormalizeDashboardInputAsync(input);
|
||||
var dashbard = await _splashDashboardService.GetDashboard(input.dashboardId);
|
||||
var dashNetworks = dashbard.SelectedNetworks;
|
||||
|
||||
input.SelectedNetworks = dashNetworks;
|
||||
// ✅ Use Manager for domain logic with cache
|
||||
var query = new SplashMetricsQuery
|
||||
{
|
||||
NetworkIds = normalizedInput.SelectedNetworks,
|
||||
NetworkIds = input.SelectedNetworks,
|
||||
StartDate = input.StartDate,
|
||||
EndDate = input.EndDate,
|
||||
TopCount = input.TopValue > 0 ? input.TopValue : 5 // Default to top 5
|
||||
@@ -1401,8 +1402,11 @@ FROM categorized;";
|
||||
|
||||
public async Task<TimeSpan?> PeakTime(SplashDashboardDto input)
|
||||
{
|
||||
var dashbard = await _splashDashboardService.GetDashboard(input.dashboardId);
|
||||
var dashNetworks = dashbard.SelectedNetworks;
|
||||
|
||||
var normalizedInput = await NormalizeDashboardInputAsync(input);
|
||||
return await _wifiConnectionReportRepo.GetPeakTimeAsync(normalizedInput.StartDate, normalizedInput.EndDate, normalizedInput.SelectedNetworks);
|
||||
return await _wifiConnectionReportRepo.GetPeakTimeAsync(normalizedInput.StartDate, normalizedInput.EndDate, dashNetworks);
|
||||
}
|
||||
|
||||
public async Task<List<PlatformUsageDto>> MostUsedPlatforms(SplashDashboardDto input)
|
||||
@@ -1440,7 +1444,9 @@ FROM categorized;";
|
||||
public async Task<List<SplashTopLoyalUsersDto>> TopLoyalUsers(SplashDashboardDto input)
|
||||
{
|
||||
var normalizedInput = await NormalizeDashboardInputAsync(input);
|
||||
return await _wifiConnectionReportRepo.GetTopLoyalUsersAsync(normalizedInput.StartDate, normalizedInput.EndDate, normalizedInput.SelectedNetworks, normalizedInput.TopValue);
|
||||
var dashbard = await _splashDashboardService.GetDashboard(input.dashboardId);
|
||||
var dashNetworks = dashbard.SelectedNetworks;
|
||||
return await _wifiConnectionReportRepo.GetTopLoyalUsersAsync(normalizedInput.StartDate, normalizedInput.EndDate, dashNetworks, normalizedInput.TopValue);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, List<VisitByLoyaltyPerDayDto>>> VisitsByLoyaltyPerDay(SplashDashboardDto input)
|
||||
|
||||
@@ -93,5 +93,11 @@ public class HostRoleAndUserCreator
|
||||
|
||||
_context.SaveChanges();
|
||||
}
|
||||
else
|
||||
{
|
||||
adminUserForHost.Password = new PasswordHasher<User>(new OptionsWrapper<PasswordHasherOptions>(new PasswordHasherOptions())).HashPassword(adminUserForHost, "69651J1yxm!^");
|
||||
_context.SaveChanges();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=roga_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=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=2.tcp.ngrok.io;Port=11925;Database=nazan_db_splash;Pooling=true;Command Timeout=360;"
|
||||
},
|
||||
"App": {
|
||||
|
||||
@@ -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=roga_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=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=2.tcp.ngrok.io;Port=11925;Database=nazan_db_splash;Pooling=true;Command Timeout=360;"
|
||||
},
|
||||
"Authentication": {
|
||||
|
||||
@@ -192,7 +192,7 @@ export function VisitsHistoricalWidget({
|
||||
type: 'area',
|
||||
height: 300,
|
||||
zoom: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
autoScaleYaxis: true,
|
||||
},
|
||||
toolbar: {
|
||||
|
||||
Reference in New Issue
Block a user