diff --git a/.gitignore b/.gitignore index 9611e75..fde9d20 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ Atomizer.sln.DotSettings.user example.db* .omc/ .claude/ -.planning/ \ No newline at end of file +.planning/ +.sisyphus/ +src/Atomizer.Dashboard/frontend/node_modules/ +src/Atomizer.Dashboard/frontend/dist/ \ 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/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 new file mode 100644 index 0000000..b66f23b --- /dev/null +++ b/src/Atomizer.Dashboard/Atomizer.Dashboard.csproj @@ -0,0 +1,43 @@ + + + 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 + + + + <_Parameter1>Atomizer.Tests + + + + + + + + $(MSBuildProjectDirectory)/frontend + + + + + + + + + + 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/Storage/InMemoryDashboardStorage.cs b/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs new file mode 100644 index 0000000..43f1c98 --- /dev/null +++ b/src/Atomizer.Dashboard/Storage/InMemoryDashboardStorage.cs @@ -0,0 +1,80 @@ +using Atomizer.Storage; + +namespace Atomizer.Dashboard.Storage; + +internal sealed class InMemoryDashboardStorage : IAtomizerDashboardStorage +{ + private readonly InMemoryStorage _storage; + + public InMemoryDashboardStorage(InMemoryStorage storage) + { + _storage = storage; + } + + 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.Dashboard/frontend/.nvmrc b/src/Atomizer.Dashboard/frontend/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/src/Atomizer.Dashboard/frontend/.nvmrc @@ -0,0 +1 @@ +24 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, + }, +}); 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 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/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/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/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/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 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.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..45d474f --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/MySqlDashboardStorageTests.cs @@ -0,0 +1,31 @@ +using Atomizer.EntityFrameworkCore.Tests.Fixtures; +using Atomizer.EntityFrameworkCore.Tests.Storage; +using Atomizer.EntityFrameworkCore.Tests.TestSetup.MySql; +using AwesomeAssertions; + +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 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..40c5b65 --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/PostgresDashboardStorageTests.cs @@ -0,0 +1,31 @@ +using Atomizer.EntityFrameworkCore.Tests.Fixtures; +using Atomizer.EntityFrameworkCore.Tests.Storage; +using Atomizer.EntityFrameworkCore.Tests.TestSetup.Postgres; +using AwesomeAssertions; + +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 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..7050fa9 --- /dev/null +++ b/tests/Atomizer.EntityFrameworkCore.Tests/Dashboard/SqlServerDashboardStorageTests.cs @@ -0,0 +1,31 @@ +using Atomizer.EntityFrameworkCore.Tests.Fixtures; +using Atomizer.EntityFrameworkCore.Tests.Storage; +using Atomizer.EntityFrameworkCore.Tests.TestSetup.SqlServer; +using AwesomeAssertions; + +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 result = await CreateStorage().GetJobsAsync(new JobQuery { Skip = 0, Take = 10 }, CancellationToken.None); + + result.Should().NotBeNull(); + result.Items.Should().NotBeNull(); + } +} 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..664eb0d --- /dev/null +++ b/tests/Atomizer.Tests/Dashboard/InMemoryDashboardStorageTests.cs @@ -0,0 +1,248 @@ +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); + } + + 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 + ); + + await _inMemoryStorage.InsertAsync(job, CancellationToken.None); + + if (status == AtomizerJobStatus.Processing) + { + 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) + { + var leaseToken = new LeaseToken($"server-1:*:{job.QueueKey.Key}:*:{Guid.NewGuid()}"); + job.Lease(leaseToken, _now, TimeSpan.FromMinutes(5)); + job.Attempt(); + job.MarkAsCompleted(_now); + await _inMemoryStorage.UpdateJobsAsync([job], CancellationToken.None); + } + else if (status == AtomizerJobStatus.Failed) + { + 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); + } + + 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_ShouldReturnAllRegisteredServers() + { + 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"); + } +}