diff --git a/.github/workflows/version-info.yml b/.github/workflows/version-info.yml deleted file mode 100644 index ccc37d36..00000000 --- a/.github/workflows/version-info.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Get Version Info - -on: - workflow_call: - inputs: - manual_version: - description: 'Manual version override (e.g., 1.0.0)' - required: false - type: string - default: '' - outputs: - app_version: - description: 'Application version (without v prefix)' - value: ${{ jobs.version.outputs.app_version }} - release_version: - description: 'Release version (with v prefix)' - value: ${{ jobs.version.outputs.release_version }} - is_tag: - description: 'Whether this is a tag event' - value: ${{ jobs.version.outputs.is_tag }} - repository_name: - description: 'Repository name without owner' - value: ${{ jobs.version.outputs.repository_name }} - -permissions: - contents: read - -jobs: - version: - runs-on: ubuntu-latest - outputs: - app_version: ${{ steps.version.outputs.app_version }} - release_version: ${{ steps.version.outputs.release_version }} - is_tag: ${{ steps.version.outputs.is_tag }} - repository_name: ${{ steps.version.outputs.repository_name }} - - steps: - - name: Calculate version info - id: version - run: | - repoFullName="${{ github.repository }}" - repositoryName="${repoFullName#*/}" - - if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then - # Tag event - release_version="${GITHUB_REF##refs/tags/}" - app_version="${release_version#v}" - is_tag="true" - elif [[ -n "${{ inputs.manual_version }}" ]]; then - # Manual workflow with version - app_version="${{ inputs.manual_version }}" - release_version="v${app_version}" - is_tag="false" - else - # Development build - app_version="0.0.1-dev-$(date +%Y%m%d-%H%M%S)" - release_version="v${app_version}" - is_tag="false" - fi - - echo "app_version=${app_version}" >> $GITHUB_OUTPUT - echo "release_version=${release_version}" >> $GITHUB_OUTPUT - echo "is_tag=${is_tag}" >> $GITHUB_OUTPUT - echo "repository_name=${repositoryName}" >> $GITHUB_OUTPUT - - echo "📦 Repository: ${repositoryName}" - echo "🏷️ Release Version: ${release_version}" - echo "📱 App Version: ${app_version}" - echo "🔖 Is Tag: ${is_tag}" diff --git a/code/backend/Cleanuparr.Api.Tests/Controllers/EventsResponseContractTests.cs b/code/backend/Cleanuparr.Api.Tests/Controllers/EventsResponseContractTests.cs index 56270434..12958509 100644 --- a/code/backend/Cleanuparr.Api.Tests/Controllers/EventsResponseContractTests.cs +++ b/code/backend/Cleanuparr.Api.Tests/Controllers/EventsResponseContractTests.cs @@ -98,16 +98,6 @@ public class EventsResponseContractTests : IDisposable result.Result.ShouldBeOfType(); } - [Fact] - public async Task GetEventsByTracking_ReturnsTheDocumentedListItemKeys() - { - await SeedEventAsync(); - - ActionResult> result = await _controller.GetEventsByTracking(_trackingId); - - ResponseContract.FirstItemKeys(result.Result!).ShouldBe(ListItemKeys); - } - [Fact] public async Task SingleEventShape_MatchesTheListShape() { diff --git a/code/backend/Cleanuparr.Api/Controllers/ApiDocumentationController.cs b/code/backend/Cleanuparr.Api/Controllers/ApiDocumentationController.cs deleted file mode 100644 index 6315b3f3..00000000 --- a/code/backend/Cleanuparr.Api/Controllers/ApiDocumentationController.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Cleanuparr.Api.Controllers; - -[ApiController] -[Route("api")] -[Authorize] -public class ApiDocumentationController : ControllerBase -{ - [HttpGet] - public IActionResult RedirectToSwagger() - { - return Redirect("/api/swagger"); - } -} diff --git a/code/backend/Cleanuparr.Api/Controllers/EventsController.cs b/code/backend/Cleanuparr.Api/Controllers/EventsController.cs index f70efad7..7fcf921a 100644 --- a/code/backend/Cleanuparr.Api/Controllers/EventsController.cs +++ b/code/backend/Cleanuparr.Api/Controllers/EventsController.cs @@ -153,21 +153,6 @@ public class EventsController : ControllerBase return Ok(eventItem); } - /// - /// Gets events by tracking ID - /// - [HttpGet("tracking/{trackingId}")] - public async Task>> GetEventsByTracking(Guid trackingId) - { - List events = await _context.Events - .Where(e => e.TrackingId == trackingId) - .OrderBy(e => e.Timestamp) - .Select(EventListItem.FromEvent) - .ToListAsync(); - - return Ok(events); - } - /// /// Gets unique event types /// diff --git a/code/backend/Cleanuparr.Api/Controllers/QueueRulesController.cs b/code/backend/Cleanuparr.Api/Controllers/QueueRulesController.cs deleted file mode 100644 index 30cd57b2..00000000 --- a/code/backend/Cleanuparr.Api/Controllers/QueueRulesController.cs +++ /dev/null @@ -1 +0,0 @@ -// Queue rules endpoints have moved to Cleanuparr.Api.Features.QueueCleaner.Controllers \ No newline at end of file diff --git a/code/backend/Cleanuparr.Api/DependencyInjection/ApiDI.cs b/code/backend/Cleanuparr.Api/DependencyInjection/ApiDI.cs index 1610aac4..91a3db19 100644 --- a/code/backend/Cleanuparr.Api/DependencyInjection/ApiDI.cs +++ b/code/backend/Cleanuparr.Api/DependencyInjection/ApiDI.cs @@ -98,6 +98,13 @@ public static class ApiDI // Custom SPA fallback to inject base path app.MapFallback(async context => { + // Serving the SPA here would mask unknown API routes as 200 + HTML + if (context.Request.Path.StartsWithSegments("/api")) + { + context.Response.StatusCode = 404; + return; + } + var basePath = app.Configuration.GetValue(ConfigurationKeys.BasePath) ?? "/"; // Normalize the base path (remove trailing slash if not root) diff --git a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/OrphanedFilesConfigController.cs b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/OrphanedFilesConfigController.cs index 451d703d..e0620c48 100644 --- a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/OrphanedFilesConfigController.cs +++ b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/OrphanedFilesConfigController.cs @@ -25,33 +25,6 @@ public sealed class OrphanedFilesConfigController : ControllerBase _dataContext = dataContext; } - [HttpGet("{downloadClientId}")] - public async Task GetClientConfig(Guid downloadClientId) - { - await DataContext.Lock.WaitAsync(); - try - { - var client = await _dataContext.DownloadClients - .AsNoTracking() - .FirstOrDefaultAsync(c => c.Id == downloadClientId); - - if (client is null) - { - return this.ProblemResult(StatusCodes.Status404NotFound, $"Download client with ID {downloadClientId} not found"); - } - - var config = await _dataContext.OrphanedFilesConfigs - .AsNoTracking() - .FirstOrDefaultAsync(c => c.DownloadClientConfigId == downloadClientId); - - return Ok(config is null ? null : OrphanedFilesConfigResponse.From(config)); - } - finally - { - DataContext.Lock.Release(); - } - } - [HttpPut("{downloadClientId}")] public async Task UpdateClientConfig(Guid downloadClientId, [FromBody] OrphanedFilesConfigRequest dto) { diff --git a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/SeedingRuleHelper.cs b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/SeedingRuleHelper.cs index b71453b0..f5e09c74 100644 --- a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/SeedingRuleHelper.cs +++ b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/SeedingRuleHelper.cs @@ -71,18 +71,6 @@ internal static class SeedingRuleHelper }; } - /// - /// Loads the client by ID then queries its seeding rules. - /// - public static async Task> GetForClientIdAsync(DataContext ctx, Guid clientId) - { - var client = await ctx.DownloadClients - .AsNoTracking() - .FirstOrDefaultAsync(c => c.Id == clientId); - - return client is null ? [] : await GetForClientAsync(ctx, client); - } - /// /// Filters seeding rules for a client from pre-loaded in-memory lists. /// Use this in bulk-load scenarios to avoid N+1 queries. diff --git a/code/backend/Cleanuparr.Api/Middleware/SetupGuardMiddleware.cs b/code/backend/Cleanuparr.Api/Middleware/SetupGuardMiddleware.cs index 9715af74..2de9e1dc 100644 --- a/code/backend/Cleanuparr.Api/Middleware/SetupGuardMiddleware.cs +++ b/code/backend/Cleanuparr.Api/Middleware/SetupGuardMiddleware.cs @@ -58,11 +58,6 @@ public class SetupGuardMiddleware await _next(context); } - public void ResetSetupState() - { - _setupCompleted = false; - } - private async Task IsSetupCompleted() { if (_setupCompleted) diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateAppriseProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateAppriseProviderDto.cs deleted file mode 100644 index 92dc6fa1..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateAppriseProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using CreateAppriseProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.CreateAppriseProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.CreateAppriseProviderRequest instead.")] -public sealed record CreateAppriseProviderDto : CreateAppriseProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNotifiarrProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNotifiarrProviderDto.cs deleted file mode 100644 index 8d9b39c0..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNotifiarrProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using CreateNotifiarrProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.CreateNotifiarrProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.CreateNotifiarrProviderRequest instead.")] -public sealed record CreateNotifiarrProviderDto : CreateNotifiarrProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNotificationProviderBaseDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNotificationProviderBaseDto.cs deleted file mode 100644 index 8b137891..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNotificationProviderBaseDto.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNtfyProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNtfyProviderDto.cs deleted file mode 100644 index 6750cd95..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/CreateNtfyProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using CreateNtfyProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.CreateNtfyProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.CreateNtfyProviderRequest instead.")] -public sealed record CreateNtfyProviderDto : CreateNtfyProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestAppriseProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestAppriseProviderDto.cs deleted file mode 100644 index 8f5f6168..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestAppriseProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using TestAppriseProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.TestAppriseProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.TestAppriseProviderRequest instead.")] -public sealed record TestAppriseProviderDto : TestAppriseProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestNotifiarrProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestNotifiarrProviderDto.cs deleted file mode 100644 index 3ac65c5d..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestNotifiarrProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using TestNotifiarrProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.TestNotifiarrProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.TestNotifiarrProviderRequest instead.")] -public sealed record TestNotifiarrProviderDto : TestNotifiarrProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestNtfyProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestNtfyProviderDto.cs deleted file mode 100644 index 6541774e..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/TestNtfyProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using TestNtfyProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.TestNtfyProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.TestNtfyProviderRequest instead.")] -public sealed record TestNtfyProviderDto : TestNtfyProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateAppriseProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateAppriseProviderDto.cs deleted file mode 100644 index 09449235..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateAppriseProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using UpdateAppriseProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.UpdateAppriseProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.UpdateAppriseProviderRequest instead.")] -public sealed record UpdateAppriseProviderDto : UpdateAppriseProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateNotifiarrProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateNotifiarrProviderDto.cs deleted file mode 100644 index 48355d01..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateNotifiarrProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using UpdateNotifiarrProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.UpdateNotifiarrProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.UpdateNotifiarrProviderRequest instead.")] -public sealed record UpdateNotifiarrProviderDto : UpdateNotifiarrProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateNtfyProviderDto.cs b/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateNtfyProviderDto.cs deleted file mode 100644 index 86acb86c..00000000 --- a/code/backend/Cleanuparr.Api/Models/NotificationProviders/UpdateNtfyProviderDto.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; - -namespace Cleanuparr.Api.Models.NotificationProviders; - -using UpdateNtfyProviderRequest = Cleanuparr.Api.Features.Notifications.Contracts.Requests.UpdateNtfyProviderRequest; - -[Obsolete("Use Cleanuparr.Api.Features.Notifications.Contracts.Requests.UpdateNtfyProviderRequest instead.")] -public sealed record UpdateNtfyProviderDto : UpdateNtfyProviderRequest; diff --git a/code/backend/Cleanuparr.Api/Models/QueueCleaner/QueueRuleDto.cs b/code/backend/Cleanuparr.Api/Models/QueueCleaner/QueueRuleDto.cs deleted file mode 100644 index 589cd48e..00000000 --- a/code/backend/Cleanuparr.Api/Models/QueueCleaner/QueueRuleDto.cs +++ /dev/null @@ -1 +0,0 @@ -// Moved to Cleanuparr.Api.Features.QueueCleaner.Contracts.Requests diff --git a/code/backend/Cleanuparr.Api/Models/QueueCleaner/SlowRuleDto.cs b/code/backend/Cleanuparr.Api/Models/QueueCleaner/SlowRuleDto.cs deleted file mode 100644 index febb8348..00000000 --- a/code/backend/Cleanuparr.Api/Models/QueueCleaner/SlowRuleDto.cs +++ /dev/null @@ -1 +0,0 @@ -// Moved to Cleanuparr.Api.Features.QueueCleaner.Contracts.Requests \ No newline at end of file diff --git a/code/backend/Cleanuparr.Api/Models/QueueCleaner/StallRuleDto.cs b/code/backend/Cleanuparr.Api/Models/QueueCleaner/StallRuleDto.cs deleted file mode 100644 index febb8348..00000000 --- a/code/backend/Cleanuparr.Api/Models/QueueCleaner/StallRuleDto.cs +++ /dev/null @@ -1 +0,0 @@ -// Moved to Cleanuparr.Api.Features.QueueCleaner.Contracts.Requests \ No newline at end of file diff --git a/code/backend/Cleanuparr.Application/Features/BlacklistSync/BlacklistSynchronizer.cs b/code/backend/Cleanuparr.Application/Features/BlacklistSync/BlacklistSynchronizer.cs deleted file mode 100644 index 41a3f274..00000000 --- a/code/backend/Cleanuparr.Application/Features/BlacklistSync/BlacklistSynchronizer.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using Cleanuparr.Domain.Enums; -using Cleanuparr.Infrastructure.Features.DownloadClient; -using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent; -using Cleanuparr.Infrastructure.Features.Jobs; -using Cleanuparr.Infrastructure.Helpers; -using Cleanuparr.Infrastructure.Interceptors; -using Cleanuparr.Persistence; -using Cleanuparr.Persistence.Models.Configuration; -using Cleanuparr.Persistence.Models.Configuration.BlacklistSync; -using Cleanuparr.Persistence.Models.State; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace Cleanuparr.Application.Features.BlacklistSync; - -public sealed class BlacklistSynchronizer : IHandler -{ - private readonly ILogger _logger; - private readonly DataContext _dataContext; - private readonly DownloadServiceFactory _downloadServiceFactory; - private readonly FileReader _fileReader; - private readonly IDryRunInterceptor _dryRunInterceptor; - - public BlacklistSynchronizer( - ILogger logger, - DataContext dataContext, - DownloadServiceFactory downloadServiceFactory, - FileReader fileReader, - IDryRunInterceptor dryRunInterceptor - ) - { - _logger = logger; - _dataContext = dataContext; - _downloadServiceFactory = downloadServiceFactory; - _fileReader = fileReader; - _dryRunInterceptor = dryRunInterceptor; - } - - public async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - BlacklistSyncConfig config = await _dataContext.BlacklistSyncConfigs - .AsNoTracking() - .FirstAsync(); - - if (!config.Enabled) - { - _logger.LogDebug("Blacklist sync is disabled"); - return; - } - - if (string.IsNullOrWhiteSpace(config.BlacklistPath)) - { - _logger.LogWarning("Blacklist sync path is not configured"); - return; - } - - string[] patterns = await _fileReader.ReadContentAsync(config.BlacklistPath); - string excludedFileNames = string.Join('\n', patterns.Where(p => !string.IsNullOrWhiteSpace(p))); - - string currentHash = ComputeHash(excludedFileNames); - - await _dryRunInterceptor.InterceptAsync(() => SyncBlacklist(currentHash, excludedFileNames)); - await _dryRunInterceptor.InterceptAsync(() => RemoveOldSyncDataAsync(currentHash)); - - _logger.LogDebug("Blacklist synchronization completed"); - } - - private async Task SyncBlacklist(string currentHash, string excludedFileNames) - { - List qBittorrentClients = await _dataContext.DownloadClients - .AsNoTracking() - .Where(c => c.Enabled && c.TypeName == DownloadClientTypeName.qBittorrent) - .ToListAsync(); - - if (qBittorrentClients.Count is 0) - { - _logger.LogDebug("No enabled qBittorrent clients found for blacklist sync"); - return; - } - - _logger.LogDebug("Starting blacklist synchronization for {Count} qBittorrent clients", qBittorrentClients.Count); - - // Pull existing sync history for this hash - var alreadySynced = await _dataContext.BlacklistSyncHistory - .AsNoTracking() - .Where(s => s.Hash == currentHash) - .Select(x => x.DownloadClientId) - .ToListAsync(); - - // Only update clients not present in history for current hash - foreach (var clientConfig in qBittorrentClients) - { - try - { - if (alreadySynced.Contains(clientConfig.Id)) - { - _logger.LogDebug("Client {ClientName} already synced for current blacklist, skipping", clientConfig.Name); - continue; - } - - var downloadService = _downloadServiceFactory.GetDownloadService(clientConfig); - if (downloadService is not QBitService qBitService) - { - _logger.LogError("Expected QBitService but got {ServiceType} for client {ClientName}", downloadService.GetType().Name, clientConfig.Name); - continue; - } - - try - { - await qBitService.LoginAsync(); - await qBitService.UpdateBlacklistAsync(excludedFileNames); - - _logger.LogDebug("Successfully updated blacklist for qBittorrent client {ClientName}", clientConfig.Name); - - // Insert history row marking this client as synced for current hash - _dataContext.BlacklistSyncHistory.Add(new BlacklistSyncHistory - { - Hash = currentHash, - DownloadClientId = clientConfig.Id - }); - await _dataContext.SaveChangesAsync(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update blacklist for qBittorrent client {ClientName}", clientConfig.Name); - } - finally - { - qBitService.Dispose(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create download service for client {ClientName}", clientConfig.Name); - } - } - } - - private static string ComputeHash(string excludedFileNames) - { - using var sha = SHA256.Create(); - byte[] bytes = Encoding.UTF8.GetBytes(excludedFileNames); - byte[] hash = sha.ComputeHash(bytes); - return Convert.ToHexString(hash).ToLowerInvariant(); - } - - private async Task RemoveOldSyncDataAsync(string currentHash) - { - try - { - await _dataContext.BlacklistSyncHistory - .Where(s => s.Hash != currentHash) - .ExecuteDeleteAsync(); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to cleanup old blacklist sync history"); - } - } -} diff --git a/code/backend/Cleanuparr.Domain/Entities/Cache/StalledCacheItem.cs b/code/backend/Cleanuparr.Domain/Entities/Cache/StalledCacheItem.cs deleted file mode 100644 index e455d794..00000000 --- a/code/backend/Cleanuparr.Domain/Entities/Cache/StalledCacheItem.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Cleanuparr.Domain.Entities.Cache; - -public sealed record StalledCacheItem -{ - /// - /// The amount of bytes that have been downloaded. - /// - public long Downloaded { get; set; } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Domain/Entities/Deluge/Response/DelugeTorrent.cs b/code/backend/Cleanuparr.Domain/Entities/Deluge/Response/DelugeTorrent.cs deleted file mode 100644 index ee1e91a0..00000000 --- a/code/backend/Cleanuparr.Domain/Entities/Deluge/Response/DelugeTorrent.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Cleanuparr.Domain.Entities.Deluge.Response; - -public record DelugeTorrent -{ - [JsonPropertyName("comment")] - public string Comment { get; set; } - - [JsonPropertyName("is_seed")] - public bool IsSeed { get; set; } - - [JsonPropertyName("hash")] - public string Hash { get; set; } - - [JsonPropertyName("paused")] - public bool Paused { get; set; } - - [JsonPropertyName("ratio")] - public double Ratio { get; set; } - - [JsonPropertyName("message")] - public string Message { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("label")] - public string Label { get; set; } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Domain/Entities/Deluge/Response/DelugeTorrentExtended.cs b/code/backend/Cleanuparr.Domain/Entities/Deluge/Response/DelugeTorrentExtended.cs deleted file mode 100644 index dfcb497d..00000000 --- a/code/backend/Cleanuparr.Domain/Entities/Deluge/Response/DelugeTorrentExtended.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Cleanuparr.Domain.Entities.Deluge.Response; - -public sealed record DelugeTorrentExtended : DelugeTorrent -{ - [JsonPropertyName("total_done")] - public long TotalDone { get; set; } - - [JsonPropertyName("total_payload_download")] - public long TotalPayloadDownload { get; set; } - - [JsonPropertyName("total_uploaded")] - public long TotalUploaded { get; set; } - - [JsonPropertyName("next_announce")] - public int NextAnnounce { get; set; } - - [JsonPropertyName("tracker_status")] - public string TrackerStatus { get; set; } - - [JsonPropertyName("num_pieces")] - public int NumPieces { get; set; } - - [JsonPropertyName("piece_length")] - public long PieceLength { get; set; } - - [JsonPropertyName("is_auto_managed")] - public bool IsAutoManaged { get; set; } - - [JsonPropertyName("active_time")] - public long ActiveTime { get; set; } - - [JsonPropertyName("seeding_time")] - public long SeedingTime { get; set; } - - [JsonPropertyName("time_since_transfer")] - public long TimeSinceTransfer { get; set; } - - [JsonPropertyName("seed_rank")] - public int SeedRank { get; set; } - - [JsonPropertyName("last_seen_complete")] - public long LastSeenComplete { get; set; } - - [JsonPropertyName("completed_time")] - public long CompletedTime { get; set; } - - [JsonPropertyName("owner")] public string Owner { get; set; } - - [JsonPropertyName("public")] - public bool Public { get; set; } - - [JsonPropertyName("shared")] - public bool Shared { get; set; } - - [JsonPropertyName("queue")] public int Queue { get; set; } - - [JsonPropertyName("total_wanted")] - public long TotalWanted { get; set; } - - [JsonPropertyName("state")] public string State { get; set; } - - [JsonPropertyName("progress")] - public float Progress { get; set; } - - [JsonPropertyName("num_seeds")] - public int NumSeeds { get; set; } - - [JsonPropertyName("total_seeds")] - public int TotalSeeds { get; set; } - - [JsonPropertyName("num_peers")] - public int NumPeers { get; set; } - - [JsonPropertyName("total_peers")] - public int TotalPeers { get; set; } - - [JsonPropertyName("download_payload_rate")] - public long DownloadPayloadRate { get; set; } - - [JsonPropertyName("upload_payload_rate")] - public long UploadPayloadRate { get; set; } - - [JsonPropertyName("eta")] public long Eta { get; set; } - - [JsonPropertyName("distributed_copies")] - public float DistributedCopies { get; set; } - - [JsonPropertyName("time_added")] - public int TimeAdded { get; set; } - - [JsonPropertyName("tracker_host")] - public string TrackerHost { get; set; } - - [JsonPropertyName("download_location")] - public string DownloadLocation { get; set; } - - [JsonPropertyName("total_remaining")] - public long TotalRemaining { get; set; } - - [JsonPropertyName("max_download_speed")] - public long MaxDownloadSpeed { get; set; } - - [JsonPropertyName("max_upload_speed")] - public long MaxUploadSpeed { get; set; } - - [JsonPropertyName("seeds_peers_ratio")] - public float SeedsPeersRatio { get; set; } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Domain/Enums/PushoverSound.cs b/code/backend/Cleanuparr.Domain/Enums/PushoverSound.cs deleted file mode 100644 index e9904200..00000000 --- a/code/backend/Cleanuparr.Domain/Enums/PushoverSound.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Cleanuparr.Domain.Enums; - -public static class PushoverSounds -{ - public const string Pushover = "pushover"; - public const string Bike = "bike"; - public const string Bugle = "bugle"; - public const string Cashregister = "cashregister"; - public const string Classical = "classical"; - public const string Cosmic = "cosmic"; - public const string Falling = "falling"; - public const string Gamelan = "gamelan"; - public const string Incoming = "incoming"; - public const string Intermission = "intermission"; - public const string Magic = "magic"; - public const string Mechanical = "mechanical"; - public const string Pianobar = "pianobar"; - public const string Siren = "siren"; - public const string Spacealarm = "spacealarm"; - public const string Tugboat = "tugboat"; - public const string Alien = "alien"; - public const string Climb = "climb"; - public const string Persistent = "persistent"; - public const string Echo = "echo"; - public const string Updown = "updown"; - public const string Vibrate = "vibrate"; - public const string None = "none"; - - public static readonly string[] All = - [ - Pushover, Bike, Bugle, Cashregister, Classical, Cosmic, Falling, - Gamelan, Incoming, Intermission, Magic, Mechanical, Pianobar, - Siren, Spacealarm, Tugboat, Alien, Climb, Persistent, Echo, - Updown, Vibrate, None - ]; -} diff --git a/code/backend/Cleanuparr.Domain/Exceptions/DelugeLoginException.cs b/code/backend/Cleanuparr.Domain/Exceptions/DelugeLoginException.cs deleted file mode 100644 index 1bbe2edd..00000000 --- a/code/backend/Cleanuparr.Domain/Exceptions/DelugeLoginException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Cleanuparr.Domain.Exceptions; - -public sealed class DelugeLoginException : DelugeClientException -{ - public DelugeLoginException() : base("login failed") - { - } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Domain/Exceptions/DelugeLogoutException.cs b/code/backend/Cleanuparr.Domain/Exceptions/DelugeLogoutException.cs deleted file mode 100644 index cee60d84..00000000 --- a/code/backend/Cleanuparr.Domain/Exceptions/DelugeLogoutException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Cleanuparr.Domain.Exceptions; - -public sealed class DelugeLogoutException : DelugeClientException -{ - public DelugeLogoutException() : base("logout failed") - { - } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Services/RuleIntervalValidatorTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Services/RuleIntervalValidatorTests.cs index 1992e816..d9b670c0 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Services/RuleIntervalValidatorTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Services/RuleIntervalValidatorTests.cs @@ -143,59 +143,7 @@ public class RuleIntervalValidatorTests result.IsValid.ShouldBeTrue(); } - [Fact] - public void FindGapsInCoverage_ReturnsFullGapWhenNoRules() - { - var gaps = _validator.FindGapsInCoverage(new List()); - gaps.ShouldNotBeEmpty(); - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Public).ShouldBe(1); - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Private).ShouldBe(1); - - gaps.First(g => g.PrivacyType == TorrentPrivacyType.Public).ShouldSatisfyAllConditions( - gap => gap.Start.ShouldBe(0), - gap => gap.End.ShouldBe(100) - ); - } - - [Fact] - public void FindGapsInCoverage_UsesMinimumBoundaries() - { - var rules = new List - { - new() - { - Id = Guid.NewGuid(), - Name = "Partial", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 0, - MaxCompletionPercentage = 40 - }, - new() - { - Id = Guid.NewGuid(), - Name = "Upper", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 60, - MaxCompletionPercentage = 90 - } - }; - - var gaps = _validator.FindGapsInCoverage(rules); - - var publicGap = gaps.FirstOrDefault(g => g.PrivacyType == TorrentPrivacyType.Public && g.Start >= 40 && g.End <= 60); - publicGap.ShouldNotBeNull(); - publicGap!.Start.ShouldBe(40); - publicGap.End.ShouldBe(60); - - var privateGap = gaps.First(g => g.PrivacyType == TorrentPrivacyType.Private); - privateGap.Start.ShouldBe(0); - privateGap.End.ShouldBe(100); - } [Fact] public void ValidateSlowRuleIntervals_AllowsTouchingRanges() @@ -301,86 +249,8 @@ public class RuleIntervalValidatorTests result.Details.ShouldNotBeEmpty(); } - [Fact] - public void FindGapsInCoverage_NoGapsWhenFullyCovered() - { - var rules = new List - { - new() - { - Id = Guid.NewGuid(), - Name = "Lower", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 0, - MaxCompletionPercentage = 50 - }, - new() - { - Id = Guid.NewGuid(), - Name = "Upper", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 50, - MaxCompletionPercentage = 100 - } - }; - var gaps = _validator.FindGapsInCoverage(rules); - // No public gaps expected - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Public).ShouldBe(0); - } - - [Fact] - public void FindGapsInCoverage_NoGapsWhenBothRuleCoversAll() - { - var rules = new List - { - new() - { - Id = Guid.NewGuid(), - Name = "BothCoverage", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Both, - MinCompletionPercentage = 0, - MaxCompletionPercentage = 100 - } - }; - - var gaps = _validator.FindGapsInCoverage(rules); - - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Public).ShouldBe(0); - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Private).ShouldBe(0); - } - - [Fact] - public void FindGapsInCoverage_ClampsBounds() - { - var rules = new List - { - new() - { - Id = Guid.NewGuid(), - Name = "OutOfRange", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 20, - MaxCompletionPercentage = 150 - } - }; - - var gaps = _validator.FindGapsInCoverage(rules); - - var publicGap = gaps.FirstOrDefault(g => g.PrivacyType == TorrentPrivacyType.Public); - publicGap.ShouldNotBeNull(); - publicGap!.Start.ShouldBe(0); - publicGap.End.ShouldBe(20); - } [Fact] public void ValidateStallRuleIntervals_IgnoresDisabledRules() @@ -583,61 +453,7 @@ public class RuleIntervalValidatorTests result.IsValid.ShouldBeTrue(); } - [Fact] - public void FindGapsInCoverage_IgnoresInvalidIntervalsWhereMaxLessThanMin() - { - var rules = new List - { - new() - { - Id = Guid.NewGuid(), - Name = "Invalid", - Enabled = true, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 80, - MaxCompletionPercentage = 20 - } - }; - // Invalid interval should be ignored, resulting in full gap - var gaps = _validator.FindGapsInCoverage(rules); - - var publicGap = gaps.First(g => g.PrivacyType == TorrentPrivacyType.Public); - publicGap.Start.ShouldBe(0); - publicGap.End.ShouldBe(100); - } - - [Fact] - public void FindGapsInCoverage_IgnoresDisabledRulesWhenCalculatingCoverage() - { - var rules = new List - { - new() - { - Id = Guid.NewGuid(), - Name = "Disabled", - Enabled = false, - MaxStrikes = 3, - PrivacyType = TorrentPrivacyType.Public, - MinCompletionPercentage = 0, - MaxCompletionPercentage = 100 - } - }; - - var gaps = _validator.FindGapsInCoverage(rules); - - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Public).ShouldBe(1); - gaps.Count(g => g.PrivacyType == TorrentPrivacyType.Private).ShouldBe(1); - - var publicGap = gaps.First(g => g.PrivacyType == TorrentPrivacyType.Public); - publicGap.Start.ShouldBe(0); - publicGap.End.ShouldBe(100); - - var privateGap = gaps.First(g => g.PrivacyType == TorrentPrivacyType.Private); - privateGap.Start.ShouldBe(0); - privateGap.End.ShouldBe(100); - } [Fact] public void ValidateStallRuleIntervals_DoesNotReportSelfOverlapWhenEnablingDisabledRule() diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs index 1a44229f..3bc426ec 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs @@ -89,37 +89,6 @@ public sealed class DelugeClient return id is { ValueKind: JsonValueKind.String } hostId ? hostId.GetString() : null; } - - public async Task> ListTorrents(Dictionary? filters = null) - { - filters ??= new Dictionary(); - var keys = typeof(DelugeTorrent).GetAllJsonPropertyFromType(); - Dictionary? result = - await SendRequest>("core.get_torrents_status", filters, keys); - return result?.Values.ToList() ?? []; - } - - public async Task> ListTorrentsExtended(Dictionary? filters = null) - { - filters ??= new Dictionary(); - var keys = typeof(DelugeTorrentExtended).GetAllJsonPropertyFromType(); - Dictionary? result = - await SendRequest>("core.get_torrents_status", filters, keys); - return result?.Values.ToList() ?? []; - } - - public async Task GetTorrent(string hash) - { - List torrents = await ListTorrents(new Dictionary() { { "hash", hash } }); - return torrents.FirstOrDefault(); - } - - public async Task GetTorrentExtended(string hash) - { - List torrents = - await ListTorrentsExtended(new Dictionary { { "hash", hash } }); - return torrents.FirstOrDefault(); - } public async Task GetTorrentStatus(string hash) { diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs index 74471783..5364ea1b 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs @@ -26,12 +26,6 @@ public sealed class DelugeClientWrapper : IDelugeClientWrapper public Task GetTorrentFiles(string hash) => _client.GetTorrentFiles(hash); - public Task GetTorrent(string hash) - => _client.GetTorrent(hash); - - public Task GetTorrentExtended(string hash) - => _client.GetTorrentExtended(hash); - public Task?> GetStatusForAllTorrents() => _client.GetStatusForAllTorrents(); diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeService.cs index c1b8093a..1d4f4b4c 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeService.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeService.cs @@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging; namespace Cleanuparr.Infrastructure.Features.DownloadClient.Deluge; -public partial class DelugeService : DownloadService, IDelugeService +public partial class DelugeService : DownloadService { private readonly IDelugeClientWrapper _client; diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs index 65fbb94d..4628d198 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs @@ -9,8 +9,6 @@ public interface IDelugeClientWrapper Task Connect(); Task GetTorrentStatus(string hash); Task GetTorrentFiles(string hash); - Task GetTorrent(string hash); - Task GetTorrentExtended(string hash); Task?> GetStatusForAllTorrents(); Task DeleteTorrents(List hashes, bool removeData); Task PauseTorrents(List hashes); diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeService.cs deleted file mode 100644 index 83ca3894..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Cleanuparr.Infrastructure.Features.DownloadClient.Deluge; - -public interface IDelugeService : IDownloadService -{ -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs index 4f2a8f11..f16bf4d8 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs @@ -333,26 +333,6 @@ public abstract class DownloadService : IDownloadService return new(); } - protected string? GetRootWithFirstDirectory(string path) - { - if (string.IsNullOrWhiteSpace(path)) - { - return null; - } - - string? root = Path.GetPathRoot(path); - - if (root is null) - { - return null; - } - - string relativePath = path[root.Length..].TrimStart(Path.DirectorySeparatorChar); - string[] parts = relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); - - return parts.Length > 0 ? Path.Combine(root, parts[0]) : root; - } - private bool DownloadReachedRatio(double ratio, TimeSpan seedingTime, ISeedingRule seedingRule) { if (seedingRule.MaxRatio < 0) diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/IQBitService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/IQBitService.cs deleted file mode 100644 index 5735e5e4..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/IQBitService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent; - -public interface IQBitService : IDownloadService, IDisposable -{ - Task UpdateBlacklistAsync(string blacklistPath); -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitService.cs index 0a75f872..137f927b 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitService.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitService.cs @@ -17,7 +17,7 @@ using QBittorrent.Client; namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent; -public partial class QBitService : DownloadService, IQBitService +public partial class QBitService : DownloadService, IDisposable { protected readonly IQBittorrentClientWrapper _client; private bool? _altSpeedLimitActive; diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/IRTorrentService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/IRTorrentService.cs deleted file mode 100644 index d564b7d5..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/IRTorrentService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent; - -public interface IRTorrentService : IDownloadService -{ -} diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentService.cs index 87460936..fba7f88d 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentService.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/RTorrent/RTorrentService.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; namespace Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent; -public partial class RTorrentService : DownloadService, IRTorrentService +public partial class RTorrentService : DownloadService { private readonly IRTorrentClientWrapper _client; diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/ITransmissionService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/ITransmissionService.cs deleted file mode 100644 index 94dc0c43..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/ITransmissionService.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission; - -public interface ITransmissionService : IDownloadService -{ -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionService.cs index 244ff258..d61504c3 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionService.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionService.cs @@ -16,7 +16,7 @@ using Transmission.API.RPC.Entity; namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission; -public partial class TransmissionService : DownloadService, ITransmissionService +public partial class TransmissionService : DownloadService { private readonly ITransmissionClientWrapper _client; private bool? _altSpeedLimitActive; diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentService.cs deleted file mode 100644 index 58bff7c7..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentService.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent; - -/// -/// Interface for µTorrent download service -/// -public interface IUTorrentService : IDownloadService -{ -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentService.cs index 0dbc2be5..54235f2d 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentService.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentService.cs @@ -16,7 +16,7 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent; /// µTorrent download service implementation /// Provides business logic layer on top of UTorrentClient /// -public partial class UTorrentService : DownloadService, IUTorrentService +public partial class UTorrentService : DownloadService { private readonly IUTorrentClientWrapper _client; diff --git a/code/backend/Cleanuparr.Infrastructure/Helpers/CacheKeys.cs b/code/backend/Cleanuparr.Infrastructure/Helpers/CacheKeys.cs index 7fd0610f..521fa1e8 100644 --- a/code/backend/Cleanuparr.Infrastructure/Helpers/CacheKeys.cs +++ b/code/backend/Cleanuparr.Infrastructure/Helpers/CacheKeys.cs @@ -23,6 +23,5 @@ public static class CacheKeys public static class UTorrent { public static string GetAuthTokenKey(string clientId) => $"utorrent:auth:token:{clientId}"; - public static string GetGuidCookieKey(string clientId) => $"utorrent:auth:cookie:{clientId}"; } } \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/HttpClientConfigStore.cs b/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/HttpClientConfigStore.cs index e6d67ec8..0d30af39 100644 --- a/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/HttpClientConfigStore.cs +++ b/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/HttpClientConfigStore.cs @@ -8,7 +8,6 @@ namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem; public class HttpClientConfigStore : IHttpClientConfigStore { private readonly ConcurrentDictionary _configurations = new(); - private readonly ConcurrentDictionary _retryConfigurations = new(); public bool TryGetConfiguration(string clientName, out HttpClientConfig config) { @@ -23,17 +22,6 @@ public class HttpClientConfigStore : IHttpClientConfigStore public void RemoveConfiguration(string clientName) { _configurations.TryRemove(clientName, out _); - _retryConfigurations.TryRemove(clientName, out _); - } - - public void AddRetryConfiguration(string clientName, RetryConfig retryConfig) - { - _retryConfigurations.AddOrUpdate(clientName, retryConfig, (key, oldValue) => retryConfig); - } - - public bool TryGetRetryConfiguration(string clientName, out RetryConfig retryConfig) - { - return _retryConfigurations.TryGetValue(clientName, out retryConfig!); } public IEnumerable> GetAllConfigurations() diff --git a/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/IHttpClientConfigStore.cs b/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/IHttpClientConfigStore.cs index 2f03ac34..670f1345 100644 --- a/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/IHttpClientConfigStore.cs +++ b/code/backend/Cleanuparr.Infrastructure/Http/DynamicHttpClientSystem/IHttpClientConfigStore.cs @@ -20,11 +20,6 @@ public interface IHttpClientConfigStore /// void RemoveConfiguration(string clientName); - /// - /// Adds or updates a retry configuration for the specified client name - /// - void AddRetryConfiguration(string clientName, RetryConfig retryConfig); - /// /// Gets all currently registered configurations /// diff --git a/code/backend/Cleanuparr.Infrastructure/Logging/LogBuffer.cs b/code/backend/Cleanuparr.Infrastructure/Logging/LogBuffer.cs deleted file mode 100644 index 052c4d17..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Logging/LogBuffer.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Concurrent; - -namespace Cleanuparr.Infrastructure.Logging; - -/// -/// Maintains a buffer of recent log entries for newly connected clients -/// -public class LogBuffer -{ - private readonly ConcurrentQueue _recentLogs; - private readonly int _bufferSize; - - public LogBuffer(int bufferSize) - { - _bufferSize = Math.Max(10, bufferSize); - _recentLogs = new ConcurrentQueue(); - } - - /// - /// Adds a log entry to the buffer - /// - /// The log event to buffer - public void AddLog(object logEvent) - { - _recentLogs.Enqueue(logEvent); - - // Trim buffer if it exceeds size - while (_recentLogs.Count > _bufferSize && _recentLogs.TryDequeue(out _)) { } - } - - /// - /// Gets all buffered log entries - /// - /// Collection of recent log events - public IEnumerable GetRecentLogs() => _recentLogs.ToArray(); -} diff --git a/code/backend/Cleanuparr.Infrastructure/Models/IntervalGap.cs b/code/backend/Cleanuparr.Infrastructure/Models/IntervalGap.cs deleted file mode 100644 index 366579b9..00000000 --- a/code/backend/Cleanuparr.Infrastructure/Models/IntervalGap.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Cleanuparr.Domain.Enums; - -namespace Cleanuparr.Infrastructure.Models; - -public class IntervalGap -{ - public TorrentPrivacyType PrivacyType { get; set; } - public double Start { get; set; } - public double End { get; set; } - - public string GetPrivacyTypeDisplayName() - { - return PrivacyType switch - { - TorrentPrivacyType.Public => "Public", - TorrentPrivacyType.Private => "Private", - TorrentPrivacyType.Both => "Both", - _ => "Unknown" - }; - } -} diff --git a/code/backend/Cleanuparr.Infrastructure/Services/Interfaces/IRuleIntervalValidator.cs b/code/backend/Cleanuparr.Infrastructure/Services/Interfaces/IRuleIntervalValidator.cs index b4d204b6..55ded075 100644 --- a/code/backend/Cleanuparr.Infrastructure/Services/Interfaces/IRuleIntervalValidator.cs +++ b/code/backend/Cleanuparr.Infrastructure/Services/Interfaces/IRuleIntervalValidator.cs @@ -20,12 +20,4 @@ public interface IRuleIntervalValidator /// Existing rules (excluding the one being updated if applicable) /// Validation result with error details if invalid ValidationResult ValidateSlowRuleIntervals(SlowRule newRule, List existingRules); - - /// - /// Finds gaps in coverage for a given set of rules - /// - /// Rule type (StallRule or SlowRule) - /// List of rules to analyze - /// List of gaps in coverage - List FindGapsInCoverage(List rules) where T : QueueRule; } diff --git a/code/backend/Cleanuparr.Infrastructure/Services/RuleIntervalValidator.cs b/code/backend/Cleanuparr.Infrastructure/Services/RuleIntervalValidator.cs index d4c001d4..00075fe7 100644 --- a/code/backend/Cleanuparr.Infrastructure/Services/RuleIntervalValidator.cs +++ b/code/backend/Cleanuparr.Infrastructure/Services/RuleIntervalValidator.cs @@ -35,22 +35,6 @@ public class RuleIntervalValidator : IRuleIntervalValidator return ValidateRuleIntervals(allRules, newRule.Name); } - public List FindGapsInCoverage(List rules) where T : QueueRule - { - _logger.LogDebug("Finding gaps in coverage for {rule} rules", rules.Count); - - var gaps = new List(); - var enabledRules = rules.Where(r => r.Enabled).ToList(); - - // Find gaps for each privacy type - gaps.AddRange(FindGapsForPrivacyType(enabledRules, TorrentPrivacyType.Public)); - gaps.AddRange(FindGapsForPrivacyType(enabledRules, TorrentPrivacyType.Private)); - - _logger.LogDebug("Found {GapCount} gaps in coverage", gaps.Count); - - return gaps; - } - /// /// Validates that the provided rules do not create overlapping intervals. /// @@ -204,75 +188,6 @@ public class RuleIntervalValidator : IRuleIntervalValidator return overlaps; } - private static List FindGapsForPrivacyType(List rules, TorrentPrivacyType privacyType) where T : QueueRule - { - var gaps = new List(); - - // Get relevant intervals for this privacy type - var relevantRules = rules.Where(r => - r.PrivacyType == privacyType || - r.PrivacyType == TorrentPrivacyType.Both).ToList(); - - if (!relevantRules.Any()) - { - gaps.Add(new IntervalGap - { - PrivacyType = privacyType, - Start = 0, - End = 100 - }); - return gaps; - } - - var intervals = relevantRules - .Select(r => new - { - Start = Math.Max(0, Math.Min(100, (int)r.MinCompletionPercentage)), - End = Math.Max(0, Math.Min(100, (int)r.MaxCompletionPercentage)) - }) - .Where(i => i.End >= i.Start) - .OrderBy(i => i.Start) - .ThenBy(i => i.End) - .ToList(); - - double currentCoverageEnd = 0; - - foreach (var interval in intervals) - { - if (interval.Start > currentCoverageEnd) - { - gaps.Add(new IntervalGap - { - PrivacyType = privacyType, - Start = currentCoverageEnd, - End = interval.Start - }); - } - - if (interval.End > currentCoverageEnd) - { - currentCoverageEnd = interval.End; - } - - if (currentCoverageEnd >= 100) - { - break; - } - } - - if (currentCoverageEnd < 100) - { - gaps.Add(new IntervalGap - { - PrivacyType = privacyType, - Start = currentCoverageEnd, - End = 100 - }); - } - - return gaps; - } - private class OverlapResult { public string ConflictingRuleName { get; set; } = string.Empty; diff --git a/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/QueueCleaner/SlowConfigTests.cs b/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/QueueCleaner/SlowConfigTests.cs deleted file mode 100644 index 5f83776d..00000000 --- a/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/QueueCleaner/SlowConfigTests.cs +++ /dev/null @@ -1,275 +0,0 @@ -using Cleanuparr.Persistence.Models.Configuration.QueueCleaner; -using Shouldly; -using Xunit; -using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException; - -namespace Cleanuparr.Persistence.Tests.Models.Configuration.QueueCleaner; - -public sealed class SlowConfigTests -{ - #region Validate - Valid Configurations - - [Fact] - public void Validate_WithDisabledConfig_DoesNotThrow() - { - var config = new SlowConfig - { - MaxStrikes = 0 - }; - - Should.NotThrow(() => config.Validate()); - } - - [Fact] - public void Validate_WithValidMinSpeed_DoesNotThrow() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "100KB", - MaxTime = 0 - }; - - Should.NotThrow(() => config.Validate()); - } - - [Fact] - public void Validate_WithValidMaxTime_DoesNotThrow() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "", - MaxTime = 24 - }; - - Should.NotThrow(() => config.Validate()); - } - - [Fact] - public void Validate_WithBothMinSpeedAndMaxTime_DoesNotThrow() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "1MB", - MaxTime = 48 - }; - - Should.NotThrow(() => config.Validate()); - } - - #endregion - - #region Validate - MaxStrikes Validation - - [Theory] - [InlineData((ushort)1)] - [InlineData((ushort)2)] - public void Validate_WithMaxStrikesBetween1And2_ThrowsValidationException(ushort maxStrikes) - { - var config = new SlowConfig - { - MaxStrikes = maxStrikes, - MinSpeed = "100KB", - MaxTime = 0 - }; - - var exception = Should.Throw(() => config.Validate()); - exception.Message.ShouldBe("the minimum value for slow max strikes must be 3"); - } - - [Fact] - public void Validate_WithMinimumValidMaxStrikes_DoesNotThrow() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "100KB", - MaxTime = 0 - }; - - Should.NotThrow(() => config.Validate()); - } - - #endregion - - #region Validate - MinSpeed Validation - - [Theory] - [InlineData("invalid")] - [InlineData("abc")] - [InlineData("100")] - [InlineData("KB")] - public void Validate_WithInvalidMinSpeedFormat_ThrowsValidationException(string minSpeed) - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = minSpeed, - MaxTime = 0 - }; - - var exception = Should.Throw(() => config.Validate()); - exception.Message.ShouldBe("invalid value for slow min speed"); - } - - [Theory] - [InlineData("1KB")] - [InlineData("100KB")] - [InlineData("1MB")] - [InlineData("1GB")] - public void Validate_WithValidMinSpeedFormats_DoesNotThrow(string minSpeed) - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = minSpeed, - MaxTime = 0 - }; - - Should.NotThrow(() => config.Validate()); - } - - #endregion - - #region Validate - MaxTime Validation - - [Fact] - public void Validate_WithNegativeMaxTime_ThrowsValidationException() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "100KB", - MaxTime = -1 - }; - - var exception = Should.Throw(() => config.Validate()); - exception.Message.ShouldBe("invalid value for slow max time"); - } - - #endregion - - #region Validate - MinSpeed and MaxTime Required - - [Fact] - public void Validate_WithNoMinSpeedAndNoMaxTime_ThrowsValidationException() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "", - MaxTime = 0 - }; - - var exception = Should.Throw(() => config.Validate()); - exception.Message.ShouldBe("either slow min speed or slow max time must be set"); - } - - #endregion - - #region Validate - IgnoreAboveSize Validation - - [Theory] - [InlineData("100MB")] - [InlineData("1GB")] - [InlineData("10GB")] - public void Validate_WithValidIgnoreAboveSize_DoesNotThrow(string ignoreAboveSize) - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "100KB", - MaxTime = 0, - IgnoreAboveSize = ignoreAboveSize - }; - - Should.NotThrow(() => config.Validate()); - } - - [Fact] - public void Validate_WithEmptyIgnoreAboveSize_DoesNotThrow() - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "100KB", - MaxTime = 0, - IgnoreAboveSize = "" - }; - - Should.NotThrow(() => config.Validate()); - } - - [Theory] - [InlineData("invalid")] - [InlineData("abc")] - [InlineData("100")] - public void Validate_WithInvalidIgnoreAboveSizeFormat_ThrowsValidationException(string ignoreAboveSize) - { - var config = new SlowConfig - { - MaxStrikes = 3, - MinSpeed = "100KB", - MaxTime = 0, - IgnoreAboveSize = ignoreAboveSize - }; - - var exception = Should.Throw(() => config.Validate()); - exception.Message.ShouldContain("invalid value for slow ignore above size"); - } - - #endregion - - #region ByteSize Property Tests - - [Fact] - public void MinSpeedByteSize_WithValidSpeed_ParsesCorrectly() - { - var config = new SlowConfig - { - MinSpeed = "1MB" - }; - - config.MinSpeedByteSize.Bytes.ShouldBe(1024 * 1024); - } - - [Fact] - public void MinSpeedByteSize_WithEmptySpeed_ReturnsZero() - { - var config = new SlowConfig - { - MinSpeed = "" - }; - - config.MinSpeedByteSize.Bytes.ShouldBe(0); - } - - [Fact] - public void IgnoreAboveSizeByteSize_WithValidSize_ParsesCorrectly() - { - var config = new SlowConfig - { - MinSpeed = "100KB", - IgnoreAboveSize = "1GB" - }; - - config.IgnoreAboveSizeByteSize.ShouldNotBeNull(); - config.IgnoreAboveSizeByteSize!.Value.Bytes.ShouldBe(1024L * 1024 * 1024); - } - - [Fact] - public void IgnoreAboveSizeByteSize_WithEmptySize_ReturnsNull() - { - var config = new SlowConfig - { - MinSpeed = "100KB", - IgnoreAboveSize = "" - }; - - config.IgnoreAboveSizeByteSize.ShouldBeNull(); - } - - #endregion -} diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/IQueueRule.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/IQueueRule.cs deleted file mode 100644 index 2e85f3ff..00000000 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/IQueueRule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Cleanuparr.Domain.Entities; -using Cleanuparr.Domain.Enums; - -namespace Cleanuparr.Persistence.Models.Configuration.QueueCleaner; - -public interface IQueueRule -{ - Guid Id { get; } - - string Name { get; } - - bool Enabled { get; } - - int MaxStrikes { get; } - - TorrentPrivacyType PrivacyType { get; } - - ushort MinCompletionPercentage { get; } - - ushort MaxCompletionPercentage { get; } - - bool MatchesTorrent(ITorrentItemWrapper torrent); - - void Validate(); -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs index 84beb4d7..80fd1885 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueRule.cs @@ -5,7 +5,7 @@ using Cleanuparr.Domain.Enums; namespace Cleanuparr.Persistence.Models.Configuration.QueueCleaner; -public abstract record QueueRule : IConfig, IQueueRule +public abstract record QueueRule : IConfig { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/SlowConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/SlowConfig.cs deleted file mode 100644 index 7a15a0ec..00000000 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/SlowConfig.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; -using Cleanuparr.Domain.Entities; -using Cleanuparr.Domain.Exceptions; - -namespace Cleanuparr.Persistence.Models.Configuration.QueueCleaner; - -[ComplexType] -public sealed record SlowConfig -{ - public ushort MaxStrikes { get; init; } - - public bool ResetStrikesOnProgress { get; init; } - - public bool IgnorePrivate { get; init; } - - public bool DeletePrivate { get; init; } - - public string MinSpeed { get; init; } = string.Empty; - - [JsonIgnore] - public ByteSize MinSpeedByteSize => string.IsNullOrEmpty(MinSpeed) ? new ByteSize(0) : ByteSize.Parse(MinSpeed); - - public double MaxTime { get; init; } - - public string IgnoreAboveSize { get; init; } = string.Empty; - - [JsonIgnore] - public ByteSize? IgnoreAboveSizeByteSize => string.IsNullOrEmpty(IgnoreAboveSize) ? null : ByteSize.Parse(IgnoreAboveSize); - - public void Validate() - { - if (MaxStrikes is > 0 and < 3) - { - throw new ValidationException("the minimum value for slow max strikes must be 3"); - } - - if (MaxStrikes > 0) - { - bool isSpeedSet = !string.IsNullOrEmpty(MinSpeed); - - if (isSpeedSet && ByteSize.TryParse(MinSpeed, out _) is false) - { - throw new ValidationException("invalid value for slow min speed"); - } - - if (MaxTime < 0) - { - throw new ValidationException("invalid value for slow max time"); - } - - if (!isSpeedSet && MaxTime is 0) - { - throw new ValidationException("either slow min speed or slow max time must be set"); - } - - bool isIgnoreAboveSizeSet = !string.IsNullOrEmpty(IgnoreAboveSize); - - if (isIgnoreAboveSizeSet && ByteSize.TryParse(IgnoreAboveSize, out _) is false) - { - throw new ValidationException($"invalid value for slow ignore above size: {IgnoreAboveSize}"); - } - } - } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs b/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs index 7692ab3a..05f081b0 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Events/AppEvent.cs @@ -17,7 +17,7 @@ namespace Cleanuparr.Persistence.Models.Events; [Index(nameof(ArrInstanceId))] [Index(nameof(CycleId))] [Index(nameof(DeleteReason))] -public class AppEvent : IEvent +public class AppEvent { [Key] public Guid Id { get; set; } = Guid.CreateVersion7(); diff --git a/code/backend/Cleanuparr.Persistence/Models/Events/IEvent.cs b/code/backend/Cleanuparr.Persistence/Models/Events/IEvent.cs deleted file mode 100644 index 7b0122c9..00000000 --- a/code/backend/Cleanuparr.Persistence/Models/Events/IEvent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Cleanuparr.Persistence.Models.Events; - -public interface IEvent -{ - Guid Id { get; set; } - - DateTimeOffset Timestamp { get; set; } -} \ No newline at end of file diff --git a/code/frontend/public/icons/ext/lidarr.svg b/code/frontend/public/icons/ext/lidarr.svg deleted file mode 100644 index f25f4086..00000000 --- a/code/frontend/public/icons/ext/lidarr.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/frontend/public/icons/ext/radarr.svg b/code/frontend/public/icons/ext/radarr.svg deleted file mode 100644 index a84ce5d3..00000000 --- a/code/frontend/public/icons/ext/radarr.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/frontend/public/icons/ext/readarr.svg b/code/frontend/public/icons/ext/readarr.svg deleted file mode 100644 index 958ef162..00000000 --- a/code/frontend/public/icons/ext/readarr.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/frontend/public/icons/ext/sonarr.svg b/code/frontend/public/icons/ext/sonarr.svg deleted file mode 100644 index 54108f4a..00000000 --- a/code/frontend/public/icons/ext/sonarr.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/frontend/public/icons/ext/whisparr.svg b/code/frontend/public/icons/ext/whisparr.svg deleted file mode 100644 index c55fd4e1..00000000 --- a/code/frontend/public/icons/ext/whisparr.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/code/frontend/src/app/app.icons.ts b/code/frontend/src/app/app.icons.ts index 33befec6..0fd0875c 100644 --- a/code/frontend/src/app/app.icons.ts +++ b/code/frontend/src/app/app.icons.ts @@ -19,13 +19,14 @@ import { tablerBrandGithub, tablerBrandDiscord, tablerHeart, - tablerStar, - tablerExternalLink, tablerQuestionMark, tablerPencil, tablerPlus, tablerAlertTriangle, - tablerX, + tablerAlertCircle, + tablerTrendingUp, + tablerPlugConnectedX, + tablerListDetails, tablerEye, tablerEyeOff, tablerArrowDown, @@ -48,7 +49,6 @@ import { tablerGripVertical, tablerFilter, tablerPalette, - tablerFileOff, } from '@ng-icons/tabler-icons'; export const APP_ICONS = { @@ -72,13 +72,14 @@ export const APP_ICONS = { tablerBrandGithub, tablerBrandDiscord, tablerHeart, - tablerStar, - tablerExternalLink, tablerQuestionMark, tablerPencil, tablerPlus, tablerAlertTriangle, - tablerX, + tablerAlertCircle, + tablerTrendingUp, + tablerPlugConnectedX, + tablerListDetails, tablerEye, tablerEyeOff, tablerArrowDown, @@ -101,5 +102,4 @@ export const APP_ICONS = { tablerGripVertical, tablerFilter, tablerPalette, - tablerFileOff, }; diff --git a/code/frontend/src/app/core/api/events.api.ts b/code/frontend/src/app/core/api/events.api.ts index a9a3c643..07d628e3 100644 --- a/code/frontend/src/app/core/api/events.api.ts +++ b/code/frontend/src/app/core/api/events.api.ts @@ -1,7 +1,7 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { AppEvent, ManualEvent, EventStats, ManualEventStats, EventFilter, ManualEventFilter, EventTypeTimelineResponse } from '@core/models/event.models'; +import { AppEvent, ManualEvent, EventFilter, ManualEventFilter, EventTypeTimelineResponse } from '@core/models/event.models'; import { PaginatedResult } from '@core/models/pagination.model'; @Injectable({ providedIn: 'root' }) @@ -23,18 +23,6 @@ export class EventsApi { return this.http.get>('/api/events', { params }); } - getEvent(id: string): Observable { - return this.http.get(`/api/events/${id}`); - } - - getEventsByTracking(trackingId: string): Observable { - return this.http.get(`/api/events/tracking/${trackingId}`); - } - - getEventStats(): Observable { - return this.http.get('/api/events/stats'); - } - getEventTypes(): Observable { return this.http.get('/api/events/types'); } @@ -64,10 +52,6 @@ export class EventsApi { return this.http.get>('/api/manualevents', { params }); } - getManualEvent(id: string): Observable { - return this.http.get(`/api/manualevents/${id}`); - } - resolveManualEvent(id: string): Observable { return this.http.post(`/api/manualevents/${id}/resolve`, {}); } @@ -75,12 +59,4 @@ export class EventsApi { resolveAllManualEvents(): Observable<{ resolvedCount: number }> { return this.http.post<{ resolvedCount: number }>('/api/manualevents/resolve_all', {}); } - - getManualEventStats(): Observable { - return this.http.get('/api/manualevents/stats'); - } - - getManualEventSeverities(): Observable { - return this.http.get('/api/manualevents/severities'); - } } diff --git a/code/frontend/src/app/core/api/jobs.api.ts b/code/frontend/src/app/core/api/jobs.api.ts index 9dda48c0..1f21dbcd 100644 --- a/code/frontend/src/app/core/api/jobs.api.ts +++ b/code/frontend/src/app/core/api/jobs.api.ts @@ -1,30 +1,13 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { JobInfo, JobScheduleRequest } from '@core/models/job.models'; import { JobType } from '@shared/models/enums'; @Injectable({ providedIn: 'root' }) export class JobsApi { private http = inject(HttpClient); - getAll(): Observable { - return this.http.get('/api/jobs'); - } - - get(jobType: JobType): Observable { - return this.http.get(`/api/jobs/${jobType}`); - } - - start(jobType: JobType): Observable<{ message: string }> { - return this.http.post<{ message: string }>(`/api/jobs/${jobType}/start`, {}); - } - trigger(jobType: JobType): Observable<{ message: string }> { return this.http.post<{ message: string }>(`/api/jobs/${jobType}/trigger`, {}); } - - updateSchedule(jobType: JobType, schedule: JobScheduleRequest): Observable<{ message: string }> { - return this.http.put<{ message: string }>(`/api/jobs/${jobType}/schedule`, schedule); - } } diff --git a/code/frontend/src/app/core/api/strikes.api.ts b/code/frontend/src/app/core/api/strikes.api.ts index 1ef100e1..6a091354 100644 --- a/code/frontend/src/app/core/api/strikes.api.ts +++ b/code/frontend/src/app/core/api/strikes.api.ts @@ -1,7 +1,7 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { DownloadItemStrikes, RecentStrike, StrikeFilter } from '@core/models/strike.models'; +import { DownloadItemStrikes, StrikeFilter } from '@core/models/strike.models'; import { PaginatedResult } from '@core/models/pagination.model'; @Injectable({ providedIn: 'root' }) @@ -19,11 +19,6 @@ export class StrikesApi { return this.http.get>('/api/strikes', { params }); } - getRecentStrikes(count = 5): Observable { - const params = new HttpParams().set('count', count); - return this.http.get('/api/strikes/recent', { params }); - } - getStrikeTypes(): Observable { return this.http.get('/api/strikes/types'); } diff --git a/code/frontend/src/app/core/feature-badges/feature-registry.ts b/code/frontend/src/app/core/feature-badges/feature-registry.ts index 6bd17e16..b4697539 100644 --- a/code/frontend/src/app/core/feature-badges/feature-registry.ts +++ b/code/frontend/src/app/core/feature-badges/feature-registry.ts @@ -6,7 +6,7 @@ * - Box sections (`app-card`, `app-accordion`) render a flame "NEW" corner ribbon in the * top-right corner. * - Field components (`app-toggle`, `app-input`, `app-select`, `app-number-input`, - * `app-chip-input`, `app-textarea`, `app-size-input`) render a small flame "NEW" pill next + * `app-chip-input`, `app-size-input`) render a small flame "NEW" pill next * to their label. * You can also drop `` (ribbon) or * `` (pill) anywhere manually. diff --git a/code/frontend/src/app/core/models/event.models.ts b/code/frontend/src/app/core/models/event.models.ts index f3e4f5ca..c1d9577e 100644 --- a/code/frontend/src/app/core/models/event.models.ts +++ b/code/frontend/src/app/core/models/event.models.ts @@ -51,21 +51,6 @@ export interface ManualEvent { strikeCount?: number; } -export interface EventStats { - totalEvents: number; - eventsBySeverity: { severity: string; count: number }[]; - eventsByType: { eventType: string; count: number }[]; - recentEventsCount: number; -} - -export interface ManualEventStats { - totalEvents: number; - unresolvedEvents: number; - resolvedEvents: number; - eventsBySeverity: { severity: string; count: number }[]; - unresolvedBySeverity: { severity: string; count: number }[]; -} - export interface EventTypeTimelineBucket { date: string; counts: Record; diff --git a/code/frontend/src/app/core/models/job.models.ts b/code/frontend/src/app/core/models/job.models.ts index a4ae972f..e6b2256e 100644 --- a/code/frontend/src/app/core/models/job.models.ts +++ b/code/frontend/src/app/core/models/job.models.ts @@ -10,10 +10,3 @@ export interface JobInfo { previousRunTime?: Date; jobType: string; } - -export interface JobScheduleRequest { - schedule: { - every: number; - type: string; - }; -} diff --git a/code/frontend/src/app/core/services/base-path.service.ts b/code/frontend/src/app/core/services/base-path.service.ts index ed09778f..f5e821ad 100644 --- a/code/frontend/src/app/core/services/base-path.service.ts +++ b/code/frontend/src/app/core/services/base-path.service.ts @@ -17,23 +17,6 @@ export class ApplicationPathService { return 'https://cleanuparr.github.io'; } - buildUrl(path: string): string { - const basePath = this.getBasePath(); - const cleanPath = path.startsWith('/') ? path : '/' + path; - return basePath === '/' ? cleanPath : basePath + cleanPath; - } - - buildApiUrl(apiPath: string): string { - const basePath = this.getBasePath(); - const cleanApiPath = apiPath.startsWith('/') ? apiPath : '/' + apiPath; - - if (isDevMode()) { - return basePath + '/api' + cleanApiPath; - } - - return basePath === '/' ? '/api' + cleanApiPath : basePath + '/api' + cleanApiPath; - } - buildHubUrl(hubPath: string): string { const basePath = this.getBasePath(); const cleanPath = hubPath.startsWith('/') ? hubPath : '/' + hubPath; diff --git a/code/frontend/src/app/core/services/documentation.service.ts b/code/frontend/src/app/core/services/documentation.service.ts index 64816af5..eec2b077 100644 --- a/code/frontend/src/app/core/services/documentation.service.ts +++ b/code/frontend/src/app/core/services/documentation.service.ts @@ -137,9 +137,6 @@ export class DocumentationService { 'blacklistPath': 'blacklist-path', }, 'notifications': { - 'enabled': 'enabled', - 'name': 'provider-name', - 'eventTriggers': 'event-configuration', 'onFailedImportStrike': 'event-configuration', 'onStalledStrike': 'event-configuration', 'onSlowStrike': 'event-configuration', @@ -243,13 +240,4 @@ export class DocumentationService { const url = this.pathService.buildDocumentationUrl(section, anchor); window.open(url, '_blank', 'noopener,noreferrer'); } - - getFieldDocumentationUrl(section: string, fieldName: string): string { - const anchor = this.fieldMappings[section]?.[fieldName]; - return this.pathService.buildDocumentationUrl(section, anchor); - } - - hasFieldDocumentation(section: string, fieldName: string): boolean { - return !!this.fieldMappings[section]?.[fieldName]; - } } diff --git a/code/frontend/src/app/core/services/theme.service.ts b/code/frontend/src/app/core/services/theme.service.ts index bddb924f..f23c3e07 100644 --- a/code/frontend/src/app/core/services/theme.service.ts +++ b/code/frontend/src/app/core/services/theme.service.ts @@ -88,22 +88,12 @@ export class ThemeService { localStorage.setItem(PERFORMANCE_MODE_KEY, String(next)); } - setPerformanceMode(value: boolean): void { - this._performanceMode.set(value); - localStorage.setItem(PERFORMANCE_MODE_KEY, String(value)); - } - toggleFullWidth(): void { const next = !this._fullWidth(); this._fullWidth.set(next); localStorage.setItem(FULL_WIDTH_KEY, String(next)); } - setFullWidth(value: boolean): void { - this._fullWidth.set(value); - localStorage.setItem(FULL_WIDTH_KEY, String(value)); - } - setAccent(accent: Accent): void { this._accent.set(accent); localStorage.setItem(ACCENT_KEY, accent); diff --git a/code/frontend/src/app/features/auth/setup/setup.component.ts b/code/frontend/src/app/features/auth/setup/setup.component.ts index 137d2c65..32c3ce42 100644 --- a/code/frontend/src/app/features/auth/setup/setup.component.ts +++ b/code/frontend/src/app/features/auth/setup/setup.component.ts @@ -48,7 +48,6 @@ export class SetupComponent { // Step 3 - Plex plexLinking = signal(false); plexLinked = signal(false); - plexUsername = signal(''); plexPinId = signal(0); // Auto-focus refs diff --git a/code/frontend/src/app/features/dashboard/dashboard.component.scss b/code/frontend/src/app/features/dashboard/dashboard.component.scss index ad57f4b8..513f04aa 100644 --- a/code/frontend/src/app/features/dashboard/dashboard.component.scss +++ b/code/frontend/src/app/features/dashboard/dashboard.component.scss @@ -582,20 +582,6 @@ app-stats-card { flex-wrap: wrap; } - &__category { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - } - - &__instance-name { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - &__time { font-size: var(--font-size-xs); color: var(--text-tertiary); diff --git a/code/frontend/src/app/features/dashboard/dashboard.component.ts b/code/frontend/src/app/features/dashboard/dashboard.component.ts index 257feb8a..a0a54c56 100644 --- a/code/frontend/src/app/features/dashboard/dashboard.component.ts +++ b/code/frontend/src/app/features/dashboard/dashboard.component.ts @@ -309,10 +309,6 @@ export class DashboardComponent { return details; } - navigateTo(path: string): void { - this.router.navigate([path]); - } - private loadOrder(): DashboardRowId[] { try { const saved = localStorage.getItem(DASHBOARD_ROW_ORDER_KEY); diff --git a/code/frontend/src/app/features/events/events.component.ts b/code/frontend/src/app/features/events/events.component.ts index 89528f6c..35f5fda0 100644 --- a/code/frontend/src/app/features/events/events.component.ts +++ b/code/frontend/src/app/features/events/events.component.ts @@ -189,10 +189,6 @@ export class EventsComponent implements OnInit, OnDestroy { this.currentPage.set(1); } - viewLogsForJobRun(runId: string): void { - this.router.navigate(['/logs'], { queryParams: { jobRunId: runId } }); - } - exportEvents(format: 'json' | 'csv' | 'text'): void { this.showExportMenu.set(false); const events = this.events(); diff --git a/code/frontend/src/app/features/logs-component/logs.component.scss b/code/frontend/src/app/features/logs-component/logs.component.scss index d78a1782..7c24e69a 100644 --- a/code/frontend/src/app/features/logs-component/logs.component.scss +++ b/code/frontend/src/app/features/logs-component/logs.component.scss @@ -82,15 +82,6 @@ font-size: var(--font-size-xs); } - &__category { - color: var(--color-primary); - flex-shrink: 0; - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - &__message { color: var(--text-secondary); padding: 0 var(--space-4) var(--space-2); @@ -169,16 +160,6 @@ } } - &__instance-name { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-shrink: 0; - } - &__run-id { background: none; border: none; @@ -276,8 +257,4 @@ padding: var(--space-2) var(--space-3); } - .log-entry__category { - max-width: 100px; - } - } diff --git a/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss b/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss index 551dbd24..a64f6fc7 100644 --- a/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss +++ b/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.scss @@ -282,69 +282,4 @@ } // Table cell content -.cell-title { - display: flex; - flex-direction: column; - gap: var(--space-1); - - &__text { - font-weight: 500; - color: var(--text-primary); - overflow-wrap: anywhere; - word-break: normal; - } - - &__chips { - display: flex; - gap: var(--space-1); - flex-wrap: wrap; - } -} - -.score-value { - font-family: var(--font-mono); - font-weight: 600; - - &--below { - color: var(--color-warning); - } -} - // Expanded detail -.score-detail { - display: flex; - flex-direction: column; - gap: var(--space-4); - - &__grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: var(--space-3); - } - - &__item { - display: flex; - flex-direction: column; - gap: var(--space-1); - } - - &__label { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - text-transform: uppercase; - letter-spacing: 0.04em; - } - - &__value { - font-size: var(--font-size-sm); - color: var(--text-primary); - overflow-wrap: anywhere; - word-break: normal; - } - - &__history { - display: flex; - flex-direction: column; - gap: var(--space-2); - } -} diff --git a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss index 29c5730e..899d696a 100644 --- a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss +++ b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.scss @@ -253,48 +253,6 @@ // Expanded row detail -.event-detail { - display: flex; - flex-direction: column; - gap: var(--space-3); - - &__chips { - display: flex; - gap: var(--space-2); - flex-wrap: wrap; - align-items: center; - } - - &__section { - display: flex; - flex-direction: column; - gap: var(--space-1); - } - - &__label { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - text-transform: uppercase; - letter-spacing: 0.04em; - } - - &__value { - font-size: var(--font-size-sm); - color: var(--text-primary); - overflow-wrap: anywhere; - word-break: normal; - } - - &__cycle { - font-family: var(--font-mono); - font-size: var(--font-size-xs); - color: var(--text-tertiary); - background: var(--glass-bg); - padding: 2px 8px; - border-radius: var(--radius-sm); - } -} - // List rows (legacy styles — kept for potential reuse) .list-row { @@ -344,13 +302,6 @@ word-break: break-word; } - &__extra { - font-family: var(--font-mono); - font-size: var(--font-size-xs); - color: var(--text-tertiary); - margin-left: var(--space-1); - } - &__cycle { font-family: var(--font-mono); font-size: var(--font-size-xs); diff --git a/code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss b/code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss index 6cef9f85..73e502d3 100644 --- a/code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss +++ b/code/frontend/src/app/features/seeker-stats/upgrades-tab/upgrades-tab.component.scss @@ -160,55 +160,4 @@ } // Table cell content -.cell-title { - display: flex; - align-items: center; - gap: var(--space-2); - flex-wrap: wrap; - - &__text { - font-weight: 500; - color: var(--text-primary); - overflow-wrap: anywhere; - word-break: normal; - } -} - -.score-value { - font-family: var(--font-mono); - font-weight: 600; - - &--new { color: var(--color-success); } - &--old { color: var(--text-tertiary); } -} - -.score-delta { - font-family: var(--font-mono); - font-weight: 600; - color: var(--color-success); -} - // Expanded detail -.upgrade-detail { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: var(--space-3); - - &__item { - display: flex; - flex-direction: column; - gap: var(--space-1); - } - - &__label { - font-size: var(--font-size-xs); - color: var(--text-tertiary); - text-transform: uppercase; - letter-spacing: 0.04em; - } - - &__value { - font-size: var(--font-size-sm); - color: var(--text-primary); - } -} diff --git a/code/frontend/src/app/layout/nav-sidebar/nav-sidebar.component.scss b/code/frontend/src/app/layout/nav-sidebar/nav-sidebar.component.scss index c5730ee6..04ce5ff8 100644 --- a/code/frontend/src/app/layout/nav-sidebar/nav-sidebar.component.scss +++ b/code/frontend/src/app/layout/nav-sidebar/nav-sidebar.component.scss @@ -180,15 +180,6 @@ font-size: 14px; } - &__section-label-text { - padding: var(--space-2) var(--space-3); - color: var(--sidebar-section-label); - font-size: var(--font-size-xs); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - } - &__logout { display: flex; align-items: center; diff --git a/code/frontend/src/app/shared/models/download-cleaner-config.model.ts b/code/frontend/src/app/shared/models/download-cleaner-config.model.ts index cb586f5e..67690e35 100644 --- a/code/frontend/src/app/shared/models/download-cleaner-config.model.ts +++ b/code/frontend/src/app/shared/models/download-cleaner-config.model.ts @@ -62,25 +62,6 @@ export interface DownloadCleanerConfig { clients: ClientCleanerConfig[]; } -export function createDefaultSeedingRule(): SeedingRule { - return { - name: '', - categories: [], - trackerPatterns: [], - tagsAny: [], - tagsAll: [], - priority: 0, - privacyType: TorrentPrivacyType.Public, - maxRatio: -1, - minSeedTime: 0, - maxSeedTime: -1, - minSeeders: 0, - maxInactiveDays: -1, - action: SeedingRuleAction.Delete, - deleteSourceFiles: true, - }; -} - export function createDefaultUnlinkedConfig(): UnlinkedConfigModel { return { enabled: false, diff --git a/code/frontend/src/app/shared/models/enums.ts b/code/frontend/src/app/shared/models/enums.ts index 6d8d85df..d258ea5c 100644 --- a/code/frontend/src/app/shared/models/enums.ts +++ b/code/frontend/src/app/shared/models/enums.ts @@ -103,11 +103,6 @@ export enum SelectionStrategy { Random = 'Random', } -export enum SeriesSearchType { - Season = 'Season', - Series = 'Series', -} - export enum SearchCommandStatus { Pending = 'Pending', Started = 'Started', @@ -129,12 +124,6 @@ export enum DeleteReason { AtLeastOneFileBlocked = 'AtLeastOneFileBlocked', } -export enum CleanReason { - None = 'None', - MaxRatioReached = 'MaxRatioReached', - MaxSeedTimeReached = 'MaxSeedTimeReached', -} - export enum InstanceType { Sonarr = 'Sonarr', Radarr = 'Radarr', diff --git a/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts b/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts index 8b0bbcca..8304bfcf 100644 --- a/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts +++ b/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts @@ -1,7 +1,6 @@ import { PatternMode } from './enums'; // Re-export for backward compatibility -export type { JobSchedule } from '@shared/utils/schedule.util'; export { ScheduleOptions } from '@shared/utils/schedule.util'; export interface FailedImportConfig { diff --git a/code/frontend/src/app/ui/chip-input/chip-input.component.scss b/code/frontend/src/app/ui/chip-input/chip-input.component.scss index db371308..7b434973 100644 --- a/code/frontend/src/app/ui/chip-input/chip-input.component.scss +++ b/code/frontend/src/app/ui/chip-input/chip-input.component.scss @@ -26,11 +26,6 @@ border-color: var(--input-border-focus); box-shadow: 0 0 0 3px var(--color-primary-subtle); } - - :host:focus-within .textarea-label { - color: var(--color-primary); - transition: color var(--duration-fast) var(--ease-default); - } &--disabled { opacity: 0.5; diff --git a/code/frontend/src/app/ui/icon/icon.component.html b/code/frontend/src/app/ui/icon/icon.component.html deleted file mode 100644 index 07a4bdc7..00000000 --- a/code/frontend/src/app/ui/icon/icon.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/code/frontend/src/app/ui/icon/icon.component.scss b/code/frontend/src/app/ui/icon/icon.component.scss deleted file mode 100644 index bc448a52..00000000 --- a/code/frontend/src/app/ui/icon/icon.component.scss +++ /dev/null @@ -1,20 +0,0 @@ -.icon { - display: inline-flex; - color: currentColor; - - &--sm { - font-size: 16px; - } - - &--md { - font-size: 20px; - } - - &--lg { - font-size: 24px; - } - - &--xl { - font-size: 32px; - } -} diff --git a/code/frontend/src/app/ui/icon/icon.component.ts b/code/frontend/src/app/ui/icon/icon.component.ts deleted file mode 100644 index 49ad847c..00000000 --- a/code/frontend/src/app/ui/icon/icon.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, ChangeDetectionStrategy, input } from '@angular/core'; -import { NgIcon } from '@ng-icons/core'; - -@Component({ - selector: 'app-icon', - standalone: true, - imports: [NgIcon], - templateUrl: './icon.component.html', - styleUrl: './icon.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class IconComponent { - name = input.required(); - size = input<'sm' | 'md' | 'lg' | 'xl'>('md'); -} diff --git a/code/frontend/src/app/ui/index.ts b/code/frontend/src/app/ui/index.ts index 281617e9..ca7605b3 100644 --- a/code/frontend/src/app/ui/index.ts +++ b/code/frontend/src/app/ui/index.ts @@ -6,14 +6,12 @@ export { InputComponent } from './input/input.component'; export { LabelComponent } from './label/label.component'; export { SpinnerComponent } from './spinner/spinner.component'; export { ToggleComponent } from './toggle/toggle.component'; -export { IconComponent } from './icon/icon.component'; export { BadgeComponent } from './badge/badge.component'; export type { BadgeSeverity, BadgeSize } from './badge/badge.component'; export { NewBadgeComponent } from './new-badge/new-badge.component'; export { SelectComponent } from './select/select.component'; export type { SelectOption } from './select/select.component'; export { NumberInputComponent } from './number-input/number-input.component'; -export { TextareaComponent } from './textarea/textarea.component'; export { ChipInputComponent } from './chip-input/chip-input.component'; export { AccordionComponent } from './accordion/accordion.component'; export { ModalComponent } from './modal/modal.component'; diff --git a/code/frontend/src/app/ui/textarea/textarea.component.html b/code/frontend/src/app/ui/textarea/textarea.component.html deleted file mode 100644 index ef623476..00000000 --- a/code/frontend/src/app/ui/textarea/textarea.component.html +++ /dev/null @@ -1,29 +0,0 @@ -@if (label()) { - -} - -@if (error()) { - {{ error() }} -} @else if (hint()) { - {{ hint() }} -} diff --git a/code/frontend/src/app/ui/textarea/textarea.component.scss b/code/frontend/src/app/ui/textarea/textarea.component.scss deleted file mode 100644 index 2d3b54a2..00000000 --- a/code/frontend/src/app/ui/textarea/textarea.component.scss +++ /dev/null @@ -1,45 +0,0 @@ -@use 'glass' as *; - -:host { - display: flex; - flex-direction: column; - gap: var(--space-1-5); -} - -.textarea-label { - font-size: var(--font-size-sm); - font-weight: 500; - color: var(--text-secondary); -} - -.textarea-field { - @include glass-input; - padding: var(--space-2) var(--space-3); - width: 100%; - resize: vertical; - min-height: 80px; - line-height: 1.5; - - &--error { - border-color: var(--color-error) !important; - - &:focus { - box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15); - } - } -} - -.textarea-error { - font-size: var(--font-size-xs); - color: var(--color-error); -} - -.textarea-hint { - font-size: var(--font-size-xs); - color: var(--text-tertiary); -} - -:host:focus-within .textarea-label { - color: var(--color-primary); - transition: color var(--duration-fast) var(--ease-default); -} diff --git a/code/frontend/src/app/ui/textarea/textarea.component.ts b/code/frontend/src/app/ui/textarea/textarea.component.ts deleted file mode 100644 index a858349a..00000000 --- a/code/frontend/src/app/ui/textarea/textarea.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, ChangeDetectionStrategy, input, model, output, inject } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { NgIcon } from '@ng-icons/core'; -import { DocumentationService } from '@core/services/documentation.service'; -import { NewBadgeComponent } from '@ui/new-badge/new-badge.component'; -import { generateControlId } from '@ui/control-id'; - -@Component({ - selector: 'app-textarea', - standalone: true, - imports: [FormsModule, NgIcon, NewBadgeComponent], - templateUrl: './textarea.component.html', - styleUrl: './textarea.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TextareaComponent { - private readonly docs = inject(DocumentationService); - - protected readonly controlId = generateControlId('app-textarea'); - - label = input(); - featureId = input(); - placeholder = input(''); - disabled = input(false); - readonly = input(false); - rows = input(4); - error = input(); - hint = input(); - helpKey = input(); - value = model(''); - - blurred = output(); - - onHelpClick(event: Event): void { - event.preventDefault(); - event.stopPropagation(); - const key = this.helpKey(); - if (key) { - const [section, field] = key.split(':'); - this.docs.openFieldDocumentation(section, field); - } - } -} diff --git a/code/frontend/src/styles.scss b/code/frontend/src/styles.scss index f67fbf61..eb83a4a8 100644 --- a/code/frontend/src/styles.scss +++ b/code/frontend/src/styles.scss @@ -13,48 +13,6 @@ max-width: none; } -// Skeleton loading utility - add class="skeleton" to any element -.skeleton { - background: linear-gradient( - 90deg, - var(--glass-bg) 0px, - rgba(var(--accent-rgb), 0.08) 30px, - rgba(var(--accent-rgb), 0.06) 50px, - var(--glass-bg-hover) 70px, - var(--glass-bg) 100px - ); - background-size: 250px 100%; - animation: shimmer 1.5s ease-in-out infinite; - border-radius: var(--radius-md); - color: transparent !important; - pointer-events: none; - user-select: none; - - * { visibility: hidden; } - - &--text { - height: 14px; - width: 60%; - } - - &--title { - height: 20px; - width: 40%; - } - - &--input { - height: 38px; - width: 100%; - max-width: 400px; - } - - &--badge { - height: 24px; - width: 80px; - border-radius: var(--radius-full); - } -} - // Shared field help button (used across all form components) .field-help-btn { display: inline-flex; diff --git a/code/frontend/src/styles/_animations.scss b/code/frontend/src/styles/_animations.scss index 628fa81a..236300fb 100644 --- a/code/frontend/src/styles/_animations.scss +++ b/code/frontend/src/styles/_animations.scss @@ -9,21 +9,11 @@ to { opacity: 1; } } -@keyframes fade-out { - from { opacity: 1; } - to { opacity: 0; } -} - @keyframes slide-up { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } -@keyframes slide-down { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } -} - @keyframes slide-in-right { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } @@ -43,11 +33,6 @@ to { transform: rotate(360deg); } } -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - // Page enter: used for route transitions (opacity-only to avoid containing blocks that break sticky) @keyframes page-enter { from { opacity: 0; } @@ -83,12 +68,6 @@ to { opacity: 1; } } -// Shimmer for skeleton loading placeholders -@keyframes shimmer { - 0% { background-position: -200px 0; } - 100% { background-position: calc(200px + 100%) 0; } -} - @keyframes orb-hue { 0% { filter: hue-rotate(0deg); } 50% { filter: hue-rotate(-35deg); } diff --git a/e2e/tests/core/events.api.spec.ts b/e2e/tests/core/events.api.spec.ts index 2dfebddb..617dffa1 100644 --- a/e2e/tests/core/events.api.spec.ts +++ b/e2e/tests/core/events.api.spec.ts @@ -42,11 +42,6 @@ test.describe('Core — events', () => { expect([404, 400]).toContain(res.status); }); - test('POST /cleanup returns deletion count', async ({ api }) => { - const res = await api.events.cleanup(0); - expect(res.status).toBe(200); - }); - test('GET /api/events requires auth', async ({ anonymousApi }) => { const res = await anonymousApi.events.list(); expect(res.status).toBe(401); diff --git a/e2e/tests/core/manual-events.api.spec.ts b/e2e/tests/core/manual-events.api.spec.ts index 60618dcb..dc29d00b 100644 --- a/e2e/tests/core/manual-events.api.spec.ts +++ b/e2e/tests/core/manual-events.api.spec.ts @@ -26,11 +26,6 @@ test.describe('Core — manual events', () => { expect([404, 400]).toContain(res.status); }); - test('POST /cleanup returns deletion count', async ({ api }) => { - const res = await api.manualEvents.cleanup(0); - expect(res.status).toBe(200); - }); - test('GET requires auth', async ({ anonymousApi }) => { const res = await anonymousApi.manualEvents.list(); expect(res.status).toBe(401); diff --git a/e2e/tests/helpers/api/account.ts b/e2e/tests/helpers/api/account.ts index 04a43e43..fff46cf3 100644 --- a/e2e/tests/helpers/api/account.ts +++ b/e2e/tests/helpers/api/account.ts @@ -39,18 +39,6 @@ export class AccountApi { return this.client.post('/api/account/api-key/regenerate'); } - linkPlex(): Promise { - return this.client.post('/api/account/plex/link'); - } - - verifyPlexLink(pinId: string): Promise { - return this.client.post('/api/account/plex/link/verify', { pinId }); - } - - unlinkPlex(): Promise { - return this.client.delete('/api/account/plex/link'); - } - getOidcConfig(): Promise { return this.client.get('/api/account/oidc'); } @@ -59,10 +47,6 @@ export class AccountApi { return this.client.put('/api/account/oidc', config); } - startOidcLink(): Promise { - return this.client.post('/api/account/oidc/link'); - } - unlinkOidc(): Promise { return this.client.delete('/api/account/oidc/link'); } diff --git a/e2e/tests/helpers/api/auth.ts b/e2e/tests/helpers/api/auth.ts index ceb6902c..f656cd96 100644 --- a/e2e/tests/helpers/api/auth.ts +++ b/e2e/tests/helpers/api/auth.ts @@ -23,14 +23,6 @@ export class AuthApi { return this.client.post('/api/auth/setup/account', { username, password }); } - setupGenerate2fa(): Promise { - return this.client.post('/api/auth/setup/2fa/generate'); - } - - setupVerify2fa(code: string): Promise { - return this.client.post('/api/auth/setup/2fa/verify', { code }); - } - setupComplete(): Promise { return this.client.post('/api/auth/setup/complete'); } @@ -51,22 +43,6 @@ export class AuthApi { return this.client.post('/api/auth/logout', { refreshToken }); } - oidcStart(): Promise { - return this.client.post('/api/auth/oidc/start'); - } - - oidcExchange(oneTimeCode: string): Promise { - return this.client.post('/api/auth/oidc/exchange', { code: oneTimeCode }); - } - - plexPin(): Promise { - return this.client.post('/api/auth/login/plex/pin'); - } - - plexVerify(pinId: string): Promise { - return this.client.post('/api/auth/login/plex/verify', { pinId }); - } - async loginAndCaptureTokens(username: string, password: string): Promise { const res = await this.login(username, password); if (!res.ok) { diff --git a/e2e/tests/helpers/api/events.ts b/e2e/tests/helpers/api/events.ts index 4e4519ba..1ad39760 100644 --- a/e2e/tests/helpers/api/events.ts +++ b/e2e/tests/helpers/api/events.ts @@ -35,10 +35,6 @@ export class EventsApi { return this.client.get(`/api/events/${id}`); } - byTracking(trackingId: string): Promise { - return this.client.get(`/api/events/tracking/${trackingId}`); - } - types(): Promise { return this.client.get('/api/events/types'); } @@ -46,10 +42,6 @@ export class EventsApi { severities(): Promise { return this.client.get('/api/events/severities'); } - - cleanup(retentionDays = 30): Promise { - return this.client.post(`/api/events/cleanup?retentionDays=${retentionDays}`); - } } export class ManualEventsApi { @@ -74,10 +66,6 @@ export class ManualEventsApi { severities(): Promise { return this.client.get('/api/ManualEvents/severities'); } - - cleanup(retentionDays = 30): Promise { - return this.client.post(`/api/ManualEvents/cleanup?retentionDays=${retentionDays}`); - } } export class StrikesApi { diff --git a/e2e/tests/helpers/api/status.ts b/e2e/tests/helpers/api/status.ts index 02e0662b..691ecf51 100644 --- a/e2e/tests/helpers/api/status.ts +++ b/e2e/tests/helpers/api/status.ts @@ -35,15 +35,7 @@ export class HealthApi { return this.client.get('/api/health'); } - downloadClient(id: string): Promise { - return this.client.get(`/api/health/${id}`); - } - triggerCheck(): Promise { return this.client.post('/api/health/check'); } - - triggerCheckOne(id: string): Promise { - return this.client.post(`/api/health/check/${id}`); - } } diff --git a/e2e/tests/helpers/app-api.ts b/e2e/tests/helpers/app-api.ts index 8529d60c..3d396aa8 100644 --- a/e2e/tests/helpers/app-api.ts +++ b/e2e/tests/helpers/app-api.ts @@ -29,28 +29,6 @@ export async function waitForApp(timeoutMs = 90_000): Promise { throw new Error(`App did not become ready within ${timeoutMs}ms`); } -export async function createAccountAndSetup(): Promise { - const createRes = await fetch(`${API}/api/auth/setup/account`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: TEST_CONFIG.adminUsername, - password: TEST_CONFIG.adminPassword, - }), - }); - // 409 = controller says account exists; 403 = middleware says setup already completed - if (!createRes.ok && createRes.status !== 409 && createRes.status !== 403) { - throw new Error(`Failed to create account: ${createRes.status}`); - } - - const completeRes = await fetch(`${API}/api/auth/setup/complete`, { - method: 'POST', - }); - if (!completeRes.ok && completeRes.status !== 409 && completeRes.status !== 403) { - throw new Error(`Failed to complete setup: ${completeRes.status}`); - } -} - export async function loginAndGetToken(): Promise { const res = await fetch(`${API}/api/auth/login`, { method: 'POST', @@ -104,26 +82,6 @@ export async function updateOidcConfig( // --- Seeker API helpers --- -export async function getSeekerConfig(accessToken: string): Promise { - return fetch(`${API}/api/configuration/seeker`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); -} - -export async function updateSeekerConfig( - accessToken: string, - config: Record, -): Promise { - return fetch(`${API}/api/configuration/seeker`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify(config), - }); -} - export async function getSearchStatsSummary(accessToken: string): Promise { return fetch(`${API}/api/seeker/search-stats/summary`, { headers: { Authorization: `Bearer ${accessToken}` }, @@ -136,22 +94,6 @@ export async function getSearchEvents(accessToken: string): Promise { }); } -export async function getCfScores( - accessToken: string, - params?: Record, -): Promise { - const query = params ? '?' + new URLSearchParams(params).toString() : ''; - return fetch(`${API}/api/seeker/cf-scores${query}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); -} - -export async function getCfScoreStats(accessToken: string): Promise { - return fetch(`${API}/api/seeker/cf-scores/stats`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); -} - // --- Arr Instance helpers --- export async function createArrInstance( @@ -202,12 +144,6 @@ export async function updateDownloadCleanerConfig( }); } -export async function getSeedingRules(accessToken: string, downloadClientId: string): Promise { - return fetch(`${API}/api/seeding-rules/${downloadClientId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); -} - export async function createSeedingRule( accessToken: string, downloadClientId: string, @@ -526,31 +462,3 @@ export async function getOidcConfig(accessToken: string): Promise { - const res = await fetch(`${API}/api/account/oidc`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify(config), - }); - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to PUT OIDC config: ${res.status} ${body}`); - } -} - -export async function clearOidcLink(accessToken: string): Promise { - const res = await fetch(`${API}/api/account/oidc/link`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${accessToken}` }, - }); - if (!res.ok && res.status !== 404) { - const body = await res.text(); - throw new Error(`Failed to clear OIDC link: ${res.status} ${body}`); - } -} diff --git a/e2e/tests/helpers/mocks/blocklist-stubs.ts b/e2e/tests/helpers/mocks/blocklist-stubs.ts index cd9db9b5..bbf63b80 100644 --- a/e2e/tests/helpers/mocks/blocklist-stubs.ts +++ b/e2e/tests/helpers/mocks/blocklist-stubs.ts @@ -1,4 +1,4 @@ -import type { Mapping, WireMockClient } from './wiremock-client'; +import type { Mapping } from './wiremock-client'; const DEFAULT_BLOCKLIST = [ '*.exe', @@ -18,6 +18,3 @@ export function blocklistResponseStub(content: string = DEFAULT_BLOCKLIST, urlPa }; } -export async function applyBlocklistDefaults(blocklist: WireMockClient): Promise { - await blocklist.stubMany([blocklistResponseStub()]); -} diff --git a/e2e/tests/helpers/mocks/download-client-stubs.ts b/e2e/tests/helpers/mocks/download-client-stubs.ts index 66736cba..221eabe4 100644 --- a/e2e/tests/helpers/mocks/download-client-stubs.ts +++ b/e2e/tests/helpers/mocks/download-client-stubs.ts @@ -1,4 +1,4 @@ -import type { Mapping, WireMockClient } from './wiremock-client'; +import type { Mapping } from './wiremock-client'; /** * Convenience stub bundles for qBittorrent / Transmission / Deluge / uTorrent / rTorrent. @@ -23,14 +23,6 @@ export function qbitLoginOkStub(sid = 'test-sid'): Mapping { }; } -export function qbitLoginFailStub(): Mapping { - return { - request: { method: 'POST', urlPath: '/api/v2/auth/login' }, - response: { status: 200, body: 'Fails.', headers: { 'Content-Type': 'text/plain' } }, - priority: 10, - }; -} - export function qbitTorrentsStub(torrents: Array> = []): Mapping { return { request: { method: 'GET', urlPath: '/api/v2/torrents/info' }, @@ -38,58 +30,6 @@ export function qbitTorrentsStub(torrents: Array> = []): }; } -/** - * Transmission's real wire protocol: first POST to /transmission/rpc without an - * X-Transmission-Session-Id header returns 409 + a fresh session id; the - * client must replay the request with that header. We model both phases with - * two stubs distinguished by request-header presence. - */ -export function transmissionSessionStub(sessionId = 'test-session'): Mapping[] { - return [ - // Phase 1: client has no session id yet → reject with 409 + session header. - { - request: { - method: 'POST', - urlPath: '/transmission/rpc', - headers: { 'X-Transmission-Session-Id': { absent: true } }, - }, - response: { - status: 409, - body: '409: ConflictConflict', - headers: { - 'X-Transmission-Session-Id': sessionId, - 'Content-Type': 'text/html; charset=ISO-8859-1', - }, - }, - priority: 1, - }, - // Phase 2: client retries with the session header → success. - { - request: { - method: 'POST', - urlPath: '/transmission/rpc', - headers: { 'X-Transmission-Session-Id': { equalTo: sessionId } }, - }, - response: { - status: 200, - jsonBody: { - result: 'success', - arguments: { - version: '4.0.6', - 'rpc-version': 17, - 'rpc-version-minimum': 1, - }, - }, - headers: { - 'Content-Type': 'application/json', - 'X-Transmission-Session-Id': sessionId, - }, - }, - priority: 5, - }, - ]; -} - export function delugeLoginStub(sessionCookie = 'test-deluge'): Mapping { return { request: { @@ -136,14 +76,6 @@ export function utorrentStubs(token = 'utorrent-token', guid = 'test-guid'): Map ]; } -/** - * @deprecated Prefer {@link utorrentStubs} which also registers the list stub. - * Kept for backwards-compatible imports. - */ -export function utorrentTokenStub(token = 'utorrent-token'): Mapping { - return utorrentStubs(token)[0]; -} - export function rtorrentXmlRpcStub(): Mapping { return { request: { method: 'POST', urlPath: '/RPC2' }, @@ -155,6 +87,3 @@ export function rtorrentXmlRpcStub(): Mapping { }; } -export async function applyQBitDefaults(dlc: WireMockClient): Promise { - await dlc.stubMany([qbitVersionStub(), qbitLoginOkStub(), qbitTorrentsStub()]); -} diff --git a/e2e/tests/helpers/mocks/notification-stubs.ts b/e2e/tests/helpers/mocks/notification-stubs.ts index 41ac82cc..ab386c99 100644 --- a/e2e/tests/helpers/mocks/notification-stubs.ts +++ b/e2e/tests/helpers/mocks/notification-stubs.ts @@ -1,4 +1,4 @@ -import type { Mapping, WireMockClient } from './wiremock-client'; +import type { Mapping } from './wiremock-client'; export function discordWebhookStub(): Mapping { return { @@ -49,14 +49,3 @@ export function appriseStub(): Mapping { }; } -export async function applyNotificationDefaults(notify: WireMockClient): Promise { - await notify.stubMany([ - discordWebhookStub(), - telegramSendMessageStub(), - ntfyTopicStub(), - gotifyMessageStub(), - pushoverMessageStub(), - notifiarrStub(), - appriseStub(), - ]); -} diff --git a/e2e/tests/helpers/oidc.ts b/e2e/tests/helpers/oidc.ts deleted file mode 100644 index b1fb4c93..00000000 --- a/e2e/tests/helpers/oidc.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { expect, Page } from '@playwright/test'; -import { TEST_CONFIG } from './test-config'; - -/** - * Drives the browser through the OIDC link flow: - * local login → settings/account → Link Account → Keycloak login → callback success. - * - * Leaves the page on /settings/account?oidc_link=success. - */ -export async function linkOidcViaBrowser(page: Page): Promise { - await page.goto(`${TEST_CONFIG.appUrl}/auth/login`); - - await page - .getByRole('textbox', { name: 'Username' }) - .fill(TEST_CONFIG.adminUsername); - await page - .getByRole('textbox', { name: 'Password' }) - .fill(TEST_CONFIG.adminPassword); - await page.getByRole('button', { name: 'Sign In', exact: true }).click(); - - await expect(page).toHaveURL(/\/dashboard/, { timeout: 10_000 }); - - await page.goto(`${TEST_CONFIG.appUrl}/settings/account`); - await page.getByText('OIDC / SSO').click(); - - const linkButton = page.getByRole('button', { name: /link account|re-link/i }); - await expect(linkButton).toBeVisible({ timeout: 5_000 }); - await linkButton.click(); - - await expect(page).toHaveURL(/localhost:8080/, { timeout: 10_000 }); - - await page.locator('#username').waitFor({ state: 'visible', timeout: 5_000 }); - await page.locator('#username').fill(TEST_CONFIG.oidcUsername); - await page.locator('#password').fill(TEST_CONFIG.oidcPassword); - await page.locator('#kc-login').click(); - - await expect(page).toHaveURL(/settings\/account\?oidc_link=success/, { - timeout: 15_000, - }); -} diff --git a/e2e/tests/helpers/torrent-fixtures.ts b/e2e/tests/helpers/torrent-fixtures.ts index c574b2a6..abc0c8e7 100644 --- a/e2e/tests/helpers/torrent-fixtures.ts +++ b/e2e/tests/helpers/torrent-fixtures.ts @@ -324,12 +324,3 @@ export function resetDirectory(path: string): void { } } -/** - * Write a random extra file directly under a directory. Useful to seed an - * unrelated file that the cleaner should classify as orphaned. - */ -export function writeRandomFile(dir: string, name: string, sizeBytes = 1024): string { - const path = join(dir, name); - writeFileShared(path, randomBytes(sizeBytes)); - return path; -}