[CHANGE] Reworked event console
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="app.css"/>
|
||||
<link href="Manager.App.styles.css" rel="stylesheet" />
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet"/>
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css?v=@Metadata.Version" rel="stylesheet"/>
|
||||
@@ -17,6 +18,8 @@
|
||||
<Routes @rendermode="InteractiveServer"/>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js?v=@Metadata.Version"></script>
|
||||
<script src="js/tz.js"></script>
|
||||
<script src="js/eventConsole.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
21
Manager.App/Components/Application/System/EventConsole.razor
Normal file
21
Manager.App/Components/Application/System/EventConsole.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
@inject IJSRuntime JsRuntime
|
||||
@implements IDisposable
|
||||
|
||||
<MudPaper Elevation="Elevation" Class="@Class" Style="@Style">
|
||||
<MudStack Class="ml-2 mb-2" Spacing="2" Row>
|
||||
<MudStack Spacing="1">
|
||||
<MudText Typo="Typo.h5">Live service events</MudText>
|
||||
<MudText Typo="Typo.caption">@($"{_serviceEvents.Count} events")</MudText>
|
||||
</MudStack>
|
||||
<MudSwitch @bind-Value="@_autoScroll">Auto-scroll</MudSwitch>
|
||||
</MudStack>
|
||||
<div @ref="@_consoleContainer" class="console-container" @onwheel="OnUserScroll">
|
||||
<Virtualize ItemsProvider="VirtualizedItemsProvider" Context="serviceEvent">
|
||||
<div class="log-line">
|
||||
@TimeZoneInfo.ConvertTime(serviceEvent.DateUtc, _timeZone)
|
||||
<span class="log-severity @GetLogClass(serviceEvent)">@serviceEvent.Severity</span> [<span style="color: #1565c0">@serviceEvent.Source</span>]
|
||||
<span style="color: snow">@serviceEvent.Message</span>
|
||||
</div>
|
||||
</Virtualize>
|
||||
</div>
|
||||
</MudPaper>
|
155
Manager.App/Components/Application/System/EventConsole.razor.cs
Normal file
155
Manager.App/Components/Application/System/EventConsole.razor.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using DotBased.Logging;
|
||||
using Manager.App.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Web.Virtualization;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Manager.App.Components.Application.System;
|
||||
|
||||
public partial class EventConsole : ComponentBase
|
||||
{
|
||||
private const int BatchDelayMs = 1000;
|
||||
private List<ServiceEvent> _serviceEvents = [];
|
||||
private readonly List<ServiceEvent> _batchBuffer = [];
|
||||
private readonly SemaphoreSlim _batchLock = new(1, 1);
|
||||
private ElementReference _consoleContainer;
|
||||
private bool _autoScroll = true;
|
||||
private CancellationTokenSource _cts = new();
|
||||
private TimeZoneInfo _timeZone = TimeZoneInfo.Local;
|
||||
|
||||
[Parameter]
|
||||
public List<ServiceEvent> InitialEvents { get; set; } = [];
|
||||
[Parameter]
|
||||
public IAsyncEnumerable<ServiceEvent>? AsyncEnumerable { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Elevation { get; set; }
|
||||
[Parameter]
|
||||
public string? Class { get; set; }
|
||||
[Parameter]
|
||||
public string? Style { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_serviceEvents.AddRange(InitialEvents);
|
||||
var jsTimeZone = await JsRuntime.InvokeAsync<string>("getUserTimeZone");
|
||||
if (!string.IsNullOrEmpty(jsTimeZone))
|
||||
{
|
||||
_timeZone = TimeZoneInfo.FindSystemTimeZoneById(jsTimeZone);
|
||||
}
|
||||
_ = Task.Run(() => ReadEventStreamsAsync(_cts.Token));
|
||||
}
|
||||
|
||||
private async Task ReadEventStreamsAsync(CancellationToken token)
|
||||
{
|
||||
if (AsyncEnumerable == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await foreach (var serviceEvent in AsyncEnumerable.WithCancellation(token))
|
||||
{
|
||||
await _batchLock.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
_batchBuffer.Add(serviceEvent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_batchLock.Release();
|
||||
}
|
||||
|
||||
_ = BatchUpdateUi();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLogClass(ServiceEvent serviceEvent) =>
|
||||
serviceEvent.Severity switch
|
||||
{
|
||||
LogSeverity.Info => "log-info",
|
||||
LogSeverity.Warning => "log-warning",
|
||||
LogSeverity.Error => "log-error",
|
||||
LogSeverity.Debug => "log-debug",
|
||||
LogSeverity.Trace => "log-trace",
|
||||
LogSeverity.Fatal => "log-fatal",
|
||||
LogSeverity.Verbose => "log-error",
|
||||
_ => "log-info"
|
||||
};
|
||||
|
||||
private DateTime _lastBatchUpdate = DateTime.MinValue;
|
||||
private bool _updateScheduled;
|
||||
|
||||
private async Task BatchUpdateUi()
|
||||
{
|
||||
if (_updateScheduled) return;
|
||||
_updateScheduled = true;
|
||||
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var elapsed = (DateTime.UtcNow - _lastBatchUpdate).TotalMilliseconds;
|
||||
if (elapsed < BatchDelayMs)
|
||||
{
|
||||
await Task.Delay(BatchDelayMs - (int)elapsed, _cts.Token);
|
||||
}
|
||||
|
||||
List<ServiceEvent> batch;
|
||||
await _batchLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_batchBuffer.Count == 0) continue;
|
||||
batch = new List<ServiceEvent>(_batchBuffer);
|
||||
_batchBuffer.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_batchLock.Release();
|
||||
}
|
||||
|
||||
_serviceEvents.AddRange(batch);
|
||||
_lastBatchUpdate = DateTime.UtcNow;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (_autoScroll)
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("scrollToBottom", _consoleContainer);
|
||||
}
|
||||
|
||||
_updateScheduled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUserScroll(WheelEventArgs e)
|
||||
{
|
||||
_ = UpdateAutoScroll();
|
||||
}
|
||||
|
||||
private async Task UpdateAutoScroll()
|
||||
{
|
||||
if (_consoleContainer.Context != null)
|
||||
{
|
||||
var scrollInfo = await JsRuntime.InvokeAsync<ScrollInfo>("getScrollInfo", _consoleContainer);
|
||||
_autoScroll = scrollInfo.ScrollTop + scrollInfo.ClientHeight >= scrollInfo.ScrollHeight - 20;
|
||||
}
|
||||
}
|
||||
|
||||
private ValueTask<ItemsProviderResult<ServiceEvent>> VirtualizedItemsProvider(ItemsProviderRequest request)
|
||||
{
|
||||
var items = _serviceEvents.Skip(request.StartIndex).Take(request.Count);
|
||||
return ValueTask.FromResult(new ItemsProviderResult<ServiceEvent>(items, _serviceEvents.Count));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_batchLock.Dispose();
|
||||
}
|
||||
|
||||
private class ScrollInfo
|
||||
{
|
||||
public double ScrollTop { get; set; }
|
||||
public double ScrollHeight { get; set; }
|
||||
public double ClientHeight { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
.console-container {
|
||||
background-color: #1e1e1e;
|
||||
color: #9c9898;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.log-severity{
|
||||
display: inline-block;
|
||||
width: 8ch;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #3f6b81;
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: #f8f802;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #f44747;
|
||||
}
|
||||
|
||||
.log-debug {
|
||||
color: #e110ff;
|
||||
}
|
||||
|
||||
.log-trace {
|
||||
color: #535353;
|
||||
}
|
||||
|
||||
.log-fatal {
|
||||
color: #af1e1e;
|
||||
}
|
||||
|
||||
.log-verbose {
|
||||
color: #8085ff;
|
||||
}
|
@@ -1,8 +1,9 @@
|
||||
@page "/Services"
|
||||
@using Manager.App.Services.System
|
||||
@using Manager.App.Components.Application.System
|
||||
@implements IDisposable
|
||||
|
||||
@inject BackgroundServiceManager ServiceManager
|
||||
@inject BackgroundServiceRegistry ServiceRegistry
|
||||
|
||||
<title>Services</title>
|
||||
|
||||
@@ -37,47 +38,5 @@
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
|
||||
<MudPaper Elevation="0" Class="mt-3" Style="flex: 1; display: flex; flex-direction: column; min-height: 0;">
|
||||
<MudStack Class="ml-2 mb-2" Spacing="1">
|
||||
<MudText Typo="Typo.h5">Service events</MudText>
|
||||
<MudText Typo="Typo.caption">@($"{_serviceEvents.Count}/{VisibleEventCapacity} events")</MudText>
|
||||
</MudStack>
|
||||
<div class="console-container">
|
||||
<Virtualize Items="_serviceEvents" Context="serviceEvent">
|
||||
<div class="log-line">
|
||||
@($"{serviceEvent.Date:HH:mm:ss} | {serviceEvent.Severity} | {serviceEvent.Source} - {serviceEvent.Message}")
|
||||
</div>
|
||||
</Virtualize>
|
||||
</div>
|
||||
</MudPaper>
|
||||
|
||||
<style>
|
||||
.console-container {
|
||||
background-color: #1e1e1e;
|
||||
color: #9c9898;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.log-info {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.log-warning {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.log-error {
|
||||
color: #f44747;
|
||||
}
|
||||
</style>
|
||||
<EventConsole AsyncEnumerable="@GetEventAsyncEnumerable()" InitialEvents="@GetInitialEvents()"
|
||||
Elevation="0" Class="mt-3" Style="flex: 1; display: flex; flex-direction: column; min-height: 0;"/>
|
||||
|
@@ -1,4 +1,3 @@
|
||||
using DotBased.Logging;
|
||||
using Manager.App.Extensions;
|
||||
using Manager.App.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -7,57 +6,30 @@ namespace Manager.App.Components.Pages;
|
||||
|
||||
public partial class Services : ComponentBase
|
||||
{
|
||||
private const int VisibleEventCapacity = 500;
|
||||
private string _searchText = "";
|
||||
private List<ExtendedBackgroundService> _backgroundServices = [];
|
||||
|
||||
private List<ServiceEvent> _serviceEvents = [];
|
||||
private CancellationTokenSource _cts = new();
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_backgroundServices = ServiceManager.GetServices();
|
||||
_ = Task.Run(() => ReadEventStreamsAsync(_cts.Token));
|
||||
_backgroundServices = ServiceRegistry.GetServices();
|
||||
}
|
||||
|
||||
private Func<ExtendedBackgroundService, bool> QuickFilter
|
||||
=> x => string.IsNullOrWhiteSpace(_searchText) || $"{x.Name} {x.Description} {x.State} {x.ExecuteInterval}".Contains(_searchText);
|
||||
|
||||
private async Task ReadEventStreamsAsync(CancellationToken token)
|
||||
private IAsyncEnumerable<ServiceEvent> GetEventAsyncEnumerable()
|
||||
{
|
||||
if (_backgroundServices.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var totalToGet = VisibleEventCapacity / _backgroundServices.Count;
|
||||
_serviceEvents.AddRange(_backgroundServices.SelectMany(x => x.ProgressEvents.Items.TakeLast(totalToGet)));
|
||||
|
||||
var asyncEnumerators = _backgroundServices.Select(x => x.ProgressEvents.GetStreamAsync());
|
||||
await foreach (var serviceEvent in AsyncEnumerableExtensions.Merge(asyncEnumerators, token))
|
||||
{
|
||||
if (!_serviceEvents.Contains(serviceEvent))
|
||||
{
|
||||
_serviceEvents.Add(serviceEvent);
|
||||
}
|
||||
|
||||
if (_serviceEvents.Count > VisibleEventCapacity)
|
||||
{
|
||||
_serviceEvents.RemoveAt(0);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
return AsyncEnumerableExtensions.Merge(asyncEnumerators, CancellationToken.None);
|
||||
}
|
||||
|
||||
private string GetLogClass(ServiceEvent serviceEvent) =>
|
||||
serviceEvent.Severity switch
|
||||
{
|
||||
LogSeverity.Info => "log-info",
|
||||
LogSeverity.Warning => "log-warning",
|
||||
LogSeverity.Error => "log-error",
|
||||
_ => "log-info"
|
||||
};
|
||||
private List<ServiceEvent> GetInitialEvents()
|
||||
{
|
||||
var totalToGet = 1000 / _backgroundServices.Count;
|
||||
var initial = _backgroundServices.SelectMany(x => x.ProgressEvents.Items.TakeLast(totalToGet));
|
||||
return initial.ToList();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
@@ -1,6 +1,4 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
|
@@ -25,9 +25,7 @@ public static class DependencyInjection
|
||||
options.UseSqlite($"Data Source={dbPath}");
|
||||
});
|
||||
|
||||
|
||||
builder.Services.AddSingleton<BackgroundServiceManager>();
|
||||
builder.Services.AddHostedService<ClientService>();
|
||||
builder.RegisterExtendedBackgroundServices();
|
||||
|
||||
builder.Services.AddScoped<ILibraryService, LibraryService>();
|
||||
}
|
||||
@@ -88,4 +86,20 @@ public static class DependencyInjection
|
||||
builder.Logging.SetMinimumLevel(isDevelopment ? LogLevel.Trace : LogLevel.Information);
|
||||
builder.Logging.AddDotBasedLoggerProvider(LogService.Options);
|
||||
}
|
||||
|
||||
private static void RegisterExtendedBackgroundServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
var assembly = typeof(Program).Assembly;
|
||||
|
||||
foreach (var exBgService in assembly.GetTypes()
|
||||
.Where(t => typeof(ExtendedBackgroundService).IsAssignableFrom(t)
|
||||
&& t is { IsClass: true, IsAbstract: false }))
|
||||
{
|
||||
builder.Services.AddSingleton(exBgService);
|
||||
builder.Services.AddSingleton(typeof(ExtendedBackgroundService), sp => (ExtendedBackgroundService)sp.GetRequiredService(exBgService));
|
||||
builder.Services.AddSingleton<IHostedService>(sp => (IHostedService)sp.GetRequiredService(exBgService));
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<BackgroundServiceRegistry>();
|
||||
}
|
||||
}
|
@@ -22,6 +22,7 @@
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" />
|
||||
<_ContentIncludedByDefault Remove="wwwroot\js\console.js" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -1,37 +1,27 @@
|
||||
using DotBased.Logging;
|
||||
using Manager.App.Services.System;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace Manager.App.Services;
|
||||
|
||||
public abstract class ExtendedBackgroundService : BackgroundService
|
||||
public abstract class ExtendedBackgroundService(string name, string description, ILogger logger, TimeSpan? executeInterval = null)
|
||||
: BackgroundService
|
||||
{
|
||||
private TaskCompletionSource _resumeSignal = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly ILogger _logger;
|
||||
public ServiceState State { get; private set; } = ServiceState.Stopped;
|
||||
public CircularBuffer<ServiceEvent> ProgressEvents { get; } = new(500);
|
||||
public string Name { get; }
|
||||
public string Description { get; set; }
|
||||
public TimeSpan ExecuteInterval { get; set; }
|
||||
|
||||
public ExtendedBackgroundService(string name, string description, ILogger logger, BackgroundServiceManager manager, TimeSpan? executeInterval = null)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
_logger = logger;
|
||||
manager.RegisterService(this);
|
||||
ExecuteInterval = executeInterval ?? TimeSpan.FromMinutes(1);
|
||||
}
|
||||
public string Name { get; } = name;
|
||||
public string Description { get; set; } = description;
|
||||
public TimeSpan ExecuteInterval { get; set; } = executeInterval ?? TimeSpan.FromMinutes(1);
|
||||
|
||||
protected sealed override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
State = ServiceState.Running;
|
||||
_logger.LogInformation("Initializing background service: {ServiceName}", Name);
|
||||
logger.LogInformation("Initializing background service: {ServiceName}", Name);
|
||||
await InitializeAsync(stoppingToken);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Running background service: {ServiceName}", Name);
|
||||
logger.LogInformation("Running background service: {ServiceName}", Name);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (State == ServiceState.Paused)
|
||||
@@ -49,10 +39,10 @@ public abstract class ExtendedBackgroundService : BackgroundService
|
||||
if (e is not OperationCanceledException)
|
||||
{
|
||||
State = ServiceState.Faulted;
|
||||
_logger.LogError(e,"Background service {ServiceName} faulted!", Name);
|
||||
logger.LogError(e,"Background service {ServiceName} faulted!", Name);
|
||||
throw;
|
||||
}
|
||||
_logger.LogInformation(e,"Service {ServiceName} received cancellation", Name);
|
||||
logger.LogInformation(e,"Service {ServiceName} received cancellation", Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -67,7 +57,7 @@ public abstract class ExtendedBackgroundService : BackgroundService
|
||||
if (State == ServiceState.Running)
|
||||
{
|
||||
State = ServiceState.Paused;
|
||||
_logger.LogInformation("Pauses service: {ServiceName}", Name);
|
||||
logger.LogInformation("Pauses service: {ServiceName}", Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +67,7 @@ public abstract class ExtendedBackgroundService : BackgroundService
|
||||
{
|
||||
State = ServiceState.Running;
|
||||
_resumeSignal.TrySetResult();
|
||||
_logger.LogInformation("Resumed service: {ServiceName}", Name);
|
||||
logger.LogInformation("Resumed service: {ServiceName}", Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,4 +93,4 @@ public enum ServiceState
|
||||
Paused
|
||||
}
|
||||
|
||||
public record struct ServiceEvent(string Source, string Message, DateTime Date, LogSeverity Severity);
|
||||
public record struct ServiceEvent(string Source, string Message, DateTime DateUtc, LogSeverity Severity);
|
@@ -1,16 +0,0 @@
|
||||
namespace Manager.App.Services.System;
|
||||
|
||||
public class BackgroundServiceManager
|
||||
{
|
||||
private readonly HashSet<ExtendedBackgroundService> _backgroundServices = [];
|
||||
|
||||
public void RegisterService(ExtendedBackgroundService service)
|
||||
{
|
||||
_backgroundServices.Add(service);
|
||||
}
|
||||
|
||||
public List<ExtendedBackgroundService> GetServices()
|
||||
{
|
||||
return _backgroundServices.ToList();
|
||||
}
|
||||
}
|
9
Manager.App/Services/System/BackgroundServiceRegistry.cs
Normal file
9
Manager.App/Services/System/BackgroundServiceRegistry.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Manager.App.Services.System;
|
||||
|
||||
public class BackgroundServiceRegistry(IEnumerable<ExtendedBackgroundService> backgroundServices)
|
||||
{
|
||||
public List<ExtendedBackgroundService> GetServices()
|
||||
{
|
||||
return backgroundServices.ToList();
|
||||
}
|
||||
}
|
@@ -7,8 +7,8 @@ using Manager.YouTube;
|
||||
|
||||
namespace Manager.App.Services.System;
|
||||
|
||||
public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientService> logger, BackgroundServiceManager serviceManager)
|
||||
: ExtendedBackgroundService("ClientService", "Managing YouTube clients", logger, serviceManager, TimeSpan.FromMilliseconds(100))
|
||||
public class ClientService(IServiceScopeFactory scopeFactory, ILogger<ClientService> logger)
|
||||
: ExtendedBackgroundService("ClientService", "Managing YouTube clients", logger, TimeSpan.FromMilliseconds(100))
|
||||
{
|
||||
private readonly List<YouTubeClient> _clients = [];
|
||||
private CancellationToken _cancellationToken;
|
||||
|
21
Manager.App/Services/System/TestService.cs
Normal file
21
Manager.App/Services/System/TestService.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using DotBased.Logging;
|
||||
|
||||
namespace Manager.App.Services.System;
|
||||
|
||||
public class TestService(ILogger<TestService> logger) : ExtendedBackgroundService("TestService", "Development service", logger, TimeSpan.FromMilliseconds(100))
|
||||
{
|
||||
protected override Task InitializeAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task ExecuteServiceAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
LogEvent("TestService");
|
||||
LogEvent($"Error {Guid.NewGuid()}", LogSeverity.Error);
|
||||
LogEvent("Something went wrong!", LogSeverity.Warning);
|
||||
LogEvent("Tracing.", LogSeverity.Trace);
|
||||
LogEvent("Fatal error!", LogSeverity.Fatal);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
14
Manager.App/wwwroot/js/eventConsole.js
Normal file
14
Manager.App/wwwroot/js/eventConsole.js
Normal file
@@ -0,0 +1,14 @@
|
||||
window.scrollToBottom = (element) => {
|
||||
if (element) {
|
||||
element.scroll({ top: element.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
window.getScrollInfo = (element) => {
|
||||
if (!element) return null;
|
||||
return {
|
||||
scrollTop: element.scrollTop,
|
||||
scrollHeight: element.scrollHeight,
|
||||
clientHeight: element.clientHeight
|
||||
};
|
||||
};
|
3
Manager.App/wwwroot/js/tz.js
Normal file
3
Manager.App/wwwroot/js/tz.js
Normal file
@@ -0,0 +1,3 @@
|
||||
function getUserTimeZone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
Reference in New Issue
Block a user