mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-04-04 14:25:30 -04:00
Add app status setting to general settings (#433)
This commit is contained in:
@@ -24,6 +24,8 @@ public sealed record UpdateGeneralConfigRequest
|
||||
|
||||
public ushort SearchDelay { get; init; } = Constants.DefaultSearchDelaySeconds;
|
||||
|
||||
public bool StatusCheckEnabled { get; init; } = true;
|
||||
|
||||
public string EncryptionKey { get; init; } = Guid.NewGuid().ToString();
|
||||
|
||||
public List<string> IgnoredDownloads { get; init; } = [];
|
||||
@@ -39,6 +41,7 @@ public sealed record UpdateGeneralConfigRequest
|
||||
existingConfig.HttpCertificateValidation = HttpCertificateValidation;
|
||||
existingConfig.SearchEnabled = SearchEnabled;
|
||||
existingConfig.SearchDelay = SearchDelay;
|
||||
existingConfig.StatusCheckEnabled = StatusCheckEnabled;
|
||||
existingConfig.EncryptionKey = EncryptionKey;
|
||||
existingConfig.IgnoredDownloads = IgnoredDownloads;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Cleanuparr.Domain.Entities.AppStatus;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
@@ -20,6 +21,7 @@ public class AppStatusRefreshServiceTests : IDisposable
|
||||
private readonly AppStatusSnapshot _snapshot;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly Mock<HttpMessageHandler> _httpHandlerMock;
|
||||
private readonly Mock<IServiceScopeFactory> _scopeFactoryMock;
|
||||
private AppStatusRefreshService? _service;
|
||||
|
||||
public AppStatusRefreshServiceTests()
|
||||
@@ -30,6 +32,7 @@ public class AppStatusRefreshServiceTests : IDisposable
|
||||
_snapshot = new AppStatusSnapshot();
|
||||
_jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
_httpHandlerMock = new Mock<HttpMessageHandler>();
|
||||
_scopeFactoryMock = new Mock<IServiceScopeFactory>();
|
||||
|
||||
// Setup hub context
|
||||
var clientsMock = new Mock<IHubClients>();
|
||||
@@ -50,7 +53,8 @@ public class AppStatusRefreshServiceTests : IDisposable
|
||||
_hubContextMock.Object,
|
||||
_httpClientFactoryMock.Object,
|
||||
_snapshot,
|
||||
_jsonOptions);
|
||||
_jsonOptions,
|
||||
_scopeFactoryMock.Object);
|
||||
return _service;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ using System.Text.Json;
|
||||
using Cleanuparr.Domain.Entities.AppStatus;
|
||||
using Cleanuparr.Infrastructure.Hubs;
|
||||
using Cleanuparr.Infrastructure.Models;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services;
|
||||
|
||||
@@ -17,18 +20,20 @@ public sealed class AppStatusRefreshService : BackgroundService
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly AppStatusSnapshot _snapshot;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private AppStatus? _lastBroadcast;
|
||||
|
||||
|
||||
private static readonly Uri StatusUri = new("https://cleanuparr-status.pages.dev/status.json");
|
||||
private static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(10);
|
||||
private static readonly TimeSpan StartupDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
|
||||
public AppStatusRefreshService(
|
||||
ILogger<AppStatusRefreshService> logger,
|
||||
IHubContext<AppHub> hubContext,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
AppStatusSnapshot snapshot,
|
||||
JsonSerializerOptions jsonOptions
|
||||
JsonSerializerOptions jsonOptions,
|
||||
IServiceScopeFactory scopeFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -36,6 +41,7 @@ public sealed class AppStatusRefreshService : BackgroundService
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_snapshot = snapshot;
|
||||
_jsonOptions = jsonOptions;
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@@ -66,13 +72,22 @@ public sealed class AppStatusRefreshService : BackgroundService
|
||||
|
||||
private async Task RefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await IsStatusCheckEnabledAsync(cancellationToken))
|
||||
{
|
||||
if (_snapshot.UpdateLatestVersion(null, out var status))
|
||||
{
|
||||
await BroadcastAsync(status, cancellationToken);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var client = _httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
|
||||
|
||||
using var response = await client.GetAsync(StatusUri, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
var payload = await JsonSerializer.DeserializeAsync<Status>(stream, _jsonOptions, cancellationToken: cancellationToken);
|
||||
var latest = payload?.Version;
|
||||
@@ -96,6 +111,29 @@ public sealed class AppStatusRefreshService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsStatusCheckEnabledAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using AsyncServiceScope scope = _scopeFactory.CreateAsyncScope();
|
||||
await using DataContext dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
GeneralConfig config = await dataContext.GeneralConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync(cancellationToken);
|
||||
return config.StatusCheckEnabled;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read StatusCheckEnabled setting, proceeding with status check");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BroadcastAsync(AppStatus status, CancellationToken cancellationToken)
|
||||
{
|
||||
if (status.Equals(_lastBroadcast))
|
||||
|
||||
1273
code/backend/Cleanuparr.Persistence/Migrations/Data/20260212204459_AddAppStatusSetting.Designer.cs
generated
Normal file
1273
code/backend/Cleanuparr.Persistence/Migrations/Data/20260212204459_AddAppStatusSetting.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAppStatusSetting : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "status_check_enabled",
|
||||
table: "general_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "status_check_enabled",
|
||||
table: "general_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,6 +302,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_enabled");
|
||||
|
||||
b.Property<bool>("StatusCheckEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("status_check_enabled");
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "Log", "Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig.Log#LoggingConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
@@ -26,6 +26,8 @@ public sealed record GeneralConfig : IConfig
|
||||
|
||||
public ushort SearchDelay { get; set; } = Constants.DefaultSearchDelaySeconds;
|
||||
|
||||
public bool StatusCheckEnabled { get; set; } = true;
|
||||
|
||||
public string EncryptionKey { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
public List<string> IgnoredDownloads { get; set; } = [];
|
||||
|
||||
@@ -49,6 +49,7 @@ export class DocumentationService {
|
||||
'httpCertificateValidation': 'http-certificate-validation',
|
||||
'searchEnabled': 'search-enabled',
|
||||
'searchDelay': 'search-delay',
|
||||
'statusCheckEnabled': 'status-check',
|
||||
'log.level': 'log-level',
|
||||
'log.rollingSizeMB': 'rolling-size-mb',
|
||||
'log.retainedFileCount': 'retained-file-count',
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<app-toggle label="Display Support Banner" [(checked)]="displaySupportBanner"
|
||||
hint="Show the support section on the dashboard with links to GitHub and sponsors"
|
||||
helpKey="general:displaySupportBanner" />
|
||||
<app-toggle label="Status Check" [(checked)]="statusCheckEnabled"
|
||||
hint="When enabled, Cleanuparr will periodically check for new versions. Disable this if your environment has restricted outbound network access."
|
||||
helpKey="general:statusCheckEnabled" />
|
||||
|
||||
<div class="form-divider"></div>
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export class GeneralSettingsComponent implements OnInit, HasPendingChanges {
|
||||
readonly httpCertificateValidation = signal<unknown>(CertificateValidationType.Enabled);
|
||||
readonly searchEnabled = signal(true);
|
||||
readonly searchDelay = signal<number | null>(5);
|
||||
readonly statusCheckEnabled = signal(true);
|
||||
readonly ignoredDownloads = signal<string[]>([]);
|
||||
|
||||
// Logging
|
||||
@@ -165,6 +166,7 @@ export class GeneralSettingsComponent implements OnInit, HasPendingChanges {
|
||||
this.httpCertificateValidation.set(config.httpCertificateValidation);
|
||||
this.searchEnabled.set(config.searchEnabled);
|
||||
this.searchDelay.set(config.searchDelay);
|
||||
this.statusCheckEnabled.set(config.statusCheckEnabled);
|
||||
this.ignoredDownloads.set(config.ignoredDownloads ?? []);
|
||||
if (config.log) {
|
||||
this.logLevel.set(config.log.level);
|
||||
@@ -200,6 +202,7 @@ export class GeneralSettingsComponent implements OnInit, HasPendingChanges {
|
||||
httpCertificateValidation: this.httpCertificateValidation() as CertificateValidationType,
|
||||
searchEnabled: this.searchEnabled(),
|
||||
searchDelay: this.searchDelay() ?? 5,
|
||||
statusCheckEnabled: this.statusCheckEnabled(),
|
||||
ignoredDownloads: this.ignoredDownloads(),
|
||||
log: {
|
||||
level: this.logLevel() as LogEventLevel,
|
||||
@@ -237,6 +240,7 @@ export class GeneralSettingsComponent implements OnInit, HasPendingChanges {
|
||||
httpCertificateValidation: this.httpCertificateValidation(),
|
||||
searchEnabled: this.searchEnabled(),
|
||||
searchDelay: this.searchDelay(),
|
||||
statusCheckEnabled: this.statusCheckEnabled(),
|
||||
ignoredDownloads: this.ignoredDownloads(),
|
||||
logLevel: this.logLevel(),
|
||||
logRollingSizeMB: this.logRollingSizeMB(),
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface GeneralConfig {
|
||||
httpCertificateValidation: CertificateValidationType;
|
||||
searchEnabled: boolean;
|
||||
searchDelay: number;
|
||||
statusCheckEnabled: boolean;
|
||||
log?: LoggingConfig;
|
||||
ignoredDownloads: string[];
|
||||
}
|
||||
|
||||
@@ -41,6 +41,19 @@ Logs all operations without making changes. Test your configuration safely befor
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Status Check"
|
||||
icon="📡"
|
||||
>
|
||||
|
||||
Periodically checks for new Cleanuparr versions. Disable this if your environment has restricted outbound network access.
|
||||
|
||||
<Note>
|
||||
When disabled, the version check and "update available" notification in the sidebar will not appear.
|
||||
</Note>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
Reference in New Issue
Block a user