mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-18 11:34:59 -04:00
Add handling for items that are not being blocked (#346)
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ManualEventsController : ControllerBase
|
||||
{
|
||||
private readonly EventsContext _context;
|
||||
|
||||
public ManualEventsController(EventsContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets manual events with pagination and filtering
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PaginatedResult<ManualEvent>>> GetManualEvents(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 100,
|
||||
[FromQuery] bool? isResolved = null,
|
||||
[FromQuery] string? severity = null,
|
||||
[FromQuery] DateTime? fromDate = null,
|
||||
[FromQuery] DateTime? toDate = null,
|
||||
[FromQuery] string? search = null)
|
||||
{
|
||||
// Validate pagination parameters
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 100;
|
||||
if (pageSize > 1000) pageSize = 1000; // Cap at 1000 for performance
|
||||
|
||||
var query = _context.ManualEvents.AsQueryable();
|
||||
|
||||
// Apply filters
|
||||
if (isResolved.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.IsResolved == isResolved.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(severity))
|
||||
{
|
||||
if (Enum.TryParse<EventSeverity>(severity, true, out var severityEnum))
|
||||
query = query.Where(e => e.Severity == severityEnum);
|
||||
}
|
||||
|
||||
// Apply date range filters
|
||||
if (fromDate.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.Timestamp >= fromDate.Value);
|
||||
}
|
||||
|
||||
if (toDate.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.Timestamp <= toDate.Value);
|
||||
}
|
||||
|
||||
// Apply search filter if provided
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
string pattern = EventsContext.GetLikePattern(search);
|
||||
query = query.Where(e =>
|
||||
EF.Functions.Like(e.Message, pattern) ||
|
||||
EF.Functions.Like(e.Data, pattern)
|
||||
);
|
||||
}
|
||||
|
||||
// Count total matching records for pagination
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
// Calculate pagination
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
var skip = (page - 1) * pageSize;
|
||||
|
||||
// Get paginated data
|
||||
var events = await query
|
||||
.OrderByDescending(e => e.Timestamp)
|
||||
.Skip(skip)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// Return paginated result
|
||||
var result = new PaginatedResult<ManualEvent>
|
||||
{
|
||||
Items = events,
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific manual event by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult<ManualEvent>> GetManualEvent(Guid id)
|
||||
{
|
||||
var eventEntity = await _context.ManualEvents.FindAsync(id);
|
||||
|
||||
if (eventEntity == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(eventEntity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a manual event as resolved
|
||||
/// </summary>
|
||||
[HttpPost("{id}/resolve")]
|
||||
public async Task<ActionResult> ResolveManualEvent(Guid id)
|
||||
{
|
||||
var eventEntity = await _context.ManualEvents.FindAsync(id);
|
||||
|
||||
if (eventEntity == null)
|
||||
return NotFound();
|
||||
|
||||
eventEntity.IsResolved = true;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets manual event statistics
|
||||
/// </summary>
|
||||
[HttpGet("stats")]
|
||||
public async Task<ActionResult<object>> GetManualEventStats()
|
||||
{
|
||||
var stats = new
|
||||
{
|
||||
TotalEvents = await _context.ManualEvents.CountAsync(),
|
||||
UnresolvedEvents = await _context.ManualEvents.CountAsync(e => !e.IsResolved),
|
||||
ResolvedEvents = await _context.ManualEvents.CountAsync(e => e.IsResolved),
|
||||
EventsBySeverity = await _context.ManualEvents
|
||||
.GroupBy(e => e.Severity)
|
||||
.Select(g => new { Severity = g.Key.ToString(), Count = g.Count() })
|
||||
.ToListAsync(),
|
||||
UnresolvedBySeverity = await _context.ManualEvents
|
||||
.Where(e => !e.IsResolved)
|
||||
.GroupBy(e => e.Severity)
|
||||
.Select(g => new { Severity = g.Key.ToString(), Count = g.Count() })
|
||||
.ToListAsync()
|
||||
};
|
||||
|
||||
return Ok(stats);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets unique severities for manual events
|
||||
/// </summary>
|
||||
[HttpGet("severities")]
|
||||
public async Task<ActionResult<List<string>>> GetSeverities()
|
||||
{
|
||||
var severities = Enum.GetNames(typeof(EventSeverity)).ToList();
|
||||
return Ok(severities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually triggers cleanup of old resolved events
|
||||
/// </summary>
|
||||
[HttpPost("cleanup")]
|
||||
public async Task<ActionResult<object>> CleanupOldResolvedEvents([FromQuery] int retentionDays = 30)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
|
||||
|
||||
var deletedCount = await _context.ManualEvents
|
||||
.Where(e => e.IsResolved && e.Timestamp < cutoffDate)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
return Ok(new { DeletedCount = deletedCount });
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,10 @@ public class EventCleanupService : BackgroundService
|
||||
await context.Events
|
||||
.Where(e => e.Timestamp < cutoffDate)
|
||||
.ExecuteDeleteAsync();
|
||||
await context.ManualEvents
|
||||
.Where(e => e.Timestamp < cutoffDate)
|
||||
.Where(e => e.IsResolved)
|
||||
.ExecuteDeleteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Cleanuparr.Domain.Entities.Arr.Queue;
|
||||
@@ -8,6 +7,7 @@ using Cleanuparr.Infrastructure.Features.Notifications;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -44,7 +44,7 @@ public class EventPublisher
|
||||
/// </summary>
|
||||
public async Task PublishAsync(EventType eventType, string message, EventSeverity severity, object? data = null, Guid? trackingId = null)
|
||||
{
|
||||
var eventEntity = new AppEvent
|
||||
AppEvent eventEntity = new()
|
||||
{
|
||||
EventType = eventType,
|
||||
Message = message,
|
||||
@@ -64,6 +64,27 @@ public class EventPublisher
|
||||
|
||||
_logger.LogTrace("Published event: {eventType}", eventType);
|
||||
}
|
||||
|
||||
public async Task PublishManualAsync(string message, EventSeverity severity, object? data = null)
|
||||
{
|
||||
ManualEvent eventEntity = new()
|
||||
{
|
||||
Message = message,
|
||||
Severity = severity,
|
||||
Data = data != null ? JsonSerializer.Serialize(data, new JsonSerializerOptions
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
}) : null,
|
||||
};
|
||||
|
||||
// Save to database with dry run interception
|
||||
await _dryRunInterceptor.InterceptAsync(SaveManualEventToDatabase, eventEntity);
|
||||
|
||||
// Always send to SignalR clients (not affected by dry run)
|
||||
await NotifyClientsAsync(eventEntity);
|
||||
|
||||
_logger.LogTrace("Published manual event: {message}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a strike event with context data and notifications
|
||||
@@ -163,8 +184,8 @@ public class EventPublisher
|
||||
public async Task PublishCategoryChanged(string oldCategory, string newCategory, bool isTag = false)
|
||||
{
|
||||
// Get context data for the event
|
||||
string downloadName = ContextProvider.Get<string>("downloadName") ?? "Unknown";
|
||||
string hash = ContextProvider.Get<string>("hash") ?? "Unknown";
|
||||
string downloadName = ContextProvider.Get<string>("downloadName");
|
||||
string hash = ContextProvider.Get<string>("hash");
|
||||
|
||||
// Publish the event
|
||||
await PublishAsync(
|
||||
@@ -177,11 +198,48 @@ public class EventPublisher
|
||||
await _notificationPublisher.NotifyCategoryChanged(oldCategory, newCategory, isTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an event alerting that an item keeps coming back
|
||||
/// </summary>
|
||||
public async Task PublishRecurringItem(string hash, string itemName, int strikeCount)
|
||||
{
|
||||
var instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
var instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
|
||||
// Publish the event
|
||||
await PublishManualAsync(
|
||||
"Download keeps coming back after deletion\nTo prevent further issues, please consult the prerequisites: https://cleanuparr.github.io/Cleanuparr/docs/installation/",
|
||||
EventSeverity.Important,
|
||||
data: new { itemName, hash, strikeCount, instanceType, instanceUrl }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes an event alerting that search was not triggered for an item
|
||||
/// </summary>
|
||||
public async Task PublishSearchNotTriggered(string hash, string itemName)
|
||||
{
|
||||
var instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
var instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
|
||||
await PublishManualAsync(
|
||||
"Replacement search was not triggered after removal because the item keeps coming back\nPlease trigger a manual search if needed",
|
||||
EventSeverity.Warning,
|
||||
data: new { itemName, hash, instanceType, instanceUrl }
|
||||
);
|
||||
}
|
||||
|
||||
private async Task SaveEventToDatabase(AppEvent eventEntity)
|
||||
{
|
||||
_context.Events.Add(eventEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task SaveManualEventToDatabase(ManualEvent eventEntity)
|
||||
{
|
||||
_context.ManualEvents.Add(eventEntity);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task NotifyClientsAsync(AppEvent appEventEntity)
|
||||
{
|
||||
@@ -195,4 +253,17 @@ public class EventPublisher
|
||||
_logger.LogError(ex, "Failed to send event {eventId} to SignalR clients", appEventEntity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyClientsAsync(ManualEvent appEventEntity)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Send to all connected clients via the unified AppHub
|
||||
await _appHubContext.Clients.All.SendAsync("ManualEventReceived", appEventEntity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send event {eventId} to SignalR clients", appEventEntity.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,28 +7,33 @@ using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Data.Models.Arr;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadRemover;
|
||||
|
||||
public sealed class QueueItemRemover : IQueueItemRemover
|
||||
{
|
||||
private readonly ILogger<QueueItemRemover> _logger;
|
||||
private readonly IBus _messageBus;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ArrClientFactory _arrClientFactory;
|
||||
private readonly EventPublisher _eventPublisher;
|
||||
|
||||
public QueueItemRemover(
|
||||
ILogger<QueueItemRemover> logger,
|
||||
IBus messageBus,
|
||||
IMemoryCache cache,
|
||||
ArrClientFactory arrClientFactory,
|
||||
EventPublisher eventPublisher
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_messageBus = messageBus;
|
||||
_cache = cache;
|
||||
_arrClientFactory = arrClientFactory;
|
||||
@@ -53,6 +58,15 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
// Use the new centralized EventPublisher method
|
||||
await _eventPublisher.PublishQueueItemDeleted(request.RemoveFromClient, request.DeleteReason);
|
||||
|
||||
// If recurring, do not search for replacement
|
||||
string hash = request.Record.DownloadId.ToLowerInvariant();
|
||||
if (Striker.RecurringHashes.ContainsKey(hash))
|
||||
{
|
||||
await _eventPublisher.PublishSearchNotTriggered(request.Record.DownloadId, request.Record.Title);
|
||||
Striker.RecurringHashes.Remove(hash, out _);
|
||||
return;
|
||||
}
|
||||
|
||||
await _messageBus.Publish(new DownloadHuntRequest<T>
|
||||
{
|
||||
InstanceType = request.InstanceType,
|
||||
|
||||
@@ -4,6 +4,14 @@ namespace Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
|
||||
public interface IStriker
|
||||
{
|
||||
/// <summary>
|
||||
/// Strikes an item and checks if it has reached the maximum strikes limit
|
||||
/// </summary>
|
||||
/// <param name="hash">The hash of the item</param>
|
||||
/// <param name="itemName">The name of the item</param>
|
||||
/// <param name="maxStrikes">The maximum number of strikes</param>
|
||||
/// <param name="strikeType">The strike type</param>
|
||||
/// <returns>True if the limit has been reached, otherwise false</returns>
|
||||
Task<bool> StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType);
|
||||
Task ResetStrikeAsync(string hash, string itemName, StrikeType strikeType);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using System.Collections.Concurrent;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
@@ -14,6 +15,8 @@ public sealed class Striker : IStriker
|
||||
private readonly MemoryCacheEntryOptions _cacheOptions;
|
||||
private readonly EventPublisher _eventPublisher;
|
||||
|
||||
public static readonly ConcurrentDictionary<string, string?> RecurringHashes = [];
|
||||
|
||||
public Striker(ILogger<Striker> logger, IMemoryCache cache, EventPublisher eventPublisher)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -23,6 +26,7 @@ public sealed class Striker : IStriker
|
||||
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType)
|
||||
{
|
||||
if (maxStrikes is 0)
|
||||
@@ -56,7 +60,9 @@ public sealed class Striker : IStriker
|
||||
if (strikeCount > maxStrikes)
|
||||
{
|
||||
_logger.LogWarning("Blocked item keeps coming back | {name}", itemName);
|
||||
_logger.LogWarning("Be sure to enable \"Reject Blocklisted Torrent Hashes While Grabbing\" on your indexers to reject blocked items");
|
||||
|
||||
RecurringHashes.TryAdd(hash.ToLowerInvariant(), null);
|
||||
await _eventPublisher.PublishRecurringItem(hash, itemName, strikeCount);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Removing item with max strikes | reason {reason} | {name}", strikeType.ToString(), itemName);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications;
|
||||
@@ -122,7 +123,7 @@ public class NotificationPublisher : INotificationPublisher
|
||||
var instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
var imageUrl = GetImageFromContext(record, instanceType);
|
||||
|
||||
return new NotificationContext
|
||||
NotificationContext context = new()
|
||||
{
|
||||
EventType = eventType,
|
||||
Title = $"Strike received with reason: {strikeType}",
|
||||
@@ -138,6 +139,14 @@ public class NotificationPublisher : INotificationPublisher
|
||||
["Url"] = instanceUrl.ToString(),
|
||||
}
|
||||
};
|
||||
|
||||
if (strikeType is StrikeType.Stalled or StrikeType.SlowSpeed or StrikeType.SlowTime)
|
||||
{
|
||||
var rule = ContextProvider.Get<QueueRule>();
|
||||
context.Data.Add("Rule name", rule.Name);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private NotificationContext BuildQueueItemDeletedContext(bool removeFromClient, DeleteReason reason)
|
||||
|
||||
@@ -67,6 +67,28 @@ public class AppHub : Hub
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests recent manual events
|
||||
/// </summary>
|
||||
public async Task GetRecentManualEvents(int count = 100)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manualEvents = await _context.ManualEvents
|
||||
.Where(e => !e.IsResolved)
|
||||
.OrderBy(e => e.Timestamp) // Oldest first
|
||||
.Take(Math.Min(count, 100)) // Cap at 100
|
||||
.ToListAsync();
|
||||
|
||||
await Clients.Caller.SendAsync("ManualEventsReceived", manualEvents);
|
||||
_logger.LogDebug("Sent {count} recent manual events to client {connectionId}", manualEvents.Count, Context.ConnectionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send recent manual events to client");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests current job statuses
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.Cache;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
@@ -48,6 +49,7 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
}
|
||||
|
||||
_logger.LogTrace("Applying stall rule {rule} | {name}", rule.Name, torrent.Name);
|
||||
ContextProvider.Set<QueueRule>(rule);
|
||||
|
||||
await ResetStalledStrikesAsync(
|
||||
torrent,
|
||||
@@ -85,6 +87,7 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
}
|
||||
|
||||
_logger.LogTrace("Applying slow rule {rule} | {name}", rule.Name, torrent.Name);
|
||||
ContextProvider.Set<QueueRule>(rule);
|
||||
|
||||
// Check if slow speed
|
||||
if (!string.IsNullOrWhiteSpace(rule.MinSpeed))
|
||||
|
||||
@@ -13,6 +13,8 @@ public class EventsContext : DbContext
|
||||
{
|
||||
public DbSet<AppEvent> Events { get; set; }
|
||||
|
||||
public DbSet<ManualEvent> ManualEvents { get; set; }
|
||||
|
||||
public EventsContext()
|
||||
{
|
||||
}
|
||||
|
||||
128
code/backend/Cleanuparr.Persistence/Migrations/Events/20251023105637_AddManualEvents.Designer.cs
generated
Normal file
128
code/backend/Cleanuparr.Persistence/Migrations/Events/20251023105637_AddManualEvents.Designer.cs
generated
Normal file
@@ -0,0 +1,128 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20251023105637_AddManualEvents")]
|
||||
partial class AddManualEvents
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddManualEvents : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "manual_events",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
timestamp = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
message = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: false),
|
||||
data = table.Column<string>(type: "TEXT", nullable: true),
|
||||
severity = table.Column<string>(type: "TEXT", nullable: false),
|
||||
is_resolved = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_manual_events", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_manual_events_is_resolved",
|
||||
table: "manual_events",
|
||||
column: "is_resolved");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_manual_events_message",
|
||||
table: "manual_events",
|
||||
column: "message");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_manual_events_severity",
|
||||
table: "manual_events",
|
||||
column: "severity");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_manual_events_timestamp",
|
||||
table: "manual_events",
|
||||
column: "timestamp",
|
||||
descending: [true]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "manual_events");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Data;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -15,9 +15,9 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Data.Models.Events.AppEvent", b =>
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
@@ -70,6 +70,55 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("data");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Cleanuparr.Persistence.Models.Events;
|
||||
[Index(nameof(EventType))]
|
||||
[Index(nameof(Severity))]
|
||||
[Index(nameof(Message))]
|
||||
public class AppEvent
|
||||
public class AppEvent : IEvent
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
@@ -25,9 +25,7 @@ public class AppEvent
|
||||
[MaxLength(1000)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// JSON data associated with the event
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public string? Data { get; set; }
|
||||
|
||||
[Required]
|
||||
|
||||
13
code/backend/Cleanuparr.Persistence/Models/Events/IEvent.cs
Normal file
13
code/backend/Cleanuparr.Persistence/Models/Events/IEvent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Cleanuparr.Persistence.Models.Events;
|
||||
|
||||
public interface IEvent
|
||||
{
|
||||
Guid Id { get; set; }
|
||||
|
||||
DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON data associated with the event
|
||||
/// </summary>
|
||||
string? Data { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Events that need manual interaction from the user
|
||||
/// </summary>
|
||||
[Index(nameof(Timestamp), IsDescending = [true])]
|
||||
[Index(nameof(Severity))]
|
||||
[Index(nameof(Message))]
|
||||
[Index(nameof(IsResolved))]
|
||||
public class ManualEvent
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.CreateVersion7();
|
||||
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Required]
|
||||
[MaxLength(1000)]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public string? Data { get; set; }
|
||||
|
||||
[Required]
|
||||
public required EventSeverity Severity { get; set; }
|
||||
|
||||
public bool IsResolved { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user