feat(phase-7-frontend): anomaly feed UI + nav badge + Settings toggle (+31 bUnit tests)
Frontend portion of Phase 7. Backend (commit a6ff368) had already shipped
the AnomalyDetector, DetectAnomaliesUseCase, AnomalyDetectionPoller, and
all DI wiring. This commit adds the user-facing surfaces.
New surfaces (Option A routing — folder-per-feature):
- Pages/Anomalies/AnomalyFeed.razor (@page /anomalies) — replaces the
Phase 5 placeholder with a severity-coded card stream, filter chips
(severity / sport / date), unread-count summary, 'Mark all read' action.
- Pages/Anomalies/Detail.razor (@page /anomalies/{id:guid}) — m-detail-header
lockup + AnomalyEvidence panel + back link to /events/{eventCode}.
New components:
- AnomalyCard.razor — severity-tinted left border (signal-red on High,
amber on Medium, neutral on Low) + SeverityBadge pill + sport icon +
pre→post tabular-mono rate strip + relative time. Click navigates.
- SeverityBadge.razor — small pill mapping score → bucket per backend
handoff (Low <0.45, Medium <0.60, High ≥0.60).
- AnomalyEvidence.razor — two-column pre/post panel with implied-prob
bars + raw rates; favourite-swap callout when argmax(p_pre) ≠ argmax(p_post);
signal-red 3px left border on the post column. Handles 2-way (no draw).
State + service split mirrors Phase 6's pattern:
- AnomalyViewModels.cs — AnomalyListItem / AnomalyDetailVm / Severity enum
/ AnomalyEvidenceSnapshot record. Severity computed in the view-model
from Score.
- IAnomalyBrowsingService / AnomalyBrowsingService — wraps IAnomalyRepository,
parses Anomaly.EvidenceJson into typed view-models, applies filters
client-side. Methods: ListAsync(filter, ct), GetByIdAsync(id, ct),
GetUnreadCountAsync(since, ct).
- AnomalyBrowsingState — Singleton holding AnomalyFilter (severity threshold,
sport set, date range) + LastSeenUtc + cached UnreadCount. OnChange event.
Nav badge:
- NavBody.razor subscribes to AnomalyBrowsingState.OnChange, renders a
pulsing red m-nav__badge when UnreadCount > 0. Badge resets when the
user clicks 'Mark all read' on the feed toolbar.
Settings toggle:
- Settings.razor — added Workers:AnomalyDetectionEnabled toggle (backend
added the flag). Localized via Settings.Worker.AnomalyDetectionEnabled.
- Marathon.UI.Services.WorkerOptions mirror — added AnomalyDetectionEnabled
(default true).
Localization: +30 RU/EN keys following the dot-segmented convention
(Anomaly.*, Settings.Worker.AnomalyDetectionEnabled). Full key parity verified.
Tests (+31 bUnit, all passing):
- AnomalyFeedTests, AnomalyDetailTests
- AnomalyCardTests, SeverityBadgeTests, AnomalyEvidenceTests
- FakeAnomalyBrowsingService support fake registered in MarathonTestContext.
Routing: deleted the Phase 5 Pages/Anomalies.razor placeholder; new feed
page lives at Pages/Anomalies/AnomalyFeed.razor.
Build: 0 warnings, 0 errors.
Tests: Domain 109 + Application 19 + Infrastructure 80 + UI 68 = 276/276
(baseline 245, +31 new bUnit tests, no regressions).
Phase 7 status: ✅ Done (backend + frontend both complete, awaiting review).
Known deferral: AnomalyBrowsingState.LastSeenUtc is in-memory only; the
unread-count badge resets on app restart. Acceptable for now; Phase 9 may
extend ISettingsWriter or add an ILastSeenStore.
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
@page "/anomalies"
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
|
||||
<PageTitle>@L["App.Title"] · @L["Nav.Anomalies"]</PageTitle>
|
||||
<Placeholders Surface="@L["Nav.Section.Analysis"]" Title="@L["Nav.Anomalies"]" />
|
||||
@@ -0,0 +1,304 @@
|
||||
@page "/anomalies"
|
||||
@using Marathon.UI.Components
|
||||
@implements IDisposable
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
@inject IAnomalyBrowsingService Anomalies
|
||||
@inject AnomalyBrowsingState State
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<PageTitle>@L["App.Title"] · @L["Anomaly.Title"]</PageTitle>
|
||||
|
||||
<section class="m-shell">
|
||||
<header class="m-rise m-rise-1" style="display: grid; gap: var(--m-space-3); max-width: 880px;">
|
||||
<span class="m-kicker" style="color: var(--m-c-anomaly); border-color: var(--m-c-anomaly);">
|
||||
@L["Nav.Section.Analysis"]
|
||||
</span>
|
||||
<h1 class="m-display" style="font-size: clamp(2rem, 4vw, 3rem);">@L["Anomaly.Title"]</h1>
|
||||
<p style="color: var(--m-c-ink-soft); max-width: 60ch;">@L["Anomaly.Lede"]</p>
|
||||
|
||||
<dl class="m-anomaly-feed__stats" aria-label="@L["Anomaly.Title"]">
|
||||
<div class="m-anomaly-feed__stat">
|
||||
<dt>@L["Anomaly.Stat.Total"]</dt>
|
||||
<dd class="m-mono">@_items.Count</dd>
|
||||
</div>
|
||||
<div class="m-anomaly-feed__stat m-anomaly-feed__stat--high">
|
||||
<dt>@L["Anomaly.Severity.High"]</dt>
|
||||
<dd class="m-mono">@_items.Count(i => i.Severity == AnomalySeverity.High)</dd>
|
||||
</div>
|
||||
<div class="m-anomaly-feed__stat m-anomaly-feed__stat--medium">
|
||||
<dt>@L["Anomaly.Severity.Medium"]</dt>
|
||||
<dd class="m-mono">@_items.Count(i => i.Severity == AnomalySeverity.Medium)</dd>
|
||||
</div>
|
||||
<div class="m-anomaly-feed__stat">
|
||||
<dt>@L["Anomaly.Severity.Low"]</dt>
|
||||
<dd class="m-mono">@_items.Count(i => i.Severity == AnomalySeverity.Low)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</header>
|
||||
|
||||
<div class="m-list-toolbar m-rise m-rise-2" role="toolbar" aria-label="@L["PreMatch.Filter.Toolbar"]">
|
||||
<div class="m-list-toolbar__row m-list-toolbar__chips">
|
||||
<span class="m-list-toolbar__label">@L["Anomaly.Filter.Severity"]</span>
|
||||
@foreach (var severity in _severityOptions)
|
||||
{
|
||||
var current = severity;
|
||||
var active = _filter.MinSeverity == current;
|
||||
<button type="button"
|
||||
class="m-chip @(active ? "is-active" : null)"
|
||||
aria-pressed="@active"
|
||||
data-test="severity-chip"
|
||||
data-severity="@current"
|
||||
@onclick="() => ToggleSeverity(current)">
|
||||
@SeverityLabel(current)
|
||||
</button>
|
||||
}
|
||||
<button type="button"
|
||||
class="m-chip @(_filter.MinSeverity is null ? "is-active" : null)"
|
||||
aria-pressed="@(_filter.MinSeverity is null)"
|
||||
data-test="severity-chip-any"
|
||||
@onclick="ClearSeverity">
|
||||
@L["Anomaly.Filter.AnySeverity"]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (_availableSports.Count > 0)
|
||||
{
|
||||
<div class="m-list-toolbar__row m-list-toolbar__chips">
|
||||
<span class="m-list-toolbar__label">@L["Anomaly.Filter.Sport"]</span>
|
||||
@foreach (var sportCode in _availableSports)
|
||||
{
|
||||
var localCode = sportCode;
|
||||
var active = _filter.SportCodes is { Count: > 0 } sc && sc.Contains(localCode);
|
||||
var sportLabel = SportLabel(localCode);
|
||||
<button type="button"
|
||||
class="m-chip @(active ? "is-active" : null)"
|
||||
aria-pressed="@active"
|
||||
data-test="sport-chip"
|
||||
@onclick="() => ToggleSport(localCode)">
|
||||
<SportIcon Code="@localCode" Label="@sportLabel" ClassName="m-chip__icon" />
|
||||
<span>@sportLabel</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="m-list-toolbar__row">
|
||||
<div class="m-list-toolbar__group">
|
||||
<label class="m-list-toolbar__label">@L["Anomaly.Filter.From"]</label>
|
||||
<input class="m-input" type="date" value="@FormatDate(_filter.From)"
|
||||
aria-label="@L["Anomaly.Filter.From"]"
|
||||
@onchange="OnFromChanged" />
|
||||
</div>
|
||||
<div class="m-list-toolbar__group">
|
||||
<label class="m-list-toolbar__label">@L["Anomaly.Filter.To"]</label>
|
||||
<input class="m-input" type="date" value="@FormatDate(_filter.To)"
|
||||
aria-label="@L["Anomaly.Filter.To"]"
|
||||
@onchange="OnToChanged" />
|
||||
</div>
|
||||
<button type="button" class="m-chip" @onclick="MarkAllRead" data-test="mark-read">
|
||||
@L["Anomaly.Filter.MarkRead"]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-anomaly-feed m-rise m-rise-3" role="region" aria-label="@L["Anomaly.Title"]" data-test="anomaly-feed">
|
||||
@if (_loading && _items.Count == 0)
|
||||
{
|
||||
<div class="m-list-empty">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
|
||||
<span class="m-mono">@L["Common.Loading"]</span>
|
||||
</div>
|
||||
}
|
||||
else if (_items.Count == 0)
|
||||
{
|
||||
<div class="m-list-empty" data-test="anomaly-empty">
|
||||
<span class="m-kicker" style="border-color: var(--m-c-ink-soft); color: var(--m-c-ink-soft);">
|
||||
@L["Common.Empty"]
|
||||
</span>
|
||||
<p style="color: var(--m-c-ink-soft); margin-top: var(--m-space-3); max-width: 50ch;">
|
||||
@L["Anomaly.Empty.NoneInRange"]
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="m-anomaly-feed__list">
|
||||
@foreach (var item in _items)
|
||||
{
|
||||
<AnomalyCard Item="item" OnClick="HandleClick" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.m-anomaly-feed__stats {
|
||||
display: flex;
|
||||
gap: var(--m-space-5);
|
||||
margin: var(--m-space-3) 0 0;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.m-anomaly-feed__stat {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
padding-right: var(--m-space-4);
|
||||
border-right: 1px solid var(--m-c-rule);
|
||||
}
|
||||
.m-anomaly-feed__stat:last-child { border-right: 0; padding-right: 0; }
|
||||
.m-anomaly-feed__stat dt {
|
||||
font-family: var(--m-font-mono);
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
color: var(--m-c-ink-soft);
|
||||
}
|
||||
.m-anomaly-feed__stat dd {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--m-c-ink);
|
||||
font-feature-settings: var(--m-num-feature);
|
||||
}
|
||||
.m-anomaly-feed__stat--high dd { color: var(--m-c-anomaly); }
|
||||
.m-anomaly-feed__stat--medium dd { color: var(--m-c-accent); }
|
||||
|
||||
.m-anomaly-feed__list {
|
||||
display: grid;
|
||||
gap: var(--m-space-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private static readonly AnomalySeverity[] _severityOptions =
|
||||
{ AnomalySeverity.Low, AnomalySeverity.Medium, AnomalySeverity.High };
|
||||
|
||||
private List<AnomalyListItem> _items = new();
|
||||
private IReadOnlyList<int> _availableSports = Array.Empty<int>();
|
||||
private bool _loading = true;
|
||||
private CancellationTokenSource? _loadCts;
|
||||
private AnomalyFilter _filter = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_filter = State.Filter;
|
||||
State.OnChange += OnStateChanged;
|
||||
|
||||
try
|
||||
{
|
||||
_availableSports = await Anomalies.ListKnownSportCodesAsync(CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_availableSports = Array.Empty<int>();
|
||||
}
|
||||
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private void OnStateChanged()
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
_loadCts?.Cancel();
|
||||
_loadCts = new CancellationTokenSource();
|
||||
var ct = _loadCts.Token;
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var rows = await Anomalies.ListAsync(_filter, ct);
|
||||
if (ct.IsCancellationRequested) return;
|
||||
_items = rows.ToList();
|
||||
var unread = await Anomalies.GetUnreadCountAsync(State.LastSeenUtc, ct);
|
||||
State.SetUnreadCount(unread);
|
||||
}
|
||||
catch (OperationCanceledException) { /* superseded */ }
|
||||
catch
|
||||
{
|
||||
_items = new List<AnomalyListItem>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateFilter(AnomalyFilter next)
|
||||
{
|
||||
_filter = next;
|
||||
State.UpdateFilter(next);
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private Task ToggleSeverity(AnomalySeverity severity)
|
||||
=> UpdateFilter(_filter with { MinSeverity = _filter.MinSeverity == severity ? null : severity });
|
||||
|
||||
private Task ClearSeverity() => UpdateFilter(_filter with { MinSeverity = null });
|
||||
|
||||
private Task ToggleSport(int code)
|
||||
{
|
||||
var existing = _filter.SportCodes?.ToList() ?? new List<int>();
|
||||
if (!existing.Remove(code)) existing.Add(code);
|
||||
return UpdateFilter(_filter with { SportCodes = existing.Count == 0 ? null : existing });
|
||||
}
|
||||
|
||||
private async Task OnFromChanged(ChangeEventArgs e)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||
{
|
||||
var moscow = TimeSpan.FromHours(3);
|
||||
await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) });
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnToChanged(ChangeEventArgs e)
|
||||
{
|
||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||
{
|
||||
var moscow = TimeSpan.FromHours(3);
|
||||
await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) });
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkAllRead()
|
||||
{
|
||||
State.MarkAllSeen(DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private void HandleClick(AnomalyListItem item)
|
||||
{
|
||||
Nav.NavigateTo($"/anomalies/{item.Id}");
|
||||
}
|
||||
|
||||
private string SeverityLabel(AnomalySeverity s) => s switch
|
||||
{
|
||||
AnomalySeverity.High => L["Anomaly.Severity.High"],
|
||||
AnomalySeverity.Medium => L["Anomaly.Severity.Medium"],
|
||||
_ => L["Anomaly.Severity.Low"],
|
||||
};
|
||||
|
||||
private string SportLabel(int code) => code switch
|
||||
{
|
||||
6 => L["Sport.Basketball"],
|
||||
11 => L["Sport.Football"],
|
||||
22723 => L["Sport.Tennis"],
|
||||
43658 => L["Sport.Hockey"],
|
||||
_ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code),
|
||||
};
|
||||
|
||||
private static string FormatDate(DateTimeOffset? value)
|
||||
=> value?.ToString("yyyy-MM-dd") ?? string.Empty;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
State.OnChange -= OnStateChanged;
|
||||
_loadCts?.Cancel();
|
||||
_loadCts?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
@page "/anomalies/{Id:guid}"
|
||||
@using Marathon.UI.Components
|
||||
@inject IStringLocalizer<SharedResource> L
|
||||
@inject IAnomalyBrowsingService Anomalies
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<PageTitle>@L["App.Title"] · @L["Anomaly.Title"]</PageTitle>
|
||||
|
||||
<section class="m-shell">
|
||||
@if (_loading && _detail is null)
|
||||
{
|
||||
<div class="m-list-empty">
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
|
||||
<span class="m-mono">@L["Common.Loading"]</span>
|
||||
</div>
|
||||
}
|
||||
else if (_detail is null)
|
||||
{
|
||||
<div class="m-list-empty">
|
||||
<span class="m-kicker" style="border-color: var(--m-c-ink-soft); color: var(--m-c-ink-soft);">404</span>
|
||||
<p style="color: var(--m-c-ink-soft);">@L["Anomaly.Detail.NotFound"]</p>
|
||||
<MudButton Variant="Variant.Outlined" OnClick='() => Nav.NavigateTo("/anomalies")'>
|
||||
@L["Anomaly.Detail.BackToFeed"]
|
||||
</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<header class="m-detail-header m-rise m-rise-1">
|
||||
<div class="m-detail-header__lockup">
|
||||
<span class="m-kicker" style="color: var(--m-c-anomaly); border-color: var(--m-c-anomaly);">
|
||||
@KindLabel(_detail.Item.Kind) · @_detail.Item.CountryCode · @_detail.Item.LeagueId
|
||||
</span>
|
||||
<h1 class="m-display" style="font-size: clamp(1.75rem, 3vw, 2.5rem); margin-top: var(--m-space-2);">
|
||||
@_detail.Item.EventTitle
|
||||
</h1>
|
||||
<div class="m-mono" style="margin-top: var(--m-space-2); color: var(--m-c-ink-soft); text-transform: uppercase; letter-spacing: 0.14em; font-size: 0.75rem;">
|
||||
@L["Anomaly.Card.DetectedAt"] @_detail.Item.DetectedAt.ToString("dd MMM yyyy · HH:mm:ss") · MSK
|
||||
</div>
|
||||
</div>
|
||||
<aside class="m-detail-header__odds">
|
||||
<div class="m-detail-header__odds-row">
|
||||
<span class="m-detail-header__odds-label">@L["Anomaly.Card.Score"]</span>
|
||||
<SeverityBadge Severity="_detail.Item.Severity" Score="_detail.Item.Score" />
|
||||
</div>
|
||||
<div class="m-detail-header__odds-row">
|
||||
<span class="m-detail-header__odds-label">@L["Anomaly.Card.GapSeconds"]</span>
|
||||
<span class="m-mono" data-test="suspension-duration">@FormatGap(_detail.Item.SuspensionGapSeconds)</span>
|
||||
</div>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
StartIcon="@Icons.Material.Outlined.OpenInNew"
|
||||
OnClick="@(() => Nav.NavigateTo($"/events/{Uri.EscapeDataString(_detail.Item.EventId.Value)}"))"
|
||||
Class="m-detail-header__export"
|
||||
data-test="link-back-to-event">
|
||||
@L["Anomaly.Detail.LinkBackToEvent"]
|
||||
</MudButton>
|
||||
</aside>
|
||||
</header>
|
||||
|
||||
<hr class="m-rule" />
|
||||
|
||||
<article class="m-card m-card--anomaly m-rise m-rise-2">
|
||||
<span class="m-kicker" style="color: var(--m-c-anomaly); border-color: var(--m-c-anomaly);">
|
||||
@L["Anomaly.Detail.EvidenceTitle"]
|
||||
</span>
|
||||
<div style="margin-top: var(--m-space-4);">
|
||||
<AnomalyEvidence Pre="_detail.Pre"
|
||||
Post="_detail.Post"
|
||||
SuspensionGapSeconds="_detail.Item.SuspensionGapSeconds"
|
||||
IsTwoWay="_detail.Item.IsTwoWay" />
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
[Parameter] public Guid Id { get; set; }
|
||||
|
||||
private AnomalyDetailVm? _detail;
|
||||
private bool _loading = true;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
_detail = await Anomalies.GetByIdAsync(Id, CancellationToken.None);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_detail = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string KindLabel(AnomalyKind kind) => kind switch
|
||||
{
|
||||
AnomalyKind.SuspensionFlip => L["Anomaly.Kind.SuspensionFlip"],
|
||||
_ => kind.ToString(),
|
||||
};
|
||||
|
||||
private static string FormatGap(int seconds)
|
||||
{
|
||||
if (seconds <= 0) return "—";
|
||||
var ts = TimeSpan.FromSeconds(seconds);
|
||||
if (ts.TotalSeconds < 60) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}s", (int)ts.TotalSeconds);
|
||||
if (ts.TotalMinutes < 60) return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}m {1:00}s", (int)ts.TotalMinutes, ts.Seconds);
|
||||
return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}h {1:00}m", (int)ts.TotalHours, ts.Minutes);
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,9 @@
|
||||
<Field Label="@L["Settings.Workers.ResultsPollIntervalSeconds"]">
|
||||
<MudNumericField T="int" @bind-Value="_workers.ResultsPollIntervalSeconds" Min="60" Max="7200" Variant="Variant.Outlined" />
|
||||
</Field>
|
||||
<Field Label="@L["Settings.Workers.AnomalyDetectionEnabled"]" Hint="@L["Settings.Workers.AnomalyDetectionEnabled.Hint"]">
|
||||
<MudSwitch T="bool" @bind-Value="_workers.AnomalyDetectionEnabled" Color="Color.Primary" />
|
||||
</Field>
|
||||
|
||||
<SectionFooter OnSave="@(() => SaveSectionAsync(WorkerOptions.SectionName, _workers))" />
|
||||
</div>
|
||||
@@ -197,6 +200,7 @@
|
||||
LivePollIntervalSeconds = WorkerOpts.CurrentValue.LivePollIntervalSeconds,
|
||||
ResultsPollerEnabled = WorkerOpts.CurrentValue.ResultsPollerEnabled,
|
||||
ResultsPollIntervalSeconds = WorkerOpts.CurrentValue.ResultsPollIntervalSeconds,
|
||||
AnomalyDetectionEnabled = WorkerOpts.CurrentValue.AnomalyDetectionEnabled,
|
||||
};
|
||||
|
||||
_storage = new StorageOptions
|
||||
|
||||
Reference in New Issue
Block a user