From 4c924e222a3d73f49a03fc94bd76e2f546b1f626 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 17:31:36 +0200 Subject: [PATCH 1/9] feat(dashboard): add Atomizer.Dashboard project skeleton Replaces netstandard2.0 stub with net6.0;net8.0;net10.0 targets, wires up ASP.NET Core FrameworkReference and Atomizer project reference, and adds folder skeleton (Abstractions, Authorization, Configuration, Contracts, DependencyInjection, Endpoints, Hubs, StaticFiles, Storage). Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +- Atomizer.sln | 7 +++ src/Atomizer.Dashboard/Abstractions/.gitkeep | 0 .../Atomizer.Dashboard.csproj | 18 ++++++++ src/Atomizer.Dashboard/Authorization/.gitkeep | 0 src/Atomizer.Dashboard/Configuration/.gitkeep | 0 src/Atomizer.Dashboard/Contracts/.gitkeep | 0 .../DependencyInjection/.gitkeep | 0 src/Atomizer.Dashboard/Endpoints/.gitkeep | 0 src/Atomizer.Dashboard/Hubs/.gitkeep | 0 src/Atomizer.Dashboard/StaticFiles/.gitkeep | 0 src/Atomizer.Dashboard/Storage/.gitkeep | 0 src/Atomizer.Dashboard/packages.lock.json | 44 +++++++++++++++++++ 13 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 src/Atomizer.Dashboard/Abstractions/.gitkeep create mode 100644 src/Atomizer.Dashboard/Atomizer.Dashboard.csproj create mode 100644 src/Atomizer.Dashboard/Authorization/.gitkeep create mode 100644 src/Atomizer.Dashboard/Configuration/.gitkeep create mode 100644 src/Atomizer.Dashboard/Contracts/.gitkeep create mode 100644 src/Atomizer.Dashboard/DependencyInjection/.gitkeep create mode 100644 src/Atomizer.Dashboard/Endpoints/.gitkeep create mode 100644 src/Atomizer.Dashboard/Hubs/.gitkeep create mode 100644 src/Atomizer.Dashboard/StaticFiles/.gitkeep create mode 100644 src/Atomizer.Dashboard/Storage/.gitkeep create mode 100644 src/Atomizer.Dashboard/packages.lock.json diff --git a/.gitignore b/.gitignore index 9611e75..1bd16a9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ Atomizer.sln.DotSettings.user example.db* .omc/ .claude/ -.planning/ \ No newline at end of file +.planning/ +.sisyphus/ \ No newline at end of file diff --git a/Atomizer.sln b/Atomizer.sln index 328e9da..c9bcc78 100644 --- a/Atomizer.sln +++ b/Atomizer.sln @@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atomizer.EntityFrameworkCor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atomizer.Tests.Utilities", "tests\Atomizer.Tests.Utilities\Atomizer.Tests.Utilities.csproj", "{57C7E426-4BD3-4984-9C8F-65F2F6E35852}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atomizer.Dashboard", "src\Atomizer.Dashboard\Atomizer.Dashboard.csproj", "{7284E6AE-8CD5-438E-826C-D6C8E2B58AB8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,6 +66,10 @@ Global {57C7E426-4BD3-4984-9C8F-65F2F6E35852}.Debug|Any CPU.Build.0 = Debug|Any CPU {57C7E426-4BD3-4984-9C8F-65F2F6E35852}.Release|Any CPU.ActiveCfg = Release|Any CPU {57C7E426-4BD3-4984-9C8F-65F2F6E35852}.Release|Any CPU.Build.0 = Release|Any CPU + {7284E6AE-8CD5-438E-826C-D6C8E2B58AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7284E6AE-8CD5-438E-826C-D6C8E2B58AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7284E6AE-8CD5-438E-826C-D6C8E2B58AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7284E6AE-8CD5-438E-826C-D6C8E2B58AB8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3FA58AFF-FFAB-4F99-BCFA-FDF7333C2615} = {57DC23B8-FC3C-41A3-AEB0-21C016F935F6} @@ -73,5 +79,6 @@ Global {5FF09171-46CB-4619-BCB9-48C596BB1BEC} = {7233FD90-018C-47BF-9001-EB1681DFE75B} {7654F745-0CFE-4802-9BE6-5AB4065FBA1F} = {7233FD90-018C-47BF-9001-EB1681DFE75B} {57C7E426-4BD3-4984-9C8F-65F2F6E35852} = {7233FD90-018C-47BF-9001-EB1681DFE75B} + {7284E6AE-8CD5-438E-826C-D6C8E2B58AB8} = {57DC23B8-FC3C-41A3-AEB0-21C016F935F6} EndGlobalSection EndGlobal diff --git a/src/Atomizer.Dashboard/Abstractions/.gitkeep b/src/Atomizer.Dashboard/Abstractions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj new file mode 100644 index 0000000..58927e7 --- /dev/null +++ b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj @@ -0,0 +1,18 @@ + + + net6.0;net8.0;net10.0 + Atomizer.Dashboard + Atomizer.Dashboard provides a real-time monitoring dashboard for Atomizer background jobs and schedules + jobs;queue;scheduler;dashboard;monitoring + enable + enable + 14 + true + true + true + + + + + + diff --git a/src/Atomizer.Dashboard/Authorization/.gitkeep b/src/Atomizer.Dashboard/Authorization/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/Configuration/.gitkeep b/src/Atomizer.Dashboard/Configuration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/Contracts/.gitkeep b/src/Atomizer.Dashboard/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/DependencyInjection/.gitkeep b/src/Atomizer.Dashboard/DependencyInjection/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/Endpoints/.gitkeep b/src/Atomizer.Dashboard/Endpoints/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/Hubs/.gitkeep b/src/Atomizer.Dashboard/Hubs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/StaticFiles/.gitkeep b/src/Atomizer.Dashboard/StaticFiles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/Storage/.gitkeep b/src/Atomizer.Dashboard/Storage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/Atomizer.Dashboard/packages.lock.json b/src/Atomizer.Dashboard/packages.lock.json new file mode 100644 index 0000000..6bd3671 --- /dev/null +++ b/src/Atomizer.Dashboard/packages.lock.json @@ -0,0 +1,44 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "Cronos": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "Nkve+Yi0+ol3ntSBRHeW0ka9cJQ8vndSUvvkXqF9LVqwpoqWueyxu/HS275r98RDOmF7/mzRnZW5XXUBXob+TA==" + }, + "atomizer": { + "type": "Project", + "dependencies": { + "Cronos": "[0.13.0, )" + } + } + }, + "net6.0": { + "Cronos": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "Nkve+Yi0+ol3ntSBRHeW0ka9cJQ8vndSUvvkXqF9LVqwpoqWueyxu/HS275r98RDOmF7/mzRnZW5XXUBXob+TA==" + }, + "atomizer": { + "type": "Project", + "dependencies": { + "Cronos": "[0.13.0, )" + } + } + }, + "net8.0": { + "Cronos": { + "type": "Transitive", + "resolved": "0.13.0", + "contentHash": "Nkve+Yi0+ol3ntSBRHeW0ka9cJQ8vndSUvvkXqF9LVqwpoqWueyxu/HS275r98RDOmF7/mzRnZW5XXUBXob+TA==" + }, + "atomizer": { + "type": "Project", + "dependencies": { + "Cronos": "[0.13.0, )" + } + } + } + } +} \ No newline at end of file From 5cd0d9389221f295e5b957eb7ae10ebf62e31431 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 17:40:14 +0200 Subject: [PATCH 2/9] feat(core): add IAtomizerEventSink, JobQuery, and PagedResult abstractions Adds a null-object event sink registered via TryAddSingleton so dashboard consumers can override it, plus JobQuery and PagedResult for paginated job querying. Includes netstandard2.0 IsExternalInit polyfill for init property support. Co-Authored-By: Claude Sonnet 4.6 --- .../Abstractions/IAtomizerEventSink.cs | 15 +++++++ src/Atomizer/Abstractions/JobQuery.cs | 42 +++++++++++++++++++ .../Abstractions/NullAtomizerEventSink.cs | 6 +++ src/Atomizer/Abstractions/PagedResult.cs | 28 +++++++++++++ .../ServiceCollectionExtensions.cs | 1 + src/Atomizer/Polyfills/IsExternalInit.cs | 6 +++ 6 files changed, 98 insertions(+) create mode 100644 src/Atomizer/Abstractions/IAtomizerEventSink.cs create mode 100644 src/Atomizer/Abstractions/JobQuery.cs create mode 100644 src/Atomizer/Abstractions/NullAtomizerEventSink.cs create mode 100644 src/Atomizer/Abstractions/PagedResult.cs create mode 100644 src/Atomizer/Polyfills/IsExternalInit.cs diff --git a/src/Atomizer/Abstractions/IAtomizerEventSink.cs b/src/Atomizer/Abstractions/IAtomizerEventSink.cs new file mode 100644 index 0000000..477b377 --- /dev/null +++ b/src/Atomizer/Abstractions/IAtomizerEventSink.cs @@ -0,0 +1,15 @@ +namespace Atomizer; + +/// +/// Receives notifications when a job's state changes. +/// Register a custom implementation to react to job lifecycle events. +/// The default implementation is a no-op. +/// +public interface IAtomizerEventSink +{ + /// + /// Called after a job transitions to a new state. + /// Implementations must be fast and non-blocking; exceptions are caught and logged. + /// + Task OnJobStateChangedAsync(AtomizerJob job, CancellationToken cancellationToken); +} diff --git a/src/Atomizer/Abstractions/JobQuery.cs b/src/Atomizer/Abstractions/JobQuery.cs new file mode 100644 index 0000000..e3c03d5 --- /dev/null +++ b/src/Atomizer/Abstractions/JobQuery.cs @@ -0,0 +1,42 @@ +namespace Atomizer; + +/// +/// Defines filter and pagination parameters for querying jobs. +/// +public sealed class JobQuery +{ + /// + /// Filter by one or more job statuses. Null returns all statuses. + /// + public IReadOnlyCollection? Statuses { get; init; } + + /// + /// Filter by queue key. Null returns jobs from all queues. + /// + public QueueKey? QueueKey { get; init; } + + /// + /// Filter by payload type name substring (case-insensitive). Null returns all types. + /// + public string? PayloadTypeName { get; init; } + + /// + /// Return only jobs created at or after this UTC timestamp. + /// + public DateTimeOffset? CreatedFromUtc { get; init; } + + /// + /// Return only jobs created at or before this UTC timestamp. + /// + public DateTimeOffset? CreatedToUtc { get; init; } + + /// + /// Number of records to skip for pagination. Defaults to 0. + /// + public int Skip { get; init; } = 0; + + /// + /// Maximum number of records to return. Defaults to 50. Hard ceiling of 500. + /// + public int Take { get; init; } = 50; +} diff --git a/src/Atomizer/Abstractions/NullAtomizerEventSink.cs b/src/Atomizer/Abstractions/NullAtomizerEventSink.cs new file mode 100644 index 0000000..8994e0c --- /dev/null +++ b/src/Atomizer/Abstractions/NullAtomizerEventSink.cs @@ -0,0 +1,6 @@ +namespace Atomizer; + +internal sealed class NullAtomizerEventSink : IAtomizerEventSink +{ + public Task OnJobStateChangedAsync(AtomizerJob job, CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/Atomizer/Abstractions/PagedResult.cs b/src/Atomizer/Abstractions/PagedResult.cs new file mode 100644 index 0000000..f347502 --- /dev/null +++ b/src/Atomizer/Abstractions/PagedResult.cs @@ -0,0 +1,28 @@ +namespace Atomizer; + +/// +/// Represents a paginated result set. +/// +/// The element type. +public sealed class PagedResult +{ + /// + /// The items in the current page. + /// + public IReadOnlyList Items { get; init; } = Array.Empty(); + + /// + /// Total count of matching records across all pages. + /// + public int TotalCount { get; init; } + + /// + /// The skip offset used to produce this page. + /// + public int Skip { get; init; } + + /// + /// The take limit used to produce this page. + /// + public int Take { get; init; } +} diff --git a/src/Atomizer/Configuration/ServiceCollectionExtensions.cs b/src/Atomizer/Configuration/ServiceCollectionExtensions.cs index 9072694..f9ad374 100644 --- a/src/Atomizer/Configuration/ServiceCollectionExtensions.cs +++ b/src/Atomizer/Configuration/ServiceCollectionExtensions.cs @@ -47,6 +47,7 @@ public static IServiceCollection AddAtomizer( services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.TryAddSingleton(); services.Add( ServiceDescriptor.Describe( diff --git a/src/Atomizer/Polyfills/IsExternalInit.cs b/src/Atomizer/Polyfills/IsExternalInit.cs new file mode 100644 index 0000000..9f7e202 --- /dev/null +++ b/src/Atomizer/Polyfills/IsExternalInit.cs @@ -0,0 +1,6 @@ +#if NETSTANDARD2_0 +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} +#endif From 93899f55f51b36f57c5d5593456fb7f3241b47c9 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 17:57:55 +0200 Subject: [PATCH 3/9] feat(dashboard): add IAtomizerDashboardStorage and InMemoryDashboardStorage Defines the IAtomizerDashboardStorage interface and QueueStats type in the dashboard package, implements them via InMemoryDashboardStorage reading from internal InMemoryStorage snapshots, and adds 12 unit tests covering all query filters, pagination, ordering, and queue stats. Co-Authored-By: Claude Sonnet 4.6 --- .../Abstractions/IAtomizerDashboardStorage.cs | 28 ++ .../Abstractions/QueueStats.cs | 22 ++ .../Atomizer.Dashboard.csproj | 5 + .../Storage/InMemoryDashboardStorage.cs | 83 ++++++ src/Atomizer/Atomizer.csproj | 8 + src/Atomizer/Storage/InMemoryStorage.cs | 20 ++ tests/Atomizer.Tests/Atomizer.Tests.csproj | 1 + .../InMemoryDashboardStorageTests.cs | 252 ++++++++++++++++++ 8 files changed, 419 insertions(+) create mode 100644 src/Atomizer.Dashboard/Abstractions/IAtomizerDashboardStorage.cs create mode 100644 src/Atomizer.Dashboard/Abstractions/QueueStats.cs create mode 100644 src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs create mode 100644 tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs diff --git a/src/Atomizer.Dashboard/Abstractions/IAtomizerDashboardStorage.cs b/src/Atomizer.Dashboard/Abstractions/IAtomizerDashboardStorage.cs new file mode 100644 index 0000000..8b32d50 --- /dev/null +++ b/src/Atomizer.Dashboard/Abstractions/IAtomizerDashboardStorage.cs @@ -0,0 +1,28 @@ +namespace Atomizer.Dashboard; + +/// +/// Provides read-only query access to Atomizer job and schedule data for dashboard display. +/// Implement this interface to support the dashboard with a custom storage backend. +/// +public interface IAtomizerDashboardStorage +{ + /// + /// Returns a paginated, filtered list of jobs ordered by creation time descending. + /// + Task> GetJobsAsync(JobQuery query, CancellationToken cancellationToken); + + /// + /// Returns all registered recurring schedules. + /// + Task> GetSchedulesAsync(CancellationToken cancellationToken); + + /// + /// Returns all active servers (instances with a recent heartbeat). + /// + Task> GetActiveServersAsync(CancellationToken cancellationToken); + + /// + /// Returns job counts grouped by queue and status. + /// + Task> GetQueueStatsAsync(CancellationToken cancellationToken); +} diff --git a/src/Atomizer.Dashboard/Abstractions/QueueStats.cs b/src/Atomizer.Dashboard/Abstractions/QueueStats.cs new file mode 100644 index 0000000..bf4a6bd --- /dev/null +++ b/src/Atomizer.Dashboard/Abstractions/QueueStats.cs @@ -0,0 +1,22 @@ +namespace Atomizer.Dashboard; + +/// +/// Job counts for a single queue, broken down by status. +/// +public sealed class QueueStats +{ + /// The queue these counts apply to. + public QueueKey QueueKey { get; init; } = null!; + + /// Number of jobs with status Pending. + public int Pending { get; init; } + + /// Number of jobs with status Processing. + public int Processing { get; init; } + + /// Number of jobs with status Completed. + public int Completed { get; init; } + + /// Number of jobs with status Failed. + public int Failed { get; init; } +} diff --git a/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj index 58927e7..e3cbb24 100644 --- a/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj +++ b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj @@ -11,6 +11,11 @@ true true + + + <_Parameter1>Atomizer.Tests + + diff --git a/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs b/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs new file mode 100644 index 0000000..e560e9d --- /dev/null +++ b/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs @@ -0,0 +1,83 @@ +using Atomizer.Core; +using Atomizer.Storage; + +namespace Atomizer.Dashboard.Storage; + +internal sealed class InMemoryDashboardStorage : IAtomizerDashboardStorage +{ + private readonly InMemoryStorage _storage; + private readonly IAtomizerClock _clock; + + public InMemoryDashboardStorage(InMemoryStorage storage, IAtomizerClock clock) + { + _storage = storage; + _clock = clock; + } + + public Task> GetJobsAsync(JobQuery query, CancellationToken cancellationToken) + { + var take = Math.Min(query.Take, 500); + + IEnumerable jobs = _storage.GetAllJobs(); + + if (query.Statuses is { Count: > 0 }) + jobs = jobs.Where(j => query.Statuses.Contains(j.Status)); + + if (query.QueueKey is not null) + jobs = jobs.Where(j => j.QueueKey == query.QueueKey); + + if (query.PayloadTypeName is not null) + jobs = jobs.Where(j => + j.PayloadType != null + && j.PayloadType.Name.Contains(query.PayloadTypeName, StringComparison.OrdinalIgnoreCase) + ); + + if (query.CreatedFromUtc.HasValue) + jobs = jobs.Where(j => j.CreatedAt >= query.CreatedFromUtc.Value); + + if (query.CreatedToUtc.HasValue) + jobs = jobs.Where(j => j.CreatedAt <= query.CreatedToUtc.Value); + + var ordered = jobs.OrderByDescending(j => j.CreatedAt).ToList(); + var total = ordered.Count; + var items = ordered.Skip(query.Skip).Take(take).ToList(); + + return Task.FromResult( + new PagedResult + { + Items = items, + TotalCount = total, + Skip = query.Skip, + Take = take, + } + ); + } + + public Task> GetSchedulesAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_storage.GetAllSchedules()); + } + + public Task> GetActiveServersAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_storage.GetAllServers()); + } + + public Task> GetQueueStatsAsync(CancellationToken cancellationToken) + { + var groups = _storage + .GetAllJobs() + .GroupBy(j => j.QueueKey) + .Select(g => new QueueStats + { + QueueKey = g.Key, + Pending = g.Count(j => j.Status == AtomizerJobStatus.Pending), + Processing = g.Count(j => j.Status == AtomizerJobStatus.Processing), + Completed = g.Count(j => j.Status == AtomizerJobStatus.Completed), + Failed = g.Count(j => j.Status == AtomizerJobStatus.Failed), + }) + .ToList(); + + return Task.FromResult>(groups); + } +} diff --git a/src/Atomizer/Atomizer.csproj b/src/Atomizer/Atomizer.csproj index a5dcbbc..eccd699 100644 --- a/src/Atomizer/Atomizer.csproj +++ b/src/Atomizer/Atomizer.csproj @@ -15,6 +15,14 @@ Atomizer Atomizer provides background job queue and scheduler for .NET applications + + + <_Parameter1>Atomizer.Dashboard + + + <_Parameter1>Atomizer.Tests + + diff --git a/src/Atomizer/Storage/InMemoryStorage.cs b/src/Atomizer/Storage/InMemoryStorage.cs index a749bbe..a4e3096 100644 --- a/src/Atomizer/Storage/InMemoryStorage.cs +++ b/src/Atomizer/Storage/InMemoryStorage.cs @@ -417,6 +417,26 @@ CancellationToken cancellationToken cancellationToken ); + // ---- dashboard snapshot accessors ---- + + internal IReadOnlyList GetAllJobs() => _jobs.Values.ToList(); + + internal IReadOnlyList GetAllSchedules() + { + lock (_syncRoot) + { + return _schedules.Values.ToList(); + } + } + + internal IReadOnlyList GetAllServers() + { + lock (_syncRoot) + { + return _activeServers.Values.ToList(); + } + } + // ---- helpers ---- private void IndexIntoQueue(AtomizerJob job) diff --git a/tests/Atomizer.Tests/Atomizer.Tests.csproj b/tests/Atomizer.Tests/Atomizer.Tests.csproj index 06d1fea..914aba2 100644 --- a/tests/Atomizer.Tests/Atomizer.Tests.csproj +++ b/tests/Atomizer.Tests/Atomizer.Tests.csproj @@ -38,5 +38,6 @@ + diff --git a/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs b/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs new file mode 100644 index 0000000..a87e4ad --- /dev/null +++ b/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs @@ -0,0 +1,252 @@ +using Atomizer.Core; +using Atomizer.Dashboard; +using Atomizer.Dashboard.Storage; +using Atomizer.Storage; + +namespace Atomizer.Tests.Dashboard; + +public class InMemoryDashboardStorageTests +{ + private readonly IAtomizerClock _clock = Substitute.For(); + private readonly TestableLogger _storageLogger = Substitute.For>(); + private readonly InMemoryStorage _inMemoryStorage; + private readonly InMemoryDashboardStorage _sut; + private readonly DateTimeOffset _now = new DateTimeOffset(2024, 1, 15, 12, 0, 0, TimeSpan.Zero); + + public InMemoryDashboardStorageTests() + { + _clock.UtcNow.Returns(_now); + _inMemoryStorage = new InMemoryStorage( + new InMemoryJobStorageOptions { AmountOfJobsToRetainInMemory = 1000 }, + _clock, + _storageLogger + ); + _sut = new InMemoryDashboardStorage(_inMemoryStorage, _clock); + } + + private async Task InsertJobAsync( + QueueKey? queue = null, + AtomizerJobStatus status = AtomizerJobStatus.Pending, + Type? payloadType = null, + DateTimeOffset? createdAt = null + ) + { + var created = createdAt ?? _now; + var job = AtomizerJob.Create( + queue ?? QueueKey.Default, + payloadType ?? typeof(string), + "payload", + created, + created + ); + + var leaseToken = new LeaseToken($"server-1:*:{job.QueueKey.Key}:*:{Guid.NewGuid()}"); + + if (status == AtomizerJobStatus.Processing) + { + await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); + await _inMemoryStorage.UpdateJobsAsync([job], CancellationToken.None); + } + else if (status == AtomizerJobStatus.Completed) + { + await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); + job.Attempt(); + job.MarkAsCompleted(_now); + await _inMemoryStorage.UpdateJobsAsync([job], CancellationToken.None); + } + else if (status == AtomizerJobStatus.Failed) + { + await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); + job.Attempt(); + job.MarkAsFailed(_now); + await _inMemoryStorage.UpdateJobsAsync([job], CancellationToken.None); + } + else + { + await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + } + + return job; + } + + [Fact] + public async Task GetJobsAsync_WhenStoreIsEmpty_ShouldReturnEmptyResult() + { + var result = await _sut.GetJobsAsync(new JobQuery(), CancellationToken.None); + + result.Items.Should().BeEmpty(); + result.TotalCount.Should().Be(0); + } + + [Fact] + public async Task GetJobsAsync_WhenStatusFilterApplied_ShouldReturnOnlyMatchingJobs() + { + await InsertJobAsync(status: AtomizerJobStatus.Pending); + await InsertJobAsync(status: AtomizerJobStatus.Processing); + + var result = await _sut.GetJobsAsync( + new JobQuery { Statuses = [AtomizerJobStatus.Pending] }, + CancellationToken.None + ); + + result.Items.Should().AllSatisfy(j => j.Status.Should().Be(AtomizerJobStatus.Pending)); + result.TotalCount.Should().Be(1); + } + + [Fact] + public async Task GetJobsAsync_WhenQueueFilterApplied_ShouldReturnOnlyMatchingJobs() + { + var otherQueue = new QueueKey("other"); + await InsertJobAsync(queue: QueueKey.Default); + await InsertJobAsync(queue: otherQueue); + + var result = await _sut.GetJobsAsync(new JobQuery { QueueKey = QueueKey.Default }, CancellationToken.None); + + result.Items.Should().AllSatisfy(j => j.QueueKey.Should().Be(QueueKey.Default)); + result.TotalCount.Should().Be(1); + } + + [Fact] + public async Task GetJobsAsync_WhenPayloadTypeNameFilterApplied_ShouldMatchCaseInsensitiveSubstring() + { + await InsertJobAsync(payloadType: typeof(string)); + await InsertJobAsync(payloadType: typeof(int)); + + var result = await _sut.GetJobsAsync(new JobQuery { PayloadTypeName = "STR" }, CancellationToken.None); + + result.Items.Should().HaveCount(1); + result.Items[0].PayloadType.Should().Be(typeof(string)); + } + + [Fact] + public async Task GetJobsAsync_WhenDateRangeFilterApplied_ShouldReturnJobsWithinRange() + { + var early = _now.AddHours(-2); + var late = _now.AddHours(2); + + await InsertJobAsync(createdAt: early); + await InsertJobAsync(createdAt: _now); + await InsertJobAsync(createdAt: late); + + var result = await _sut.GetJobsAsync( + new JobQuery { CreatedFromUtc = _now.AddHours(-1), CreatedToUtc = _now.AddHours(1) }, + CancellationToken.None + ); + + result.TotalCount.Should().Be(1); + result.Items[0].CreatedAt.Should().Be(_now); + } + + [Fact] + public async Task GetJobsAsync_WhenMultipleFiltersApplied_ShouldApplyAllFilters() + { + var otherQueue = new QueueKey("other"); + await InsertJobAsync(queue: QueueKey.Default, status: AtomizerJobStatus.Pending); + await InsertJobAsync(queue: otherQueue, status: AtomizerJobStatus.Pending); + await InsertJobAsync(queue: QueueKey.Default, status: AtomizerJobStatus.Processing); + + var result = await _sut.GetJobsAsync( + new JobQuery { QueueKey = QueueKey.Default, Statuses = [AtomizerJobStatus.Pending] }, + CancellationToken.None + ); + + result.TotalCount.Should().Be(1); + result.Items[0].QueueKey.Should().Be(QueueKey.Default); + result.Items[0].Status.Should().Be(AtomizerJobStatus.Pending); + } + + [Fact] + public async Task GetJobsAsync_WhenSkipExceedsTotalCount_ShouldReturnEmptyItems() + { + await InsertJobAsync(); + await InsertJobAsync(); + + var result = await _sut.GetJobsAsync(new JobQuery { Skip = 10 }, CancellationToken.None); + + result.Items.Should().BeEmpty(); + result.TotalCount.Should().Be(2); + } + + [Fact] + public async Task GetJobsAsync_WhenTakeExceedsHardCeiling_ShouldCapAt500() + { + var result = await _sut.GetJobsAsync(new JobQuery { Take = 1000 }, CancellationToken.None); + + result.Take.Should().Be(500); + } + + [Fact] + public async Task GetJobsAsync_ShouldReturnJobsOrderedByCreatedAtDescending() + { + var job1 = await InsertJobAsync(createdAt: _now.AddSeconds(-2)); + var job2 = await InsertJobAsync(createdAt: _now.AddSeconds(-1)); + var job3 = await InsertJobAsync(createdAt: _now); + + var result = await _sut.GetJobsAsync(new JobQuery(), CancellationToken.None); + + result.Items[0].Id.Should().Be(job3.Id); + result.Items[1].Id.Should().Be(job2.Id); + result.Items[2].Id.Should().Be(job1.Id); + } + + [Fact] + public async Task GetQueueStatsAsync_ShouldGroupByQueueAndCountByStatus() + { + var queue2 = new QueueKey("queue2"); + + await InsertJobAsync(queue: QueueKey.Default, status: AtomizerJobStatus.Pending); + await InsertJobAsync(queue: QueueKey.Default, status: AtomizerJobStatus.Pending); + await InsertJobAsync(queue: QueueKey.Default, status: AtomizerJobStatus.Processing); + await InsertJobAsync(queue: queue2, status: AtomizerJobStatus.Completed); + + var result = await _sut.GetQueueStatsAsync(CancellationToken.None); + + var defaultStats = result.Single(s => s.QueueKey == QueueKey.Default); + defaultStats.Pending.Should().Be(2); + defaultStats.Processing.Should().Be(1); + defaultStats.Completed.Should().Be(0); + defaultStats.Failed.Should().Be(0); + + var queue2Stats = result.Single(s => s.QueueKey == queue2); + queue2Stats.Completed.Should().Be(1); + } + + [Fact] + public async Task GetSchedulesAsync_ShouldReturnAllSchedules() + { + var schedule = AtomizerSchedule.Create( + new JobKey("test-job"), + QueueKey.Default, + typeof(string), + "{}", + Schedule.Default, + TimeZoneInfo.Utc, + _now + ); + await _inMemoryStorage.UpsertScheduleAsync(schedule, CancellationToken.None); + + var result = await _sut.GetSchedulesAsync(CancellationToken.None); + + result.Should().HaveCount(1); + result[0].JobKey.Should().Be(schedule.JobKey); + } + + [Fact] + public async Task GetActiveServersAsync_ShouldReturnOnlyServersWithRecentHeartbeat() + { + var server1 = new AtomizerActiveServer { InstanceId = "server-1", LastHeartbeatAt = _now }; + var server2 = new AtomizerActiveServer { InstanceId = "server-2", LastHeartbeatAt = _now.AddMinutes(-1) }; + + await _inMemoryStorage.UpsertHeartbeatAsync(server1, CancellationToken.None); + await _inMemoryStorage.UpsertHeartbeatAsync(server2, CancellationToken.None); + + var result = await _sut.GetActiveServersAsync(CancellationToken.None); + + result.Should().HaveCount(2); + result.Should().Contain(s => s.InstanceId == "server-1"); + result.Should().Contain(s => s.InstanceId == "server-2"); + } +} From 56c5f89a19663e1f04e52dafab0ee36b3f0d9569 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 18:00:50 +0200 Subject: [PATCH 4/9] refactor(dashboard): remove unused clock dep and clean up test helpers Drop the unused IAtomizerClock field from InMemoryDashboardStorage (snapshot methods require no clock), move lease token creation into the branches that need it, and rename the misleading active-server test method to accurately reflect its assertion. Co-Authored-By: Claude Sonnet 4.6 --- .../Storage/InMemoryDashboardStorage.cs | 5 +---- .../Dashboard/InMemoryDashboardStorageTests.cs | 16 ++++++---------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs b/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs index e560e9d..43f1c98 100644 --- a/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs +++ b/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs @@ -1,4 +1,3 @@ -using Atomizer.Core; using Atomizer.Storage; namespace Atomizer.Dashboard.Storage; @@ -6,12 +5,10 @@ namespace Atomizer.Dashboard.Storage; internal sealed class InMemoryDashboardStorage : IAtomizerDashboardStorage { private readonly InMemoryStorage _storage; - private readonly IAtomizerClock _clock; - public InMemoryDashboardStorage(InMemoryStorage storage, IAtomizerClock clock) + public InMemoryDashboardStorage(InMemoryStorage storage) { _storage = storage; - _clock = clock; } public Task> GetJobsAsync(JobQuery query, CancellationToken cancellationToken) diff --git a/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs b/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs index a87e4ad..664eb0d 100644 --- a/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs +++ b/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs @@ -21,7 +21,7 @@ public InMemoryDashboardStorageTests() _clock, _storageLogger ); - _sut = new InMemoryDashboardStorage(_inMemoryStorage, _clock); + _sut = new InMemoryDashboardStorage(_inMemoryStorage); } private async Task InsertJobAsync( @@ -40,17 +40,17 @@ private async Task InsertJobAsync( created ); - var leaseToken = new LeaseToken($"server-1:*:{job.QueueKey.Key}:*:{Guid.NewGuid()}"); + await _inMemoryStorage.InsertAsync(job, CancellationToken.None); if (status == AtomizerJobStatus.Processing) { - await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + var leaseToken = new LeaseToken($"server-1:*:{job.QueueKey.Key}:*:{Guid.NewGuid()}"); job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); await _inMemoryStorage.UpdateJobsAsync([job], CancellationToken.None); } else if (status == AtomizerJobStatus.Completed) { - await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + var leaseToken = new LeaseToken($"server-1:*:{job.QueueKey.Key}:*:{Guid.NewGuid()}"); job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); job.Attempt(); job.MarkAsCompleted(_now); @@ -58,16 +58,12 @@ private async Task InsertJobAsync( } else if (status == AtomizerJobStatus.Failed) { - await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + var leaseToken = new LeaseToken($"server-1:*:{job.QueueKey.Key}:*:{Guid.NewGuid()}"); job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); job.Attempt(); job.MarkAsFailed(_now); await _inMemoryStorage.UpdateJobsAsync([job], CancellationToken.None); } - else - { - await _inMemoryStorage.InsertAsync(job, CancellationToken.None); - } return job; } @@ -235,7 +231,7 @@ public async Task GetSchedulesAsync_ShouldReturnAllSchedules() } [Fact] - public async Task GetActiveServersAsync_ShouldReturnOnlyServersWithRecentHeartbeat() + public async Task GetActiveServersAsync_ShouldReturnAllRegisteredServers() { var server1 = new AtomizerActiveServer { InstanceId = "server-1", LastHeartbeatAt = _now }; var server2 = new AtomizerActiveServer { InstanceId = "server-2", LastHeartbeatAt = _now.AddMinutes(-1) }; From 12c7ca81ae5562cc99b76d1101c6a1c4efd03e6c Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 22:01:10 +0200 Subject: [PATCH 5/9] feat(dashboard): add EntityFrameworkCoreDashboardStorage and CreatedAt index Implements IAtomizerDashboardStorage for the EF Core backend using IDbContextFactory, AsNoTracking, and LINQ-only queries. Adds a CreatedAt index to AtomizerJobEntityConfiguration to support efficient ORDER BY CreatedAt DESC. Includes integration tests against PostgreSQL, MySQL, and SQL Server containers. Co-Authored-By: Claude Sonnet 4.6 --- .../Atomizer.EntityFrameworkCore.csproj | 1 + .../AtomizerJobEntityConfiguration.cs | 1 + .../EntityFrameworkCoreDashboardStorage.cs | 112 ++++++++++ .../packages.lock.json | 18 ++ .../Atomizer.EntityFrameworkCore.Tests.csproj | 1 + ...ntityFrameworkCoreDashboardStorageTests.cs | 209 ++++++++++++++++++ .../Dashboard/MySqlDashboardStorageTests.cs | 35 +++ .../PostgresDashboardStorageTests.cs | 35 +++ .../SqlServerDashboardStorageTests.cs | 35 +++ 9 files changed, 447 insertions(+) create mode 100644 src/Atomizer.EntityFrameworkCore/Dashboard/EntityFrameworkCoreDashboardStorage.cs create mode 100644 tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/EntityFrameworkCoreDashboardStorageTests.cs create mode 100644 tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs create mode 100644 tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs create mode 100644 tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs diff --git a/src/Atomizer.EntityFrameworkCore/Atomizer.EntityFrameworkCore.csproj b/src/Atomizer.EntityFrameworkCore/Atomizer.EntityFrameworkCore.csproj index 8e07bac..8c4689f 100644 --- a/src/Atomizer.EntityFrameworkCore/Atomizer.EntityFrameworkCore.csproj +++ b/src/Atomizer.EntityFrameworkCore/Atomizer.EntityFrameworkCore.csproj @@ -22,5 +22,6 @@ + diff --git a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs index 8553293..d134fa6 100644 --- a/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs +++ b/src/Atomizer.EntityFrameworkCore/Configurations/AtomizerJobEntityConfiguration.cs @@ -93,5 +93,6 @@ public void Configure(EntityTypeBuilder builder) .HasIndex(job => new { job.Status, job.LeaseToken }) .HasDatabaseName("IX_AtomizerJobs_Status_LeaseToken"); builder.HasIndex(job => job.IdempotencyKey).HasDatabaseName("IX_AtomizerJobs_IdempotencyKey"); + builder.HasIndex(job => job.CreatedAt).HasDatabaseName("IX_AtomizerJobs_CreatedAt"); } } diff --git a/src/Atomizer.EntityFrameworkCore/Dashboard/EntityFrameworkCoreDashboardStorage.cs b/src/Atomizer.EntityFrameworkCore/Dashboard/EntityFrameworkCoreDashboardStorage.cs new file mode 100644 index 0000000..dd424fd --- /dev/null +++ b/src/Atomizer.EntityFrameworkCore/Dashboard/EntityFrameworkCoreDashboardStorage.cs @@ -0,0 +1,112 @@ +using Atomizer.Core; +using Atomizer.Dashboard; +using Atomizer.EntityFrameworkCore.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Atomizer.EntityFrameworkCore.Dashboard; + +internal sealed class EntityFrameworkCoreDashboardStorage : IAtomizerDashboardStorage + where TContext : DbContext +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly IAtomizerClock _clock; + + public EntityFrameworkCoreDashboardStorage(IDbContextFactory dbContextFactory, IAtomizerClock clock) + { + _dbContextFactory = dbContextFactory; + _clock = clock; + } + + public async Task> GetJobsAsync(JobQuery query, CancellationToken cancellationToken) + { + var take = Math.Min(query.Take, 500); + + await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + IQueryable q = db.Set().AsNoTracking(); + + if (query.Statuses is { Count: > 0 }) + { + var statuses = query.Statuses.Select(s => (AtomizerEntityJobStatus)(int)s).ToList(); + q = q.Where(e => statuses.Contains(e.Status)); + } + + if (query.QueueKey is not null) + q = q.Where(e => e.QueueKey == query.QueueKey.ToString()); + + if (query.PayloadTypeName is not null) + q = q.Where(e => e.PayloadType.Contains(query.PayloadTypeName)); + + if (query.CreatedFromUtc.HasValue) + q = q.Where(e => e.CreatedAt >= query.CreatedFromUtc.Value); + + if (query.CreatedToUtc.HasValue) + q = q.Where(e => e.CreatedAt <= query.CreatedToUtc.Value); + + q = q.OrderByDescending(e => e.CreatedAt); + + var total = await q.CountAsync(cancellationToken); + + var entities = await q.Skip(query.Skip).Take(take).Include(e => e.Errors).ToListAsync(cancellationToken); + + return new PagedResult + { + Items = entities.Select(e => e.ToAtomizerJob()).ToList(), + TotalCount = total, + Skip = query.Skip, + Take = take, + }; + } + + public async Task> GetSchedulesAsync(CancellationToken cancellationToken) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var entities = await db.Set().AsNoTracking().ToListAsync(cancellationToken); + + return entities.Select(e => e.ToAtomizerSchedule()).ToList(); + } + + public async Task> GetActiveServersAsync(CancellationToken cancellationToken) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var cutoff = _clock.UtcNow.AddMinutes(-5); + + var entities = await db.Set() + .AsNoTracking() + .Where(e => e.LastHeartbeatAt >= cutoff) + .ToListAsync(cancellationToken); + + return entities.Select(e => e.ToAtomizerActiveServer()).ToList(); + } + + public async Task> GetQueueStatsAsync(CancellationToken cancellationToken) + { + await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var stats = await db.Set() + .AsNoTracking() + .GroupBy(e => e.QueueKey) + .Select(g => new + { + QueueKey = g.Key, + Pending = g.Count(e => e.Status == AtomizerEntityJobStatus.Pending), + Processing = g.Count(e => e.Status == AtomizerEntityJobStatus.Processing), + Completed = g.Count(e => e.Status == AtomizerEntityJobStatus.Completed), + Failed = g.Count(e => e.Status == AtomizerEntityJobStatus.Failed), + }) + .ToListAsync(cancellationToken); + + return stats + .Select(s => new QueueStats + { + QueueKey = new QueueKey(s.QueueKey), + Pending = s.Pending, + Processing = s.Processing, + Completed = s.Completed, + Failed = s.Failed, + }) + .ToList(); + } +} diff --git a/src/Atomizer.EntityFrameworkCore/packages.lock.json b/src/Atomizer.EntityFrameworkCore/packages.lock.json index 09c2989..3df709f 100644 --- a/src/Atomizer.EntityFrameworkCore/packages.lock.json +++ b/src/Atomizer.EntityFrameworkCore/packages.lock.json @@ -137,6 +137,12 @@ "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )" } + }, + "atomizer.dashboard": { + "type": "Project", + "dependencies": { + "Atomizer": "[1.0.0, )" + } } }, "net6.0": { @@ -275,6 +281,12 @@ "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )" } + }, + "atomizer.dashboard": { + "type": "Project", + "dependencies": { + "Atomizer": "[1.0.0, )" + } } }, "net8.0": { @@ -413,6 +425,12 @@ "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, )", "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )" } + }, + "atomizer.dashboard": { + "type": "Project", + "dependencies": { + "Atomizer": "[1.0.0, )" + } } } } diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Atomizer.EntityFrameworkCore.Tests.csproj b/tests/Atomizer.EntityFrameworkCore.Tests/Atomizer.EntityFrameworkCore.Tests.csproj index e114e14..4ee28d8 100644 --- a/tests/Atomizer.EntityFrameworkCore.Tests/Atomizer.EntityFrameworkCore.Tests.csproj +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Atomizer.EntityFrameworkCore.Tests.csproj @@ -48,6 +48,7 @@ + diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/EntityFrameworkCoreDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/EntityFrameworkCoreDashboardStorageTests.cs new file mode 100644 index 0000000..bb845db --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/EntityFrameworkCoreDashboardStorageTests.cs @@ -0,0 +1,209 @@ +using Atomizer.Core; +using Atomizer.Dashboard; +using Atomizer.EntityFrameworkCore.Dashboard; +using Atomizer.EntityFrameworkCore.Entities; +using Atomizer.EntityFrameworkCore.Tests.TestSetup; +using AwesomeAssertions; +using Microsoft.EntityFrameworkCore; +using NSubstitute; + +namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; + +public abstract class EntityFrameworkCoreDashboardStorageTests : IAsyncLifetime + where TDbContext : TestDbContext +{ + protected readonly IAtomizerClock Clock = Substitute.For(); + protected abstract TDbContext CreateDbContext(); + + protected IAtomizerDashboardStorage CreateStorage() + { + var factory = new DelegatingDbContextFactory(CreateDbContext); + return new EntityFrameworkCoreDashboardStorage(factory, Clock); + } + + public abstract ValueTask InitializeAsync(); + + public abstract ValueTask DisposeAsync(); + + [Fact] + public async Task GetJobsAsync_WhenJobsExist_ShouldReturnPagedResults() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + await using var db = CreateDbContext(); + db.Set() + .AddRange( + AtomizerJob + .Create(QueueKey.Default, typeof(object), "{}", now.AddMinutes(-3), now.AddMinutes(-3)) + .ToEntity(), + AtomizerJob + .Create(QueueKey.Default, typeof(object), "{}", now.AddMinutes(-2), now.AddMinutes(-2)) + .ToEntity(), + AtomizerJob + .Create(QueueKey.Default, typeof(object), "{}", now.AddMinutes(-1), now.AddMinutes(-1)) + .ToEntity() + ); + await db.SaveChangesAsync(); + + var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 2 }, CancellationToken.None); + + result.TotalCount.Should().Be(3); + result.Items.Count.Should().Be(2); + result.Take.Should().Be(2); + result.Items[0].CreatedAt.Should().Be(now.AddMinutes(-1)); + } + + [Fact] + public async Task GetJobsAsync_WhenFilterByStatus_ShouldReturnMatchingJobs() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + await using var db = CreateDbContext(); + db.Set() + .AddRange( + AtomizerJob.Create(QueueKey.Default, typeof(object), "{}", now, now).ToEntity(), + CreateFailedJobEntity(now) + ); + await db.SaveChangesAsync(); + + var result = await CreateStorage() + .GetJobsAsync( + new JobQuery + { + Skip = 0, + Take = 100, + Statuses = [AtomizerJobStatus.Failed], + }, + CancellationToken.None + ); + + result.TotalCount.Should().Be(1); + result.Items[0].Status.Should().Be(AtomizerJobStatus.Failed); + } + + [Fact] + public async Task GetJobsAsync_WhenTakeExceedsMax_ShouldCapAt500() + { + Clock.UtcNow.Returns(DateTimeOffset.UtcNow); + + var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 9999 }, CancellationToken.None); + + result.Take.Should().Be(500); + } + + [Fact] + public async Task GetSchedulesAsync_WhenSchedulesExist_ShouldReturnAll() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + var schedule = AtomizerSchedule.Create( + new JobKey("test-job"), + QueueKey.Default, + typeof(object), + "{}", + Schedule.Cron("0 * * * *"), + TimeZoneInfo.Utc, + now + ); + + await using var db = CreateDbContext(); + db.Set().Add(schedule.ToEntity()); + await db.SaveChangesAsync(); + + var result = await CreateStorage().GetSchedulesAsync(CancellationToken.None); + + result.Count.Should().Be(1); + result[0].JobKey.ToString().Should().Be("test-job"); + } + + [Fact] + public async Task GetActiveServersAsync_WhenServerHasRecentHeartbeat_ShouldReturnIt() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + await using var db = CreateDbContext(); + db.Set() + .Add(new AtomizerActiveServerEntity { InstanceId = "server-1", LastHeartbeatAt = now.AddMinutes(-1) }); + await db.SaveChangesAsync(); + + var result = await CreateStorage().GetActiveServersAsync(CancellationToken.None); + + result.Count.Should().Be(1); + result[0].InstanceId.Should().Be("server-1"); + } + + [Fact] + public async Task GetActiveServersAsync_WhenServerHeartbeatIsStale_ShouldExcludeIt() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + await using var db = CreateDbContext(); + db.Set() + .Add(new AtomizerActiveServerEntity { InstanceId = "stale-server", LastHeartbeatAt = now.AddMinutes(-10) }); + await db.SaveChangesAsync(); + + var result = await CreateStorage().GetActiveServersAsync(CancellationToken.None); + + result.Count.Should().Be(0); + } + + [Fact] + public async Task GetQueueStatsAsync_ShouldReturnCorrectCountsPerQueue() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + var defaultQueue = QueueKey.Default; + var otherQueue = new QueueKey("other"); + + await using var db = CreateDbContext(); + db.Set() + .AddRange( + AtomizerJob.Create(defaultQueue, typeof(object), "{}", now, now).ToEntity(), + AtomizerJob.Create(defaultQueue, typeof(object), "{}", now, now).ToEntity(), + AtomizerJob.Create(otherQueue, typeof(object), "{}", now, now).ToEntity(), + CreateFailedJobEntity(now, defaultQueue) + ); + await db.SaveChangesAsync(); + + var result = await CreateStorage().GetQueueStatsAsync(CancellationToken.None); + + var defaultStats = result.Single(s => s.QueueKey == defaultQueue); + defaultStats.Pending.Should().Be(2); + defaultStats.Failed.Should().Be(1); + + var otherStats = result.Single(s => s.QueueKey == otherQueue); + otherStats.Pending.Should().Be(1); + } + + private static AtomizerJobEntity CreateFailedJobEntity(DateTimeOffset now, QueueKey? queueKey = null) => + new AtomizerJobEntity + { + Id = Guid.NewGuid(), + QueueKey = (queueKey ?? QueueKey.Default).ToString(), + PayloadType = typeof(object).AssemblyQualifiedName!, + Payload = "{}", + ScheduledAt = now, + Status = AtomizerEntityJobStatus.Failed, + Attempts = 1, + RetryIntervals = [], + CreatedAt = now, + UpdatedAt = now, + FailedAt = now, + }; +} + +internal sealed class DelegatingDbContextFactory : IDbContextFactory + where T : DbContext +{ + private readonly Func _factory; + + public DelegatingDbContextFactory(Func factory) => _factory = factory; + + public T CreateDbContext() => _factory(); +} diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs new file mode 100644 index 0000000..ef8c864 --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs @@ -0,0 +1,35 @@ +using Atomizer.EntityFrameworkCore.Tests.Fixtures; +using Atomizer.EntityFrameworkCore.Tests.Storage; +using Atomizer.EntityFrameworkCore.Tests.TestSetup.MySql; +using AwesomeAssertions; +using NSubstitute; + +namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; + +[Collection(nameof(MySqlDatabaseFixture))] +public sealed class MySqlDashboardStorageTests(MySqlDatabaseFixture fixture) + : EntityFrameworkCoreDashboardStorageTests +{ + protected override MySqlDbContext CreateDbContext() => fixture.CreateNewDbContext(); + + public override ValueTask InitializeAsync() => ValueTask.CompletedTask; + + public override async ValueTask DisposeAsync() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await using var db = CreateDbContext(); + await StorageTestCleanup.ClearAsync(db, cts.Token); + } + + [Fact] + public async Task GetJobsAsync_WhenUsingMySql_ShouldReturnPagedResults() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); + + result.Should().NotBeNull(); + result.Items.Should().NotBeNull(); + } +} diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs new file mode 100644 index 0000000..4d2c9a4 --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs @@ -0,0 +1,35 @@ +using Atomizer.EntityFrameworkCore.Tests.Fixtures; +using Atomizer.EntityFrameworkCore.Tests.Storage; +using Atomizer.EntityFrameworkCore.Tests.TestSetup.Postgres; +using AwesomeAssertions; +using NSubstitute; + +namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; + +[Collection(nameof(PostgreSqlDatabaseFixture))] +public sealed class PostgresDashboardStorageTests(PostgreSqlDatabaseFixture fixture) + : EntityFrameworkCoreDashboardStorageTests +{ + protected override PostgresDbContext CreateDbContext() => fixture.CreateNewDbContext(); + + public override ValueTask InitializeAsync() => ValueTask.CompletedTask; + + public override async ValueTask DisposeAsync() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await using var db = CreateDbContext(); + await StorageTestCleanup.ClearAsync(db, cts.Token); + } + + [Fact] + public async Task GetJobsAsync_WhenUsingPostgres_ShouldReturnPagedResults() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); + + result.Should().NotBeNull(); + result.Items.Should().NotBeNull(); + } +} diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs new file mode 100644 index 0000000..95644fd --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs @@ -0,0 +1,35 @@ +using Atomizer.EntityFrameworkCore.Tests.Fixtures; +using Atomizer.EntityFrameworkCore.Tests.Storage; +using Atomizer.EntityFrameworkCore.Tests.TestSetup.SqlServer; +using AwesomeAssertions; +using NSubstitute; + +namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; + +[Collection(nameof(SqlServerDatabaseFixture))] +public sealed class SqlServerDashboardStorageTests(SqlServerDatabaseFixture fixture) + : EntityFrameworkCoreDashboardStorageTests +{ + protected override SqlServerDbContext CreateDbContext() => fixture.CreateNewDbContext(); + + public override ValueTask InitializeAsync() => ValueTask.CompletedTask; + + public override async ValueTask DisposeAsync() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + await using var db = CreateDbContext(); + await StorageTestCleanup.ClearAsync(db, cts.Token); + } + + [Fact] + public async Task GetJobsAsync_WhenUsingMsSql_ShouldReturnPagedResults() + { + var now = DateTimeOffset.UtcNow; + Clock.UtcNow.Returns(now); + + var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); + + result.Should().NotBeNull(); + result.Items.Should().NotBeNull(); + } +} From 9fcba1a4746df0641704b85d74dc58002e984b81 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 22:04:34 +0200 Subject: [PATCH 6/9] refactor(dashboard): remove dead mock setup from provider smoke tests Co-Authored-By: Claude Sonnet 4.6 --- .../Dashboard/MySqlDashboardStorageTests.cs | 4 ---- .../Dashboard/PostgresDashboardStorageTests.cs | 4 ---- .../Dashboard/SqlServerDashboardStorageTests.cs | 4 ---- 3 files changed, 12 deletions(-) diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs index ef8c864..45d474f 100644 --- a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs @@ -2,7 +2,6 @@ using Atomizer.EntityFrameworkCore.Tests.Storage; using Atomizer.EntityFrameworkCore.Tests.TestSetup.MySql; using AwesomeAssertions; -using NSubstitute; namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; @@ -24,9 +23,6 @@ public override async ValueTask DisposeAsync() [Fact] public async Task GetJobsAsync_WhenUsingMySql_ShouldReturnPagedResults() { - var now = DateTimeOffset.UtcNow; - Clock.UtcNow.Returns(now); - var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); result.Should().NotBeNull(); diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs index 4d2c9a4..40c5b65 100644 --- a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs @@ -2,7 +2,6 @@ using Atomizer.EntityFrameworkCore.Tests.Storage; using Atomizer.EntityFrameworkCore.Tests.TestSetup.Postgres; using AwesomeAssertions; -using NSubstitute; namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; @@ -24,9 +23,6 @@ public override async ValueTask DisposeAsync() [Fact] public async Task GetJobsAsync_WhenUsingPostgres_ShouldReturnPagedResults() { - var now = DateTimeOffset.UtcNow; - Clock.UtcNow.Returns(now); - var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); result.Should().NotBeNull(); diff --git a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs index 95644fd..7050fa9 100644 --- a/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs @@ -2,7 +2,6 @@ using Atomizer.EntityFrameworkCore.Tests.Storage; using Atomizer.EntityFrameworkCore.Tests.TestSetup.SqlServer; using AwesomeAssertions; -using NSubstitute; namespace Atomizer.EntityFrameworkCore.Tests.Dashboard; @@ -24,9 +23,6 @@ public override async ValueTask DisposeAsync() [Fact] public async Task GetJobsAsync_WhenUsingMsSql_ShouldReturnPagedResults() { - var now = DateTimeOffset.UtcNow; - Clock.UtcNow.Returns(now); - var result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); result.Should().NotBeNull(); From 158b127e2730d30854f6911a30dc8b15d75d3a3d Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 22:34:19 +0200 Subject: [PATCH 7/9] refactor(dashboard): remove unused NullAtomizerEventSink registration --- src/Atomizer/Abstractions/IAtomizerEventSink.cs | 15 --------------- .../Abstractions/NullAtomizerEventSink.cs | 6 ------ .../Configuration/ServiceCollectionExtensions.cs | 1 - 3 files changed, 22 deletions(-) delete mode 100644 src/Atomizer/Abstractions/IAtomizerEventSink.cs delete mode 100644 src/Atomizer/Abstractions/NullAtomizerEventSink.cs diff --git a/src/Atomizer/Abstractions/IAtomizerEventSink.cs b/src/Atomizer/Abstractions/IAtomizerEventSink.cs deleted file mode 100644 index 477b377..0000000 --- a/src/Atomizer/Abstractions/IAtomizerEventSink.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Atomizer; - -/// -/// Receives notifications when a job's state changes. -/// Register a custom implementation to react to job lifecycle events. -/// The default implementation is a no-op. -/// -public interface IAtomizerEventSink -{ - /// - /// Called after a job transitions to a new state. - /// Implementations must be fast and non-blocking; exceptions are caught and logged. - /// - Task OnJobStateChangedAsync(AtomizerJob job, CancellationToken cancellationToken); -} diff --git a/src/Atomizer/Abstractions/NullAtomizerEventSink.cs b/src/Atomizer/Abstractions/NullAtomizerEventSink.cs deleted file mode 100644 index 8994e0c..0000000 --- a/src/Atomizer/Abstractions/NullAtomizerEventSink.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Atomizer; - -internal sealed class NullAtomizerEventSink : IAtomizerEventSink -{ - public Task OnJobStateChangedAsync(AtomizerJob job, CancellationToken cancellationToken) => Task.CompletedTask; -} diff --git a/src/Atomizer/Configuration/ServiceCollectionExtensions.cs b/src/Atomizer/Configuration/ServiceCollectionExtensions.cs index f9ad374..9072694 100644 --- a/src/Atomizer/Configuration/ServiceCollectionExtensions.cs +++ b/src/Atomizer/Configuration/ServiceCollectionExtensions.cs @@ -47,7 +47,6 @@ public static IServiceCollection AddAtomizer( services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.TryAddSingleton(); services.Add( ServiceDescriptor.Describe( From 633263d4d95612a69f93d93e49689b9ed59f0e01 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 22:54:32 +0200 Subject: [PATCH 8/9] feat(dashboard): scaffold React+TypeScript+Vite+Tailwind frontend Bootstraps the SPA in frontend/, wires it into the MSBuild pipeline via a BuildSpa target that runs once at the outer build level (TargetFramework == '') to avoid parallel node_modules corruption across multi-TFM builds. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 +- .../Atomizer.Dashboard.csproj | 20 + src/Atomizer.Dashboard/frontend/.nvmrc | 1 + src/Atomizer.Dashboard/frontend/index.html | 19 + .../frontend/package-lock.json | 2468 +++++++++++++++++ src/Atomizer.Dashboard/frontend/package.json | 25 + src/Atomizer.Dashboard/frontend/src/index.css | 1 + src/Atomizer.Dashboard/frontend/src/main.tsx | 8 + src/Atomizer.Dashboard/frontend/tsconfig.json | 13 + .../frontend/vite.config.ts | 12 + 10 files changed, 2570 insertions(+), 1 deletion(-) create mode 100644 src/Atomizer.Dashboard/frontend/.nvmrc create mode 100644 src/Atomizer.Dashboard/frontend/index.html create mode 100644 src/Atomizer.Dashboard/frontend/package-lock.json create mode 100644 src/Atomizer.Dashboard/frontend/package.json create mode 100644 src/Atomizer.Dashboard/frontend/src/index.css create mode 100644 src/Atomizer.Dashboard/frontend/src/main.tsx create mode 100644 src/Atomizer.Dashboard/frontend/tsconfig.json create mode 100644 src/Atomizer.Dashboard/frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index 1bd16a9..fde9d20 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ example.db* .omc/ .claude/ .planning/ -.sisyphus/ \ No newline at end of file +.sisyphus/ +src/Atomizer.Dashboard/frontend/node_modules/ +src/Atomizer.Dashboard/frontend/dist/ \ No newline at end of file diff --git a/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj index e3cbb24..b66f23b 100644 --- a/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj +++ b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj @@ -20,4 +20,24 @@ + + $(MSBuildProjectDirectory)/frontend + + + + + + + + + diff --git a/src/Atomizer.Dashboard/frontend/.nvmrc b/src/Atomizer.Dashboard/frontend/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/src/Atomizer.Dashboard/frontend/index.html b/src/Atomizer.Dashboard/frontend/index.html new file mode 100644 index 0000000..0efc777 --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/index.html @@ -0,0 +1,19 @@ + + + + + + + {{TITLE}} + + +
+ + + diff --git a/src/Atomizer.Dashboard/frontend/package-lock.json b/src/Atomizer.Dashboard/frontend/package-lock.json new file mode 100644 index 0000000..3bbbcad --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/package-lock.json @@ -0,0 +1,2468 @@ +{ + "name": "atomizer-dashboard", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "atomizer-dashboard", + "version": "0.0.0", + "dependencies": { + "@tanstack/react-query": "^5.56.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.26.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.5.0", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.100.9", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tanstack/query-core/-/query-core-5.100.9.tgz", + "integrity": "sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.100.9", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@tanstack/react-query/-/react-query-5.100.9.tgz", + "integrity": "sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.100.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://fast.services.as24.tech/artifactory/api/npm/npm/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/src/Atomizer.Dashboard/frontend/package.json b/src/Atomizer.Dashboard/frontend/package.json new file mode 100644 index 0000000..99bb9de --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "atomizer-dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.26.0", + "@tanstack/react-query": "^5.56.0" + }, + "devDependencies": { + "typescript": "^5.5.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "vite": "^5.4.0", + "@vitejs/plugin-react": "^4.3.0", + "@tailwindcss/vite": "^4.0.0", + "tailwindcss": "^4.0.0" + } +} diff --git a/src/Atomizer.Dashboard/frontend/src/index.css b/src/Atomizer.Dashboard/frontend/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/Atomizer.Dashboard/frontend/src/main.tsx b/src/Atomizer.Dashboard/frontend/src/main.tsx new file mode 100644 index 0000000..cad2471 --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/src/main.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + +
Atomizer Dashboard — placeholder
+
+); diff --git a/src/Atomizer.Dashboard/frontend/tsconfig.json b/src/Atomizer.Dashboard/frontend/tsconfig.json new file mode 100644 index 0000000..cf3d531 --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/src/Atomizer.Dashboard/frontend/vite.config.ts b/src/Atomizer.Dashboard/frontend/vite.config.ts new file mode 100644 index 0000000..7fb4d95 --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + base: './', + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); From 3cd5533bac06591ea7a637366e26e84a425dbac7 Mon Sep 17 00:00:00 2001 From: mnbuhl Date: Sun, 10 May 2026 22:58:29 +0200 Subject: [PATCH 9/9] chore(dashboard): target Node 24 LTS in .nvmrc Co-Authored-By: Claude Sonnet 4.6 --- src/Atomizer.Dashboard/frontend/.nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Atomizer.Dashboard/frontend/.nvmrc b/src/Atomizer.Dashboard/frontend/.nvmrc index 209e3ef..a45fd52 100644 --- a/src/Atomizer.Dashboard/frontend/.nvmrc +++ b/src/Atomizer.Dashboard/frontend/.nvmrc @@ -1 +1 @@ -20 +24