From 7c1aa19a91e7744f5d2acefb5e1153440747f6b0 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 17 Feb 2026 13:26:03 -0600 Subject: [PATCH 01/29] Improves serialization and project config Addresses issues with serialization of various models, ensuring data integrity during round trips. Specifically: - Uses init accessors for SettingsDictionary to enable STJ to populate during deserialization. - Adds JToken handling in ObjectToInferredTypesConverter to correctly serialize JObject and JArray types. - Updates ExtendedEntityChanged properties to use setter injection instead of create method to support STJ deserialization via message bus. - Adds tests to validate serialization of core models. - Improves Project Configuration management. --- .../Models/ClientConfiguration.cs | 2 +- .../Models/Messaging/ExtendedEntityChanged.cs | 8 +- .../Exceptions/DocumentNotFoundException.cs | 2 +- .../ObjectToInferredTypesConverter.cs | 11 + .../Utility/Results/MessageContent.cs | 8 +- .../Controllers/ProjectControllerTests.cs | 287 +++++++++++++++++- .../ClientConfigurationSerializerTests.cs | 96 ++++++ .../Models/DataDictionarySerializerTests.cs | 148 +++++++++ .../ExtendedEntityChangedSerializerTests.cs | 73 +++++ .../Models/GenericArgumentsSerializerTests.cs | 68 +++++ .../Models/MessageContentSerializerTests.cs | 52 ++++ .../Models/ModuleCollectionSerializerTests.cs | 108 +++++++ .../ParameterCollectionSerializerTests.cs | 103 +++++++ .../Models/ProjectSerializerTests.cs | 72 +++++ .../SettingsDictionarySerializerTests.cs | 122 ++++++++ .../StackFrameCollectionSerializerTests.cs | 105 +++++++ .../Models/TagSetSerializerTests.cs | 56 ++++ 17 files changed, 1309 insertions(+), 12 deletions(-) create mode 100644 tests/Exceptionless.Tests/Serializer/Models/ClientConfigurationSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/DataDictionarySerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/ExtendedEntityChangedSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/GenericArgumentsSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/MessageContentSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/ProjectSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/SettingsDictionarySerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs create mode 100644 tests/Exceptionless.Tests/Serializer/Models/TagSetSerializerTests.cs diff --git a/src/Exceptionless.Core/Models/ClientConfiguration.cs b/src/Exceptionless.Core/Models/ClientConfiguration.cs index ffcd0b34c9..44f1c12495 100644 --- a/src/Exceptionless.Core/Models/ClientConfiguration.cs +++ b/src/Exceptionless.Core/Models/ClientConfiguration.cs @@ -3,7 +3,7 @@ public class ClientConfiguration { public int Version { get; set; } - public SettingsDictionary Settings { get; private set; } = new(); + public SettingsDictionary Settings { get; init; } = new(); public void IncrementVersion() { diff --git a/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs b/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs index b0462a7885..0e8dd4a67d 100644 --- a/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs +++ b/src/Exceptionless.Core/Models/Messaging/ExtendedEntityChanged.cs @@ -6,11 +6,9 @@ namespace Exceptionless.Core.Messaging.Models; [DebuggerDisplay("{Type} {ChangeType}: Id={Id}, OrganizationId={OrganizationId}, ProjectId={ProjectId}, StackId={StackId}")] public class ExtendedEntityChanged : EntityChanged { - private ExtendedEntityChanged() { } // Ensure create is used. - - public string? OrganizationId { get; private set; } - public string? ProjectId { get; private set; } - public string? StackId { get; private set; } + public string? OrganizationId { get; set; } + public string? ProjectId { get; set; } + public string? StackId { get; set; } public static ExtendedEntityChanged Create(EntityChanged entityChanged, bool removeWhenSettingProperties = true) { diff --git a/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs b/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs index 3ff2361138..ad669b9621 100644 --- a/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs +++ b/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs @@ -7,7 +7,7 @@ public DocumentNotFoundException(string id, string? message = null) : base(messa Id = id; } - public string Id { get; private set; } + public string Id { get; init; } public override string ToString() { diff --git a/src/Exceptionless.Core/Serialization/ObjectToInferredTypesConverter.cs b/src/Exceptionless.Core/Serialization/ObjectToInferredTypesConverter.cs index f450a87777..8d5dd5841f 100644 --- a/src/Exceptionless.Core/Serialization/ObjectToInferredTypesConverter.cs +++ b/src/Exceptionless.Core/Serialization/ObjectToInferredTypesConverter.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Exceptionless.Core.Models; +using Newtonsoft.Json.Linq; namespace Exceptionless.Core.Serialization; @@ -74,6 +75,16 @@ public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerO return; } + // Handle Newtonsoft JToken types (stored in DataDictionary by DataObjectConverter + // when reading from Elasticsearch via NEST). Without this, STJ enumerates JToken's + // IEnumerable interface, producing nested empty arrays instead of proper JSON. + if (value is JToken jToken) + { + using var doc = JsonDocument.Parse(jToken.ToString(Newtonsoft.Json.Formatting.None)); + doc.RootElement.WriteTo(writer); + return; + } + // Serialize using the runtime type to get proper converter handling JsonSerializer.Serialize(writer, value, value.GetType(), options); } diff --git a/src/Exceptionless.Web/Utility/Results/MessageContent.cs b/src/Exceptionless.Web/Utility/Results/MessageContent.cs index e27c5573e5..354717b64c 100644 --- a/src/Exceptionless.Web/Utility/Results/MessageContent.cs +++ b/src/Exceptionless.Web/Utility/Results/MessageContent.cs @@ -1,11 +1,11 @@ -namespace Exceptionless.Web.Utility.Results; +using System.Text.Json.Serialization; +namespace Exceptionless.Web.Utility.Results; + +[method: JsonConstructor] public record MessageContent(string? Id, string Message) { public MessageContent(string message) : this(null, message) { } - - public string? Id { get; private set; } = Id; - public string Message { get; private set; } = Message; } diff --git a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs index c394aaf853..0e148a63dd 100644 --- a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Text.Json; using Exceptionless.Core.Jobs; using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; @@ -145,23 +147,306 @@ public async Task CanUpdateProjectWithExtraPayloadProperties() } [Fact] - public async Task CanGetProjectConfiguration() + public async Task GetConfigAsync_WithClientAuth_ReturnsConfigurationWithSettings() { + // Act var response = await SendRequestAsync(r => r .AsFreeOrganizationClientUser() .AppendPath("projects/config") .StatusCodeShouldBeOk() ); + // Assert - response headers Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType); Assert.Equal("utf-8", response.Content.Headers.ContentType?.CharSet); Assert.True(response.Content.Headers.ContentLength.HasValue); Assert.True(response.Content.Headers.ContentLength > 0); + // Assert - deserialized model var config = await response.DeserializeAsync(); Assert.NotNull(config); Assert.True(config.Settings.GetBoolean("IncludeConditionalData")); Assert.Equal(0, config.Version); + + // Assert - raw JSON uses snake_case and correct structure + string json = await response.Content.ReadAsStringAsync(TestCancellationToken); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("version", out var versionProp), "Expected snake_case property 'version' in JSON"); + Assert.Equal(0, versionProp.GetInt32()); + Assert.True(root.TryGetProperty("settings", out var settingsProp), "Expected snake_case property 'settings' in JSON"); + Assert.Equal(JsonValueKind.Object, settingsProp.ValueKind); + Assert.True(settingsProp.TryGetProperty("IncludeConditionalData", out var settingValue), "Expected 'IncludeConditionalData' key in settings"); + Assert.Equal("true", settingValue.GetString()); + } + + [Fact] + public async Task GetConfigAsync_WithCurrentVersion_ReturnsNotModified() + { + // Arrange - get the current config version + var config = await SendRequestAsAsync(r => r + .AsFreeOrganizationClientUser() + .AppendPath("projects/config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(config); + + // Act - request with the current version + var response = await SendRequestAsync(r => r + .AsFreeOrganizationClientUser() + .AppendPath("projects/config") + .QueryString("v", config.Version.ToString()) + .ExpectedStatus(HttpStatusCode.NotModified) + ); + + // Assert + Assert.Equal(HttpStatusCode.NotModified, response.StatusCode); + } + + [Fact] + public async Task GetConfigAsync_WithStaleVersion_ReturnsUpdatedConfig() + { + // Arrange - get initial config + var config = await SendRequestAsAsync(r => r + .AsFreeOrganizationClientUser() + .AppendPath("projects/config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(config); + int initialVersion = config.Version; + + // Increment the version by setting a new config value + await SendRequestAsync(r => r + .AsFreeOrganizationUser() + .Post() + .AppendPaths("projects", SampleDataService.FREE_PROJECT_ID, "config") + .QueryString("key", "StaleVersionTest") + .Content(new ValueFromBody("StaleValue")) + .StatusCodeShouldBeOk() + ); + + // Act - request with the old (stale) version + var updatedConfig = await SendRequestAsAsync(r => r + .AsFreeOrganizationClientUser() + .AppendPath("projects/config") + .QueryString("v", initialVersion.ToString()) + .StatusCodeShouldBeOk() + ); + + // Assert + Assert.NotNull(updatedConfig); + Assert.True(updatedConfig.Version > initialVersion); + Assert.Equal("StaleValue", updatedConfig.Settings.GetString("StaleVersionTest")); + } + + [Fact] + public async Task SetConfigAsync_WithValidKeyAndValue_PersistsAndIncrementsVersion() + { + // Arrange - get initial config + var initialConfig = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(initialConfig); + + // Act - set a new config value + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Post() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "MyNewSetting") + .Content(new ValueFromBody("MyNewValue")) + .StatusCodeShouldBeOk() + ); + + // Assert - verify the setting was persisted + var updatedConfig = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(updatedConfig); + Assert.Equal("MyNewValue", updatedConfig.Settings.GetString("MyNewSetting")); + Assert.Equal(initialConfig.Version + 1, updatedConfig.Version); + } + + [Fact] + public async Task SetConfigAsync_WithEmptyKey_ReturnsBadRequest() + { + // Arrange - get initial config version + var configBefore = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(configBefore); + + // Act + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Post() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "") + .Content(new ValueFromBody("SomeValue")) + .StatusCodeShouldBeBadRequest() + ); + + // Assert - version should not change + var configAfter = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(configAfter); + Assert.Equal(configBefore.Version, configAfter.Version); + } + + [Fact] + public async Task SetConfigAsync_WithEmptyValue_ReturnsBadRequest() + { + // Arrange - get initial config version + var configBefore = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(configBefore); + + // Act + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Post() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "TestKey") + .Content(new ValueFromBody("")) + .StatusCodeShouldBeBadRequest() + ); + + // Assert - version should not change + var configAfter = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(configAfter); + Assert.Equal(configBefore.Version, configAfter.Version); + } + + [Fact] + public async Task SetConfigAsync_RoundTrip_JsonSerializesCorrectly() + { + // Arrange - set a config value + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Post() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "SerializationTest") + .Content(new ValueFromBody("TestValue123")) + .StatusCodeShouldBeOk() + ); + + // Act - get raw JSON from the API + var response = await SendRequestAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + + string json = await response.Content.ReadAsStringAsync(TestCancellationToken); + + // Assert - validate JSON structure matches client expectations + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + // Should have snake_case property names + Assert.True(root.TryGetProperty("version", out _), "Expected 'version' property (snake_case)"); + Assert.False(root.TryGetProperty("Version", out _), "Should not have PascalCase 'Version' property"); + Assert.True(root.TryGetProperty("settings", out var settings), "Expected 'settings' property (snake_case)"); + Assert.False(root.TryGetProperty("Settings", out _), "Should not have PascalCase 'Settings' property"); + + // Settings should be a flat dictionary, not a wrapped object + Assert.Equal(JsonValueKind.Object, settings.ValueKind); + Assert.True(settings.TryGetProperty("SerializationTest", out var testValue)); + Assert.Equal("TestValue123", testValue.GetString()); + + // Settings keys should preserve original casing (not be snake_cased) + Assert.True(settings.TryGetProperty("IncludeConditionalData", out _), + "Settings dictionary keys should preserve original casing"); + } + + [Fact] + public async Task DeleteConfigAsync_WithExistingKey_RemovesSettingAndIncrementsVersion() + { + // Arrange - add a config setting first + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Post() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "ToBeDeleted") + .Content(new ValueFromBody("DeleteMe")) + .StatusCodeShouldBeOk() + ); + + var configBeforeDelete = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(configBeforeDelete); + Assert.Equal("DeleteMe", configBeforeDelete.Settings.GetString("ToBeDeleted")); + + // Act - delete the config setting + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Delete() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "ToBeDeleted") + .StatusCodeShouldBeOk() + ); + + // Assert - verify the setting was removed and version incremented + var configAfterDelete = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(configAfterDelete); + Assert.Null(configAfterDelete.Settings.GetString("ToBeDeleted", null)); + Assert.Equal(configBeforeDelete.Version + 1, configAfterDelete.Version); + } + + [Fact] + public async Task DeleteConfigAsync_WithNonExistentKey_ReturnsOkWithoutVersionChange() + { + // Arrange - get current config version + var configBefore = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + Assert.NotNull(configBefore); + + // Act - delete a key that doesn't exist + await SendRequestAsync(r => r + .AsTestOrganizationUser() + .Delete() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .QueryString("key", "NonExistentKey12345") + .StatusCodeShouldBeOk() + ); + + // Assert - version should not change + var configAfter = await SendRequestAsAsync(r => r + .AsTestOrganizationClientUser() + .AppendPaths("projects", SampleDataService.TEST_PROJECT_ID, "config") + .StatusCodeShouldBeOk() + ); + + Assert.NotNull(configAfter); + Assert.Equal(configBefore.Version, configAfter.Version); } [Fact] diff --git a/tests/Exceptionless.Tests/Serializer/Models/ClientConfigurationSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ClientConfigurationSerializerTests.cs new file mode 100644 index 0000000000..e585d73ff4 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/ClientConfigurationSerializerTests.cs @@ -0,0 +1,96 @@ +using System.Text.Json; +using Exceptionless.Core.Models; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests ClientConfiguration serialization through ITextSerializer. +/// Critical: Settings property uses init accessor. STJ must populate the +/// SettingsDictionary during deserialization so settings survive round-trips. +/// This is the exact bug that caused empty settings in production. +/// +public class ClientConfigurationSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public ClientConfigurationSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesSettings() + { + // Arrange + var config = new ClientConfiguration { Version = 5 }; + config.Settings["IncludeConditionalData"] = "true"; + config.Settings["DataExclusions"] = "password"; + + // Act + string json = _serializer.SerializeToString(config); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(5, deserialized.Version); + Assert.Equal(2, deserialized.Settings.Count); + Assert.True(deserialized.Settings.GetBoolean("IncludeConditionalData")); + Assert.Equal("password", deserialized.Settings.GetString("DataExclusions")); + } + + [Fact] + public void SerializeToString_UsesSnakeCasePropertyNames() + { + // Arrange + var config = new ClientConfiguration { Version = 3 }; + config.Settings["TestKey"] = "TestValue"; + + // Act + string json = _serializer.SerializeToString(config); + + // Assert — property names should be snake_case, dictionary keys preserved + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("version", out _), "Expected snake_case 'version'"); + Assert.False(root.TryGetProperty("Version", out _), "Should not have PascalCase 'Version'"); + Assert.True(root.TryGetProperty("settings", out var settings), "Expected snake_case 'settings'"); + Assert.True(settings.TryGetProperty("TestKey", out var testVal), "Dictionary keys should preserve original casing"); + Assert.Equal("TestValue", testVal.GetString()); + } + + [Fact] + public void Deserialize_EmptySettings_ReturnsEmptyDictionary() + { + // Arrange + /* language=json */ + const string json = """{"version":1,"settings":{}}"""; + + // Act + var config = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(config); + Assert.Equal(1, config.Version); + Assert.NotNull(config.Settings); + Assert.Empty(config.Settings); + } + + [Fact] + public void Deserialize_MissingSettings_DefaultsToEmptyDictionary() + { + // Arrange + /* language=json */ + const string json = """{"version":2}"""; + + // Act + var config = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(config); + Assert.Equal(2, config.Version); + Assert.NotNull(config.Settings); + Assert.Empty(config.Settings); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/DataDictionarySerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/DataDictionarySerializerTests.cs new file mode 100644 index 0000000000..dae967ec08 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/DataDictionarySerializerTests.cs @@ -0,0 +1,148 @@ +using Exceptionless.Core.Models; +using Foundatio.Serializer; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests DataDictionary serialization through ITextSerializer. +/// DataDictionary extends Dictionary<string, object?> directly, so STJ +/// handles it natively. These tests guard against regressions. +/// +public class DataDictionarySerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public DataDictionarySerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesEntries() + { + // Arrange + var data = new DataDictionary + { + { "StringKey", "value" }, + { "IntKey", 42 }, + { "BoolKey", true } + }; + + // Act + string json = _serializer.SerializeToString(data); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(3, deserialized.Count); + Assert.Equal("value", deserialized.GetString("StringKey")); + } + + [Fact] + public void Deserialize_EmptyDictionary_ReturnsEmptyData() + { + // Arrange + /* language=json */ + const string json = """{}"""; + + // Act + var data = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(data); + Assert.Empty(data); + } + + /// + /// Reproduces production bug where JObject/JArray values in DataDictionary + /// (stored by Newtonsoft-based DataObjectConverter when reading from Elasticsearch) + /// serialize as nested empty arrays instead of proper JSON when written by STJ. + /// + [Fact] + public void Serialize_JObjectValue_WritesCorrectJson() + { + // Arrange — simulate Elasticsearch read path storing JObject in DataDictionary + var jObject = JObject.Parse(""" + { + "docsSecondari": [ + { "tipo": "CI", "numero": "AB123" }, + { "tipo": "PP", "numero": "CD456" } + ], + "docPrimario": { "tipo": "DL", "numero": "XY789" }, + "numeroDocumentiSecondari": 2, + "AlreadyImported": true + } + """); + + var data = new DataDictionary { ["TestUfficialeVO"] = jObject }; + + // Act + string json = _serializer.SerializeToString(data); + + // Assert — must contain actual property values, not nested empty arrays + Assert.Contains("docsSecondari", json); + Assert.Contains("CI", json); + Assert.Contains("AB123", json); + Assert.Contains("docPrimario", json); + Assert.Contains("XY789", json); + Assert.DoesNotContain("[[[]]]", json); + } + + /// + /// Verifies JArray values in DataDictionary serialize correctly. + /// + [Fact] + public void Serialize_JArrayValue_WritesCorrectJson() + { + // Arrange — simulate Elasticsearch storing JArray in DataDictionary + var jArray = JArray.Parse("""["tag1", "tag2", "tag3"]"""); + var data = new DataDictionary { ["Tags"] = jArray }; + + // Act + string json = _serializer.SerializeToString(data); + + // Assert + Assert.Contains("tag1", json); + Assert.Contains("tag2", json); + Assert.Contains("tag3", json); + Assert.DoesNotContain("[[]]", json); + } + + /// + /// Verifies deeply nested JObject structures serialize correctly, + /// matching the exact production data pattern that was broken. + /// + [Fact] + public void Serialize_DeeplyNestedJObject_PreservesStructure() + { + // Arrange — nested structure matching production data shape + var jObject = JObject.Parse(""" + { + "items": [ + { + "name": "item1", + "children": [ + { "id": 1, "value": "a" }, + { "id": 2, "value": "b" } + ] + } + ], + "count": 1 + } + """); + + var data = new DataDictionary { ["NestedData"] = jObject }; + + // Act + string json = _serializer.SerializeToString(data); + var deserialized = _serializer.Deserialize(json); + + // Assert — roundtrip preserves structure + Assert.NotNull(deserialized); + Assert.Contains("item1", json); + Assert.Contains("\"id\"", json); + Assert.Contains("\"value\"", json); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/ExtendedEntityChangedSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ExtendedEntityChangedSerializerTests.cs new file mode 100644 index 0000000000..0544b65ffb --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/ExtendedEntityChangedSerializerTests.cs @@ -0,0 +1,73 @@ +using Exceptionless.Core.Messaging.Models; +using Exceptionless.Core.Models; +using Foundatio.Repositories.Models; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests ExtendedEntityChanged serialization through ITextSerializer. +/// ExtendedEntityChanged has private set properties and a private constructor. +/// It is created via the Create() factory method but goes through message bus +/// serialization (ISerializer → STJ) in production (Redis). +/// +public class ExtendedEntityChangedSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public ExtendedEntityChangedSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesProperties() + { + // Arrange + var entityChanged = new EntityChanged + { + Id = "abc123", + Type = typeof(Project).Name, + ChangeType = ChangeType.Saved, + Data = { { "OrganizationId", "org1" }, { "ProjectId", "proj1" }, { "StackId", "stack1" } } + }; + var model = ExtendedEntityChanged.Create(entityChanged); + + // Act + string json = _serializer.SerializeToString(model); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal("abc123", deserialized.Id); + Assert.Equal("org1", deserialized.OrganizationId); + Assert.Equal("proj1", deserialized.ProjectId); + Assert.Equal("stack1", deserialized.StackId); + } + + [Fact] + public void Deserialize_RoundTrip_WithPartialData_PreservesAvailableProperties() + { + // Arrange — not all entity changes have all three IDs + var entityChanged = new EntityChanged + { + Id = "def456", + Type = typeof(Project).Name, + ChangeType = ChangeType.Removed, + Data = { { "OrganizationId", "org1" } } + }; + var model = ExtendedEntityChanged.Create(entityChanged); + + // Act + string json = _serializer.SerializeToString(model); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal("def456", deserialized.Id); + Assert.Equal("org1", deserialized.OrganizationId); + Assert.Null(deserialized.ProjectId); + Assert.Null(deserialized.StackId); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/GenericArgumentsSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/GenericArgumentsSerializerTests.cs new file mode 100644 index 0000000000..b0c3f62a45 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/GenericArgumentsSerializerTests.cs @@ -0,0 +1,68 @@ +using Exceptionless.Core.Models; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests GenericArguments serialization through ITextSerializer. +/// GenericArguments extends Collection<string> directly. +/// +public class GenericArgumentsSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public GenericArgumentsSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesAllArguments() + { + // Arrange + var args = new GenericArguments { "TEvent", "TResult", "CancellationToken" }; + + // Act + string json = _serializer.SerializeToString(args); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(3, deserialized.Count); + Assert.Equal("TEvent", deserialized[0]); + Assert.Equal("TResult", deserialized[1]); + Assert.Equal("CancellationToken", deserialized[2]); + } + + [Fact] + public void Deserialize_EmptyArray_ReturnsEmptyCollection() + { + // Arrange + /* language=json */ + const string json = """[]"""; + + // Act + var args = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(args); + Assert.Empty(args); + } + + [Fact] + public void Deserialize_SingleArgument_RoundTrips() + { + // Arrange + var args = new GenericArguments { "Task`1" }; + + // Act + string json = _serializer.SerializeToString(args); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Single(deserialized); + Assert.Equal("Task`1", deserialized[0]); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/MessageContentSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/MessageContentSerializerTests.cs new file mode 100644 index 0000000000..d9deebc742 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/MessageContentSerializerTests.cs @@ -0,0 +1,52 @@ +using Exceptionless.Web.Utility.Results; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests MessageContent serialization through ITextSerializer. +/// MessageContent is a record returned from API endpoints. +/// Validates that the record primary constructor properties serialize correctly. +/// +public class MessageContentSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public MessageContentSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesProperties() + { + // Arrange + var message = new MessageContent("id123", "Something happened"); + + // Act + string json = _serializer.SerializeToString(message); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal("id123", deserialized.Id); + Assert.Equal("Something happened", deserialized.Message); + } + + [Fact] + public void Deserialize_RoundTrip_WithNullId_PreservesMessage() + { + // Arrange + var message = new MessageContent("Operation completed successfully"); + + // Act + string json = _serializer.SerializeToString(message); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Null(deserialized.Id); + Assert.Equal("Operation completed successfully", deserialized.Message); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs new file mode 100644 index 0000000000..3af413b812 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs @@ -0,0 +1,108 @@ +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.Data; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests ModuleCollection serialization through ITextSerializer. +/// ModuleCollection extends Collection<Module> directly. +/// +public class ModuleCollectionSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public ModuleCollectionSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesAllModules() + { + // Arrange + var collection = new ModuleCollection + { + new() + { + ModuleId = 1, + Name = "Exceptionless.Core", + Version = "8.1.0", + IsEntry = true, + CreatedDate = new DateTime(2026, 1, 15, 10, 30, 0, DateTimeKind.Utc), + ModifiedDate = new DateTime(2026, 2, 10, 14, 0, 0, DateTimeKind.Utc), + Data = new DataDictionary { ["PublicKeyToken"] = "b77a5c561934e089" } + }, + new() + { + ModuleId = 2, + Name = "Foundatio", + Version = "11.0.0", + IsEntry = false + } + }; + + // Act + string json = _serializer.SerializeToString(collection); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Count); + + Assert.Equal(1, deserialized[0].ModuleId); + Assert.Equal("Exceptionless.Core", deserialized[0].Name); + Assert.Equal("8.1.0", deserialized[0].Version); + Assert.True(deserialized[0].IsEntry); + Assert.NotNull(deserialized[0].CreatedDate); + Assert.NotNull(deserialized[0].ModifiedDate); + Assert.NotNull(deserialized[0].Data); + + Assert.Equal(2, deserialized[1].ModuleId); + Assert.Equal("Foundatio", deserialized[1].Name); + Assert.False(deserialized[1].IsEntry); + } + + [Fact] + public void Deserialize_EmptyArray_ReturnsEmptyCollection() + { + // Arrange + /* language=json */ + const string json = """[]"""; + + // Act + var collection = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(collection); + Assert.Empty(collection); + } + + [Fact] + public void SerializeToString_UsesSnakeCasePropertyNames() + { + // Arrange + var collection = new ModuleCollection + { + new() + { + ModuleId = 42, + Name = "System.Runtime", + IsEntry = false, + CreatedDate = DateTime.UtcNow + } + }; + + // Act + string json = _serializer.SerializeToString(collection); + + // Assert + Assert.Contains("module_id", json); + Assert.Contains("is_entry", json); + Assert.Contains("created_date", json); + Assert.DoesNotContain("ModuleId", json); + Assert.DoesNotContain("IsEntry", json); + Assert.DoesNotContain("CreatedDate", json); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs new file mode 100644 index 0000000000..ed0cc9f550 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs @@ -0,0 +1,103 @@ +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.Data; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests ParameterCollection serialization through ITextSerializer. +/// ParameterCollection extends Collection<Parameter> directly. +/// +public class ParameterCollectionSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public ParameterCollectionSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesAllParameters() + { + // Arrange + var collection = new ParameterCollection + { + new() + { + Name = "context", + Type = "PipelineContext", + TypeNamespace = "Exceptionless.Core.Pipeline", + Data = new DataDictionary { ["IsValid"] = true }, + GenericArguments = new GenericArguments { "EventContext" } + }, + new() + { + Name = "cancellationToken", + Type = "CancellationToken", + TypeNamespace = "System.Threading" + } + }; + + // Act + string json = _serializer.SerializeToString(collection); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Count); + + Assert.Equal("context", deserialized[0].Name); + Assert.Equal("PipelineContext", deserialized[0].Type); + Assert.Equal("Exceptionless.Core.Pipeline", deserialized[0].TypeNamespace); + Assert.NotNull(deserialized[0].Data); + Assert.NotNull(deserialized[0].GenericArguments); + Assert.Single(deserialized[0].GenericArguments!); + Assert.Equal("EventContext", deserialized[0].GenericArguments![0]); + + Assert.Equal("cancellationToken", deserialized[1].Name); + Assert.Equal("CancellationToken", deserialized[1].Type); + Assert.Equal("System.Threading", deserialized[1].TypeNamespace); + } + + [Fact] + public void Deserialize_EmptyArray_ReturnsEmptyCollection() + { + // Arrange + /* language=json */ + const string json = """[]"""; + + // Act + var collection = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(collection); + Assert.Empty(collection); + } + + [Fact] + public void SerializeToString_UsesSnakeCasePropertyNames() + { + // Arrange + var collection = new ParameterCollection + { + new() + { + Name = "request", + Type = "HttpRequest", + TypeNamespace = "Microsoft.AspNetCore.Http", + GenericArguments = new GenericArguments { "string", "int" } + } + }; + + // Act + string json = _serializer.SerializeToString(collection); + + // Assert + Assert.Contains("type_namespace", json); + Assert.Contains("generic_arguments", json); + Assert.DoesNotContain("TypeNamespace", json); + Assert.DoesNotContain("GenericArguments", json); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/ProjectSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ProjectSerializerTests.cs new file mode 100644 index 0000000000..b512a647f6 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/ProjectSerializerTests.cs @@ -0,0 +1,72 @@ +using Exceptionless.Core.Models; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests Project serialization through ITextSerializer. +/// Critical: Project.Configuration contains SettingsDictionary which must +/// survive round-trip through the cache serializer (STJ). +/// +public class ProjectSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public ProjectSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesConfigurationSettings() + { + // Arrange — simulates what the cache serializer does in ProjectRepository.GetConfigAsync + var project = new Project + { + Id = "test-project-id", + OrganizationId = "test-org-id", + Name = "Test Project" + }; + project.Configuration.Version = 10; + project.Configuration.Settings["IncludeConditionalData"] = "true"; + project.Configuration.Settings["DataExclusions"] = "password,secret"; + + // Act + string json = _serializer.SerializeToString(project); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal("test-project-id", deserialized.Id); + Assert.Equal(10, deserialized.Configuration.Version); + Assert.Equal(2, deserialized.Configuration.Settings.Count); + Assert.True(deserialized.Configuration.Settings.GetBoolean("IncludeConditionalData")); + Assert.Equal("password,secret", deserialized.Configuration.Settings.GetString("DataExclusions")); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesBasicProperties() + { + // Arrange + var project = new Project + { + Id = "proj1", + OrganizationId = "org1", + Name = "My Project", + NextSummaryEndOfDayTicks = 637500000000000000, + IsConfigured = true + }; + + // Act + string json = _serializer.SerializeToString(project); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal("proj1", deserialized.Id); + Assert.Equal("org1", deserialized.OrganizationId); + Assert.Equal("My Project", deserialized.Name); + Assert.True(deserialized.IsConfigured); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/SettingsDictionarySerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/SettingsDictionarySerializerTests.cs new file mode 100644 index 0000000000..a3128df05d --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/SettingsDictionarySerializerTests.cs @@ -0,0 +1,122 @@ +using System.Text.Json; +using Exceptionless.Core.Models; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests SettingsDictionary serialization through ITextSerializer. +/// Critical: SettingsDictionary extends ObservableDictionary which implements +/// IDictionary via composition (not inheritance). STJ must serialize it as a +/// flat dictionary, not as an empty object. +/// +public class SettingsDictionarySerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public SettingsDictionarySerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void SerializeToString_WithEntries_SerializesAsFlatDictionary() + { + // Arrange + var settings = new SettingsDictionary + { + { "IncludeConditionalData", "true" }, + { "DataExclusions", "password,secret" } + }; + + // Act + string json = _serializer.SerializeToString(settings); + + // Assert — should be a flat dictionary, not a wrapped object + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + Assert.Equal(JsonValueKind.Object, root.ValueKind); + Assert.True(root.TryGetProperty("IncludeConditionalData", out var includeVal)); + Assert.Equal("true", includeVal.GetString()); + Assert.True(root.TryGetProperty("DataExclusions", out var exclusionsVal)); + Assert.Equal("password,secret", exclusionsVal.GetString()); + } + + [Fact] + public void Deserialize_FlatDictionaryJson_PopulatesEntries() + { + // Arrange + /* language=json */ + const string json = """{"IncludeConditionalData":"true","DataExclusions":"password,secret"}"""; + + // Act + var settings = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(settings); + Assert.Equal(2, settings.Count); + Assert.Equal("true", settings.GetString("IncludeConditionalData")); + Assert.Equal("password,secret", settings.GetString("DataExclusions")); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesAllEntries() + { + // Arrange + var original = new SettingsDictionary + { + { "BoolSetting", "true" }, + { "IntSetting", "42" }, + { "StringSetting", "hello" } + }; + + // Act + string json = _serializer.SerializeToString(original); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(original.Count, deserialized.Count); + Assert.True(deserialized.GetBoolean("BoolSetting")); + Assert.Equal(42, deserialized.GetInt32("IntSetting")); + Assert.Equal("hello", deserialized.GetString("StringSetting")); + } + + [Fact] + public void Deserialize_EmptyDictionary_ReturnsEmptySettings() + { + // Arrange + /* language=json */ + const string json = """{}"""; + + // Act + var settings = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(settings); + Assert.Empty(settings); + } + + [Fact] + public void SerializeToString_PreservesOriginalKeyCasing() + { + // Arrange — dictionary keys should NOT be snake_cased + var settings = new SettingsDictionary + { + { "@@DataExclusions", "password" }, + { "IncludePrivateInformation", "true" }, + { "MyCustomSetting", "value" } + }; + + // Act + string json = _serializer.SerializeToString(settings); + + // Assert + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("@@DataExclusions", out _)); + Assert.True(root.TryGetProperty("IncludePrivateInformation", out _)); + Assert.True(root.TryGetProperty("MyCustomSetting", out _)); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs new file mode 100644 index 0000000000..8c6daf8160 --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs @@ -0,0 +1,105 @@ +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.Data; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests StackFrameCollection serialization through ITextSerializer. +/// StackFrameCollection extends Collection<StackFrame> directly. +/// Individual StackFrame serialization is covered in StackFrameSerializerTests. +/// +public class StackFrameCollectionSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public StackFrameCollectionSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesAllFrames() + { + // Arrange + var collection = new StackFrameCollection + { + new() + { + Name = "ProcessEventAsync", + DeclaringNamespace = "Exceptionless.Core.Pipeline", + DeclaringType = "EventPipeline", + FileName = "EventPipeline.cs", + LineNumber = 142, + Column = 25 + }, + new() + { + Name = "ExecuteAsync", + DeclaringNamespace = "Exceptionless.Core.Jobs", + DeclaringType = "EventPostsJob", + FileName = "EventPostsJob.cs", + LineNumber = 88 + } + }; + + // Act + string json = _serializer.SerializeToString(collection); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Count); + Assert.Equal("ProcessEventAsync", deserialized[0].Name); + Assert.Equal("EventPipeline.cs", deserialized[0].FileName); + Assert.Equal(142, deserialized[0].LineNumber); + Assert.Equal(25, deserialized[0].Column); + Assert.Equal("ExecuteAsync", deserialized[1].Name); + Assert.Equal("EventPostsJob.cs", deserialized[1].FileName); + Assert.Equal(88, deserialized[1].LineNumber); + Assert.Null(deserialized[1].Column); + } + + [Fact] + public void Deserialize_EmptyArray_ReturnsEmptyCollection() + { + // Arrange + /* language=json */ + const string json = """[]"""; + + // Act + var collection = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(collection); + Assert.Empty(collection); + } + + [Fact] + public void SerializeToString_UsesSnakeCasePropertyNames() + { + // Arrange + var collection = new StackFrameCollection + { + new() + { + Name = "Main", + DeclaringType = "Program", + FileName = "Program.cs", + LineNumber = 10 + } + }; + + // Act + string json = _serializer.SerializeToString(collection); + + // Assert + Assert.Contains("declaring_type", json); + Assert.Contains("file_name", json); + Assert.Contains("line_number", json); + Assert.DoesNotContain("DeclaringType", json); + Assert.DoesNotContain("FileName", json); + Assert.DoesNotContain("LineNumber", json); + } +} diff --git a/tests/Exceptionless.Tests/Serializer/Models/TagSetSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/TagSetSerializerTests.cs new file mode 100644 index 0000000000..30fbed387c --- /dev/null +++ b/tests/Exceptionless.Tests/Serializer/Models/TagSetSerializerTests.cs @@ -0,0 +1,56 @@ +using Exceptionless.Core.Models; +using Foundatio.Serializer; +using Xunit; + +namespace Exceptionless.Tests.Serializer.Models; + +/// +/// Tests TagSet serialization through ITextSerializer. +/// TagSet extends HashSet<string?> directly, so STJ handles it natively. +/// These tests guard against regressions. +/// +public class TagSetSerializerTests : TestWithServices +{ + private readonly ITextSerializer _serializer; + + public TagSetSerializerTests(ITestOutputHelper output) : base(output) + { + _serializer = GetService(); + } + + [Fact] + public void Deserialize_RoundTrip_PreservesValues() + { + // Arrange + var tags = new TagSet(); + tags.Add("Error"); + tags.Add("Critical"); + tags.Add("Production"); + + // Act + string json = _serializer.SerializeToString(tags); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(3, deserialized.Count); + Assert.Contains("Error", deserialized); + Assert.Contains("Critical", deserialized); + Assert.Contains("Production", deserialized); + } + + [Fact] + public void Deserialize_EmptyArray_ReturnsEmptyTagSet() + { + // Arrange + /* language=json */ + const string json = """[]"""; + + // Act + var tags = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(tags); + Assert.Empty(tags); + } +} From 42bd2d44c7461bf9bb49a1ee08063dc868b147c6 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 17 Feb 2026 13:33:54 -0600 Subject: [PATCH 02/29] Fixed linting errors --- .../ClientApp/src/lib/features/auth/schemas.ts | 4 +++- .../ClientApp/src/routes/(auth)/login/+page.svelte | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/auth/schemas.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/auth/schemas.ts index 4ab8b15a95..ddf5f00b0d 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/auth/schemas.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/auth/schemas.ts @@ -7,7 +7,9 @@ export { type SignupFormData, SignupSchema } from '$generated/schemas'; // In dev mode, allow addresses like test@localhost (no TLD required) export const LoginSchema = dev ? GeneratedLoginSchema.extend({ - email: string().min(1, 'Email is required').regex(/^[^\s@]+@[^\s@]+$/, 'Please enter a valid email address') + email: string() + .min(1, 'Email is required') + .regex(/^[^\s@]+@[^\s@]+$/, 'Please enter a valid email address') }) : GeneratedLoginSchema; export type LoginFormData = Infer; diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte index a0a93ab42f..bcd4e4465d 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte @@ -98,7 +98,10 @@ {#snippet children(field)} - Password Forgot password? + Password Forgot password? Date: Thu, 19 Feb 2026 22:31:39 -0600 Subject: [PATCH 03/29] Optimize CloseInactiveSessionsJob: batch Redis calls (#2119) * Optimize the close inactive sessions job * Feedback * refactor: return tuples from GetHeartbeatsBatchAsync for cleaner foreach loop Address PR feedback: - Remove weird IReadOnlyList cast at call site - GetHeartbeatsBatchAsync now returns (PersistentEvent, HeartbeatResult?) tuples - Caller uses simple foreach instead of index-based for loop - Accept IReadOnlyCollection to avoid cast at call site * Filter messages that nobody is listening for --- .gitignore | 2 + .../Jobs/CloseInactiveSessionsJob.cs | 91 +++++++++++++++---- .../Services/MessageService.cs | 61 +++++++++---- 3 files changed, 118 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 4d374fb0d0..f647a6846a 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ src/Exceptionless\.Web/ClientApp.angular/dist/ *.DotSettings coverage/ +nul +tmpclaude* diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index fade8ec24e..c968f02623 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -57,10 +57,9 @@ protected override async Task RunInternalAsync(JobContext context) var cacheKeysToRemove = new List(results.Documents.Count * 2); var existingSessionHeartbeatIds = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var sessionStart in results.Documents) + foreach (var (sessionStart, heartbeatResult) in await GetHeartbeatsBatchAsync(results.Documents)) { var lastActivityUtc = sessionStart.Date.UtcDateTime.AddSeconds((double)sessionStart.Value.GetValueOrDefault()); - var heartbeatResult = await GetHeartbeatAsync(sessionStart); bool closeDuplicate = heartbeatResult?.CacheKey is not null && existingSessionHeartbeatIds.Contains(heartbeatResult.CacheKey); if (heartbeatResult?.CacheKey is not null && !closeDuplicate) @@ -112,33 +111,85 @@ protected override async Task RunInternalAsync(JobContext context) return JobResult.Success; } - private async Task GetHeartbeatAsync(PersistentEvent sessionStart) + private async Task<(PersistentEvent Session, HeartbeatResult? Heartbeat)[]> GetHeartbeatsBatchAsync(IReadOnlyCollection sessionCollection) { - string? sessionId = sessionStart.GetSessionId(); - if (!String.IsNullOrWhiteSpace(sessionId)) + var sessions = sessionCollection.ToList(); + var allHeartbeatKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + var sessionKeyMap = new (string? SessionIdKey, string? UserIdentityKey)[sessions.Count]; + + for (int i = 0; i < sessions.Count; i++) { - var result = await GetLastHeartbeatActivityUtcAsync($"Project:{sessionStart.ProjectId}:heartbeat:{sessionId.ToSHA1()}"); - if (result is not null) - return result; + var session = sessions[i]; + string? sessionIdKey = null; + string? userIdentityKey = null; + + string? sessionId = session.GetSessionId(); + if (!String.IsNullOrWhiteSpace(sessionId)) + { + sessionIdKey = $"Project:{session.ProjectId}:heartbeat:{sessionId.ToSHA1()}"; + allHeartbeatKeys.Add(sessionIdKey); + } + + var user = session.GetUserIdentity(_jsonOptions); + if (!String.IsNullOrWhiteSpace(user?.Identity)) + { + userIdentityKey = $"Project:{session.ProjectId}:heartbeat:{user.Identity.ToSHA1()}"; + allHeartbeatKeys.Add(userIdentityKey); + } + + sessionKeyMap[i] = (sessionIdKey, userIdentityKey); } - var user = sessionStart.GetUserIdentity(_jsonOptions); - if (String.IsNullOrWhiteSpace(user?.Identity)) - return null; + if (allHeartbeatKeys.Count == 0) + return sessions.Select(s => (s, (HeartbeatResult?)null)).ToArray(); - return await GetLastHeartbeatActivityUtcAsync($"Project:{sessionStart.ProjectId}:heartbeat:{user.Identity.ToSHA1()}"); - } + var heartbeatValues = await _cache.GetAllAsync(allHeartbeatKeys); - private async Task GetLastHeartbeatActivityUtcAsync(string cacheKey) - { - var cacheValue = await _cache.GetAsync(cacheKey); - if (cacheValue.HasValue) + var closeKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + var resolved = new (DateTime ActivityUtc, string CacheKey)?[sessions.Count]; + + for (int i = 0; i < sessionKeyMap.Length; i++) + { + var (sessionIdKey, userIdentityKey) = sessionKeyMap[i]; + string? matchedKey = null; + DateTime activityUtc = default; + + if (sessionIdKey is not null && heartbeatValues.TryGetValue(sessionIdKey, out var sidVal) && sidVal.HasValue) + { + matchedKey = sessionIdKey; + activityUtc = sidVal.Value; + } + else if (userIdentityKey is not null && heartbeatValues.TryGetValue(userIdentityKey, out var uidVal) && uidVal.HasValue) + { + matchedKey = userIdentityKey; + activityUtc = uidVal.Value; + } + + if (matchedKey is not null) + { + resolved[i] = (activityUtc, matchedKey); + closeKeys.Add($"{matchedKey}-close"); + } + } + + IDictionary> closeValues = closeKeys.Count > 0 + ? await _cache.GetAllAsync(closeKeys) + : new Dictionary>(); + + var results = new (PersistentEvent Session, HeartbeatResult? Heartbeat)[sessions.Count]; + for (int i = 0; i < sessions.Count; i++) { - bool close = await _cache.GetAsync($"{cacheKey}-close", false); - return new HeartbeatResult { ActivityUtc = cacheValue.Value, Close = close, CacheKey = cacheKey }; + if (resolved[i] is not { } r) + { + results[i] = (sessions[i], null); + continue; + } + + bool close = closeValues.TryGetValue($"{r.CacheKey}-close", out var closeVal) && closeVal.HasValue && closeVal.Value; + results[i] = (sessions[i], new HeartbeatResult { ActivityUtc = r.ActivityUtc, Close = close, CacheKey = r.CacheKey }); } - return null; + return results; } public TimeSpan DefaultInactivePeriod { get; set; } = TimeSpan.FromMinutes(5); diff --git a/src/Exceptionless.Core/Services/MessageService.cs b/src/Exceptionless.Core/Services/MessageService.cs index 2592bee42b..94b535aa14 100644 --- a/src/Exceptionless.Core/Services/MessageService.cs +++ b/src/Exceptionless.Core/Services/MessageService.cs @@ -3,6 +3,7 @@ using Exceptionless.Core.Repositories; using Exceptionless.Core.Utility; using Foundatio.Extensions.Hosting.Startup; +using Foundatio.Repositories.Elasticsearch; using Foundatio.Repositories.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -11,16 +12,37 @@ namespace Exceptionless.Core.Services; public class MessageService : IDisposable, IStartupAction { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IUserRepository _userRepository; private readonly IStackRepository _stackRepository; private readonly IEventRepository _eventRepository; + private readonly ITokenRepository _tokenRepository; + private readonly IWebHookRepository _webHookRepository; private readonly IConnectionMapping _connectionMapping; private readonly AppOptions _options; private readonly ILogger _logger; + private readonly List _disposeActions = []; - public MessageService(IStackRepository stackRepository, IEventRepository eventRepository, IConnectionMapping connectionMapping, AppOptions options, ILoggerFactory loggerFactory) + public MessageService( + IOrganizationRepository organizationRepository, + IProjectRepository projectRepository, + IUserRepository userRepository, + IStackRepository stackRepository, + IEventRepository eventRepository, + ITokenRepository tokenRepository, + IWebHookRepository webHookRepository, + IConnectionMapping connectionMapping, + AppOptions options, + ILoggerFactory loggerFactory) { + _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _userRepository = userRepository; _stackRepository = stackRepository; _eventRepository = eventRepository; + _tokenRepository = tokenRepository; + _webHookRepository = webHookRepository; _connectionMapping = connectionMapping; _options = options; _logger = loggerFactory.CreateLogger() ?? NullLogger.Instance; @@ -31,26 +53,34 @@ public Task RunAsync(CancellationToken shutdownToken = default) if (!_options.EnableRepositoryNotifications) return Task.CompletedTask; - if (_stackRepository is StackRepository sr) - sr.BeforePublishEntityChanged.AddHandler(BeforePublishStackEntityChanged); - if (_eventRepository is EventRepository er) - er.BeforePublishEntityChanged.AddHandler(BeforePublishEventEntityChanged); + RegisterHandler(_organizationRepository); + RegisterHandler(_userRepository); + RegisterHandler(_projectRepository); + RegisterHandler(_stackRepository); + RegisterHandler(_eventRepository); + RegisterHandler(_tokenRepository); + RegisterHandler(_webHookRepository); return Task.CompletedTask; } - private async Task BeforePublishStackEntityChanged(object sender, BeforePublishEntityChangedEventArgs args) + private void RegisterHandler(object repository) where T : class, IIdentity, new() { - args.Cancel = await GetNumberOfListeners(args.Message) == 0; - if (args.Cancel) - _logger.LogTrace("Cancelled Stack Entity Changed Message: {@Message}", args.Message); + if (repository is not ElasticRepositoryBase repo) + return; + + Func, Task> handler = OnBeforePublishEntityChangedAsync; + repo.BeforePublishEntityChanged.AddHandler(handler); + _disposeActions.Add(() => repo.BeforePublishEntityChanged.RemoveHandler(handler)); } - private async Task BeforePublishEventEntityChanged(object sender, BeforePublishEntityChangedEventArgs args) + private async Task OnBeforePublishEntityChangedAsync(object sender, BeforePublishEntityChangedEventArgs args) + where T : class, IIdentity, new() { - args.Cancel = await GetNumberOfListeners(args.Message) == 0; + var listenerCount = await GetNumberOfListeners(args.Message); + args.Cancel = listenerCount == 0; if (args.Cancel) - _logger.LogTrace("Cancelled Persistent Event Entity Changed Message: {@Message}", args.Message); + _logger.LogTrace("Cancelled {EntityType} Entity Changed Message: {@Message}", typeof(T).Name, args.Message); } private Task GetNumberOfListeners(EntityChanged message) @@ -64,9 +94,8 @@ private Task GetNumberOfListeners(EntityChanged message) public void Dispose() { - if (_stackRepository is StackRepository sr) - sr.BeforePublishEntityChanged.RemoveHandler(BeforePublishStackEntityChanged); - if (_eventRepository is EventRepository er) - er.BeforePublishEntityChanged.RemoveHandler(BeforePublishEventEntityChanged); + foreach (var disposeAction in _disposeActions) + disposeAction(); + _disposeActions.Clear(); } } From 58354b514af907248a9bbc3283b1e5a026cc18c5 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 19 Feb 2026 23:07:40 -0600 Subject: [PATCH 04/29] Fixes previous day/week/month range selection Updates the quick ranges in the date range picker to correctly select the previous day, week, and month. The previous implementation was selecting from the end of the previous period to the end of the current period. --- src/Exceptionless.Core/Services/MessageService.cs | 3 ++- .../shared/components/date-range-picker/quick-ranges.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Exceptionless.Core/Services/MessageService.cs b/src/Exceptionless.Core/Services/MessageService.cs index 94b535aa14..0fe3e988a9 100644 --- a/src/Exceptionless.Core/Services/MessageService.cs +++ b/src/Exceptionless.Core/Services/MessageService.cs @@ -77,7 +77,7 @@ public Task RunAsync(CancellationToken shutdownToken = default) private async Task OnBeforePublishEntityChangedAsync(object sender, BeforePublishEntityChangedEventArgs args) where T : class, IIdentity, new() { - var listenerCount = await GetNumberOfListeners(args.Message); + int listenerCount = await GetNumberOfListeners(args.Message); args.Cancel = listenerCount == 0; if (args.Cancel) _logger.LogTrace("Cancelled {EntityType} Entity Changed Message: {@Message}", typeof(T).Name, args.Message); @@ -96,6 +96,7 @@ public void Dispose() { foreach (var disposeAction in _disposeActions) disposeAction(); + _disposeActions.Clear(); } } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-ranges.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-ranges.ts index d641ee1730..93f8e0a332 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-ranges.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-ranges.ts @@ -43,9 +43,9 @@ export const quickRanges: QuickRangeSection[] = [ { label: 'Complete periods', options: [ - { label: 'Previous day', value: '[now-1d/d TO now/d}' }, - { label: 'Previous week', value: '[now-1w/w TO now/w}' }, - { label: 'Previous month', value: '[now-1M/M TO now/M}' } + { label: 'Previous day', value: '[now-1d/d TO now-1d/d]' }, + { label: 'Previous week', value: '[now-1w/w TO now-1w/w]' }, + { label: 'Previous month', value: '[now-1M/M TO now-1M/M]' } ] } ]; From 971224b374ac10def4e5fa95eccaf47af60fe1f6 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 20 Feb 2026 17:40:18 -0600 Subject: [PATCH 05/29] Adds index to stack and event queries Improves stack and event query performance by adding the index to the query. This change ensures that date range queries on stacks and events leverage the index for faster retrieval. --- src/Exceptionless.Core/Jobs/DailySummaryJob.cs | 12 +++++++++++- .../Repositories/Queries/EventStackFilterQuery.cs | 1 + src/Exceptionless.Web/Controllers/EventController.cs | 3 ++- src/Exceptionless.Web/Controllers/StackController.cs | 9 ++++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index 3879e9b85b..d06ec7dd5d 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -179,7 +179,17 @@ private async Task SendSummaryNotificationAsync(Project project, SummaryNo IReadOnlyCollection? newest = null; if (newTotal > 0) - newest = (await _stackRepository.FindAsync(q => q.AppFilter(sf).FilterExpression(filter).SortExpression("-first").DateRange(data.UtcStartTime, data.UtcEndTime, "first"), o => o.PageLimit(3))).Documents; + { + var stackResults = await _stackRepository.FindAsync( + q => q.AppFilter(sf) + .FilterExpression(filter) + .SortExpression("-first") + .DateRange(data.UtcStartTime, data.UtcEndTime, "first") + .Index(data.UtcStartTime, data.UtcEndTime), + o => o.PageLimit(3)); + + newest = stackResults.Documents; + } foreach (var user in users) { diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index fc0cd34910..72637e5370 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -195,6 +195,7 @@ private IRepositoryQuery GetSystemFilterQuery(IQueryVisitorContext context, bool foreach (var range in builderContext?.Source.GetDateRanges() ?? Enumerable.Empty()) { systemFilterQuery.DateRange(range.StartDate, range.EndDate, range.Field, range.TimeZone); + // NOTE: We do not currently specify date range indexes here.. } } diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index f62b1e3275..cfcdee2e59 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -410,7 +410,8 @@ private Task> GetEventsInternalAsync(AppFilter sf, .FilterExpression(filter) .EnforceEventStackFilter() .SortExpression(sort) - .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), + .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) + .Index(ti.Range.UtcStart, ti.Range.UtcEnd), o => page.HasValue ? o.PageNumber(page).PageLimit(limit) : o.SearchBeforeToken(before).SearchAfterToken(after).PageLimit(limit)); diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index 4d2493e90a..c50bbfa0d5 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -485,7 +485,14 @@ private async Task>> GetInternalAsync(Ap try { - var results = await _repository.FindAsync(q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null).FilterExpression(filter).SortExpression(sort).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), o => o.PageNumber(page).PageLimit(limit)); + var results = await _repository.FindAsync( + q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null) + .FilterExpression(filter) + .SortExpression(sort) + .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) + .Index(ti.Range.UtcStart, ti.Range.UtcEnd), + o => o.PageNumber(page).PageLimit(limit) + ); var stacks = results.Documents.Select(s => s.ApplyOffset(ti.Offset)).ToList(); if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "summary", StringComparison.OrdinalIgnoreCase)) From 88ca9d155e771348d109528d0e84daff48e1e570 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 23 Feb 2026 08:13:56 -0600 Subject: [PATCH 06/29] Improves stack usage tracking and serialization Refactors stack usage tracking to use a record struct for cache keys, ensuring proper serialization and distinct keys for accurate accounting. https://github.com/exceptionless/Exceptionless/issues/2124 https://github.com/exceptionless/Exceptionless/issues/2113 Enables field inclusion in JsonSerializerOptions to correctly serialize tuples and records with their field names, instead of default Item1, Item2. This is crucial for the stack usage key to be properly serialized in Redis. Also fixes a date range filter on the stacks controller and adds a test case. Fixes #2124 Fixes #2113 --- .../JsonSerializerOptionsExtensions.cs | 3 + .../Services/StackService.cs | 17 +- .../Controllers/EventController.cs | 3 +- .../Controllers/StackController.cs | 9 +- .../Controllers/EventControllerTests.cs | 11 +- .../Controllers/StackControllerTests.cs | 29 +++ .../Serializer/SerializerTests.cs | 204 +++++++++++++++++- .../Services/StackServiceTests.cs | 8 +- 8 files changed, 260 insertions(+), 24 deletions(-) diff --git a/src/Exceptionless.Core/Serialization/JsonSerializerOptionsExtensions.cs b/src/Exceptionless.Core/Serialization/JsonSerializerOptionsExtensions.cs index ffc76ac9d2..7a05de2ae7 100644 --- a/src/Exceptionless.Core/Serialization/JsonSerializerOptionsExtensions.cs +++ b/src/Exceptionless.Core/Serialization/JsonSerializerOptionsExtensions.cs @@ -20,6 +20,9 @@ public static JsonSerializerOptions ConfigureExceptionlessDefaults(this JsonSeri options.PropertyNamingPolicy = LowerCaseUnderscoreNamingPolicy.Instance; options.Converters.Add(new ObjectToInferredTypesConverter()); + // Ensures tuples and records are serialized with their field names instead of "Item1", "Item2", etc. + options.IncludeFields = true; + // Enforces C# nullable annotations (string vs string?) during serialization/deserialization. // If you see "cannot be null" errors, fix the model's nullability annotation or the data. options.RespectNullableAnnotations = true; diff --git a/src/Exceptionless.Core/Services/StackService.cs b/src/Exceptionless.Core/Services/StackService.cs index 1b88ca8936..84671040ea 100644 --- a/src/Exceptionless.Core/Services/StackService.cs +++ b/src/Exceptionless.Core/Services/StackService.cs @@ -5,6 +5,11 @@ namespace Exceptionless.Core.Services; +/// +/// Identifies a stack for deferred usage counter updates. +/// +public record StackUsageKey(string OrganizationId, string ProjectId, string StackId); + public class StackService { private readonly ILogger _logger; @@ -30,7 +35,7 @@ public async Task IncrementStackUsageAsync(string organizationId, string project return; await Task.WhenAll( - _cache.ListAddAsync(GetStackOccurrenceSetCacheKey(), (organizationId, projectId, stackId)), + _cache.ListAddAsync(GetStackOccurrenceSetCacheKey(), new StackUsageKey(organizationId, projectId, stackId)), _cache.IncrementAsync(GetStackOccurrenceCountCacheKey(stackId), count, _expireTimeout), _cache.SetIfLowerAsync(GetStackOccurrenceMinDateCacheKey(stackId), minOccurrenceDateUtc, _expireTimeout), _cache.SetIfHigherAsync(GetStackOccurrenceMaxDateCacheKey(stackId), maxOccurrenceDateUtc, _expireTimeout) @@ -40,16 +45,20 @@ await Task.WhenAll( public async Task SaveStackUsagesAsync(bool sendNotifications = true, CancellationToken cancellationToken = default) { string occurrenceSetCacheKey = GetStackOccurrenceSetCacheKey(); - var stackUsageSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(occurrenceSetCacheKey); + var stackUsageSet = await _cache.GetListAsync(occurrenceSetCacheKey); if (!stackUsageSet.HasValue) return; - foreach ((string? organizationId, string? projectId, string? stackId) in stackUsageSet.Value) + foreach (var usage in stackUsageSet.Value) { if (cancellationToken.IsCancellationRequested) break; - var removeFromSetTask = _cache.ListRemoveAsync(occurrenceSetCacheKey, (organizationId, projectId, stackId)); + string organizationId = usage.OrganizationId; + string projectId = usage.ProjectId; + string stackId = usage.StackId; + + var removeFromSetTask = _cache.ListRemoveAsync(occurrenceSetCacheKey, new StackUsageKey(organizationId, projectId, stackId)); string countCacheKey = GetStackOccurrenceCountCacheKey(stackId); var countTask = _cache.GetAsync(countCacheKey, 0); string minDateCacheKey = GetStackOccurrenceMinDateCacheKey(stackId); diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index cfcdee2e59..6daf3ef7bd 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -337,7 +337,8 @@ private async Task>> GetInternalAsync( .SystemFilter(systemFilter) .FilterExpression(filter) .EnforceEventStackFilter() - .AggregationsExpression($"terms:(stack_id~{GetSkip(resolvedPage + 1, limit) + 1} {stackAggregations})")); + .AggregationsExpression($"terms:(stack_id~{GetSkip(resolvedPage + 1, limit) + 1} {stackAggregations})") + ); var stackTerms = countResponse.Aggregations.Terms("terms_stack_id"); if (stackTerms is null || stackTerms.Buckets.Count == 0) diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index c50bbfa0d5..4d2493e90a 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -485,14 +485,7 @@ private async Task>> GetInternalAsync(Ap try { - var results = await _repository.FindAsync( - q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null) - .FilterExpression(filter) - .SortExpression(sort) - .DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field) - .Index(ti.Range.UtcStart, ti.Range.UtcEnd), - o => o.PageNumber(page).PageLimit(limit) - ); + var results = await _repository.FindAsync(q => q.AppFilter(ShouldApplySystemFilter(sf, filter) ? sf : null).FilterExpression(filter).SortExpression(sort).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, ti.Field), o => o.PageNumber(page).PageLimit(limit)); var stacks = results.Documents.Select(s => s.ApplyOffset(ti.Offset)).ToList(); if (!String.IsNullOrEmpty(mode) && String.Equals(mode, "summary", StringComparison.OrdinalIgnoreCase)) diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index 1205debd51..0dd8842560 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -522,9 +522,16 @@ await CreateDataAsync(d => [Fact] public async Task WillGetStackEvents() { + var now = TimeProvider.GetUtcNow(); + + // Create events on different days for the same stack so they land in different + // daily index partitions. Dates must stay within org.CreatedUtc - 3d to avoid + // being filtered by the org creation cutoff (see GetRetentionUtcCutoff). var (stacks, _) = await CreateDataAsync(d => { - d.Event().TestProject(); + var ev = d.Event().TestProject().Date(now); + d.Event().Stack(ev).Date(now.AddDays(-1)); + d.Event().Stack(ev).Date(now.AddDays(-2)); }); Log.SetLogLevel(LogLevel.Trace); @@ -538,7 +545,7 @@ public async Task WillGetStackEvents() ); Assert.NotNull(result); - Assert.Single(result); + Assert.Equal(3, result.Count); } [Fact] diff --git a/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs b/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs index bbd7f0235c..5ffc0c36e1 100644 --- a/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/StackControllerTests.cs @@ -168,4 +168,33 @@ await SendRequestAsync(r => r Assert.Single(stack.References); Assert.Contains(testUrl, stack.References); } + + [Fact] + public async Task GetAll_WithDateRangeFilter_ReturnsOnlyMatchingStacks() + { + // Arrange + var now = TimeProvider.GetUtcNow(); + var (stacks, _) = await CreateDataAsync(d => + { + d.Event().TestProject().Date(now.AddDays(-1)); + d.Event().TestProject().Date(now.AddDays(-3)); + }); + + Assert.Equal(2, stacks.Count); + var recentStack = stacks.Single(s => s.LastOccurrence >= now.AddDays(-2).UtcDateTime); + var oldStack = stacks.Single(s => s.LastOccurrence < now.AddDays(-2).UtcDateTime); + + // Act + var result = await SendRequestAsAsync>(r => r + .AsGlobalAdminUser() + .AppendPath("stacks") + .QueryString("time", "[now-2d TO now]") + .StatusCodeShouldBeOk() + ); + + // Assert + Assert.NotNull(result); + Assert.Contains(result, s => String.Equals(s.Id, recentStack.Id, StringComparison.Ordinal)); + Assert.DoesNotContain(result, s => String.Equals(s.Id, oldStack.Id, StringComparison.Ordinal)); + } } diff --git a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs index f3a1565100..9d9d6f51f5 100644 --- a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs @@ -1,6 +1,7 @@ using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; +using Exceptionless.Core.Services; using Exceptionless.Serializer; using Foundatio.Repositories.Extensions; using Foundatio.Serializer; @@ -12,8 +13,11 @@ namespace Exceptionless.Tests.Serializer; public class SerializerTests : TestWithServices { + private readonly ITextSerializer _serializer; + public SerializerTests(ITestOutputHelper output) : base(output) { + _serializer = GetService(); } [Fact] @@ -115,11 +119,10 @@ public void CanDeserializeWebHook() Version = WebHook.KnownVersions.Version2 }; - var serializer = GetService(); - string json = serializer.SerializeToString(hook); + string json = _serializer.SerializeToString(hook); Assert.Equal("{\"id\":\"test\",\"event_types\":[\"NewError\"],\"is_enabled\":true,\"version\":\"v2\",\"created_utc\":\"0001-01-01T00:00:00\"}", json); - var model = serializer.Deserialize(json); + var model = _serializer.Deserialize(json); Assert.Equal(hook.Id, model.Id); Assert.Equal(hook.EventTypes, model.EventTypes); Assert.Equal(hook.Version, model.Version); @@ -130,14 +133,205 @@ public void CanDeserializeProject() { string json = "{\"last_event_date_utc\":\"2020-10-18T20:54:04.3457274+01:00\", \"created_utc\":\"0001-01-01T00:00:00\",\"updated_utc\":\"2020-09-21T04:41:32.7458321Z\"}"; - var serializer = GetService(); - var model = serializer.Deserialize(json); + var model = _serializer.Deserialize(json); Assert.NotNull(model); Assert.NotNull(model.LastEventDateUtc); Assert.NotEqual(DateTime.MinValue, model.LastEventDateUtc); Assert.Equal(DateTime.MinValue, model.CreatedUtc); Assert.NotEqual(DateTime.MinValue, model.UpdatedUtc); } + + [Fact] + public void SerializeToString_ValueTupleOfStrings_SerializesFields() + { + // Arrange — with IncludeFields=true, ValueTuple fields are serialized. + // Compile-time names (OrganizationId, etc.) are erased at runtime; fields are always Item1/Item2/Item3. + // LowerCaseUnderscoreNamingPolicy converts Item1 → item1, Item2 → item2, Item3 → item3. + var tuple = (OrganizationId: "org1", ProjectId: "proj1", StackId: "stack1"); + + // Act + string json = _serializer.SerializeToString(tuple); + + // Assert + Assert.Equal("{\"item1\":\"org1\",\"item2\":\"proj1\",\"item3\":\"stack1\"}", json); + } + + [Fact] + public void SerializeToString_ValueTupleOfInts_SerializesFields() + { + // Arrange + var tuple = (A: 1, B: 2, C: 3); + + // Act + string json = _serializer.SerializeToString(tuple); + + // Assert + Assert.Equal("{\"item1\":1,\"item2\":2,\"item3\":3}", json); + } + + [Fact] + public void SerializeToString_ValueTupleOfMixed_SerializesFields() + { + // Arrange + var tuple = (Name: "test", Count: 42, Active: true); + + // Act + string json = _serializer.SerializeToString(tuple); + + // Assert + Assert.Equal("{\"item1\":\"test\",\"item2\":42,\"item3\":true}", json); + } + + [Fact] + public void SerializeToString_TwoDistinctValueTuples_ProduceDifferentJson() + { + // Arrange + var tuple1 = (OrgId: "org1", ProjId: "proj1", StackId: "stack1"); + var tuple2 = (OrgId: "org2", ProjId: "proj2", StackId: "stack2"); + + // Act + string json1 = _serializer.SerializeToString(tuple1); + string json2 = _serializer.SerializeToString(tuple2); + + // Assert — distinct tuples produce distinct JSON (no Redis sorted-set collision) + Assert.Equal("{\"item1\":\"org1\",\"item2\":\"proj1\",\"item3\":\"stack1\"}", json1); + Assert.Equal("{\"item1\":\"org2\",\"item2\":\"proj2\",\"item3\":\"stack2\"}", json2); + } + + [Fact] + public void SerializeToString_ValueTuple_UsesGenericFieldNames() + { + // Arrange — ValueTuple field names are erased at runtime; fields are always Item1/Item2/Item3 + // regardless of the compile-time names. Records preserve named properties (organization_id, etc.), + // making them the correct choice for serialized cache keys. + var tuple = (OrganizationId: "org1", ProjectId: "proj1", StackId: "stack1"); + + // Act + string json = _serializer.SerializeToString(tuple); + + // Assert — item1/item2/item3, NOT organization_id/project_id/stack_id + Assert.Equal("{\"item1\":\"org1\",\"item2\":\"proj1\",\"item3\":\"stack1\"}", json); + } + + [Fact] + public void SerializeToString_StructWithFields_SerializesFields() + { + // Arrange — with IncludeFields=true, structs with public fields serialize correctly + var value = new FieldOnlyStruct { Key = "abc", Value = 42 }; + + // Act + string json = _serializer.SerializeToString(value); + + // Assert + Assert.Equal("{\"key\":\"abc\",\"value\":42}", json); + } + + [Fact] + public void SerializeToString_StackUsageKey_RoundtripsCorrectly() + { + // Arrange + var key = new StackUsageKey("org1", "proj1", "stack1"); + + // Act + string json = _serializer.SerializeToString(key); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.Equal("{\"organization_id\":\"org1\",\"project_id\":\"proj1\",\"stack_id\":\"stack1\"}", json); + Assert.Equal(key, deserialized); + } + + [Fact] + public void SerializeToString_DistinctStackUsageKeys_ProduceDifferentJson() + { + // Arrange + var key1 = new StackUsageKey("org1", "proj1", "stack1"); + var key2 = new StackUsageKey("org2", "proj2", "stack2"); + + // Act + string json1 = _serializer.SerializeToString(key1); + string json2 = _serializer.SerializeToString(key2); + + // Assert + Assert.NotEqual(json1, json2); + } + + [Fact] + public void SerializeToString_RecordStruct_RoundtripsCorrectly() + { + // Arrange + var value = new SampleRecordStruct("key1", 42); + + // Act + string json = _serializer.SerializeToString(value); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.Equal("{\"key\":\"key1\",\"value\":42}", json); + Assert.Equal(value, deserialized); + } + + [Fact] + public void SerializeToString_ClassWithProperties_RoundtripsCorrectly() + { + // Arrange + var value = new SampleClass { Name = "test", Count = 7 }; + + // Act + string json = _serializer.SerializeToString(value); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal("{\"name\":\"test\",\"count\":7}", json); + Assert.Equal("test", deserialized.Name); + Assert.Equal(7, deserialized.Count); + } + + [Fact] + public void SerializeToString_PrimitiveTypes_RoundtripCorrectly() + { + // Act & Assert — each primitive type verified inline + Assert.Equal("42", _serializer.SerializeToString(42)); + Assert.Equal("42", _serializer.Deserialize("42").ToString()); + + Assert.Equal("99", _serializer.SerializeToString(99L)); + Assert.Equal(99L, _serializer.Deserialize("99")); + + Assert.Equal("true", _serializer.SerializeToString(true)); + Assert.True(_serializer.Deserialize("true")); + + string roundtripped = _serializer.Deserialize(_serializer.SerializeToString("hello")); + Assert.Equal("hello", roundtripped); + } + + [Fact] + public void SerializeToString_DateTime_RoundtripsCorrectly() + { + // Arrange + var dt = new DateTime(2026, 2, 22, 12, 0, 0, DateTimeKind.Utc); + + // Act + string json = _serializer.SerializeToString(dt); + var deserialized = _serializer.Deserialize(json); + + // Assert + Assert.Equal(dt, deserialized); + } + + public struct FieldOnlyStruct + { + public string Key; + public int Value; + } + + public record struct SampleRecordStruct(string Key, int Value); + + public class SampleClass + { + public string Name { get; set; } = ""; + public int Count { get; set; } + } } public record SomeModel diff --git a/tests/Exceptionless.Tests/Services/StackServiceTests.cs b/tests/Exceptionless.Tests/Services/StackServiceTests.cs index db1e001618..a20a959201 100644 --- a/tests/Exceptionless.Tests/Services/StackServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/StackServiceTests.cs @@ -39,7 +39,7 @@ public async Task IncrementUsage_OnlyChangeCache() Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(0, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - var occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(StackService.GetStackOccurrenceSetCacheKey()); + var occurrenceSet = await _cache.GetListAsync(StackService.GetStackOccurrenceSetCacheKey()); Assert.True(occurrenceSet.IsNull || !occurrenceSet.HasValue || occurrenceSet.Value.Count == 0); var firstUtcNow = DateTime.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); @@ -56,7 +56,7 @@ public async Task IncrementUsage_OnlyChangeCache() Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(1, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(StackService.GetStackOccurrenceSetCacheKey()); + occurrenceSet = await _cache.GetListAsync(StackService.GetStackOccurrenceSetCacheKey()); Assert.Single(occurrenceSet.Value); var secondUtcNow = DateTime.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); @@ -66,7 +66,7 @@ public async Task IncrementUsage_OnlyChangeCache() Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); Assert.Equal(secondUtcNow, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(3, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); - occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(StackService.GetStackOccurrenceSetCacheKey()); + occurrenceSet = await _cache.GetListAsync(StackService.GetStackOccurrenceSetCacheKey()); Assert.Single(occurrenceSet.Value); } @@ -105,7 +105,7 @@ public async Task IncrementUsageConcurrently() Assert.Equal(maxOccurrenceDate, await _cache.GetUnixTimeMillisecondsAsync(StackService.GetStackOccurrenceMaxDateCacheKey(stack2.Id))); Assert.Equal(200, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack2.Id), 0)); - var occurrenceSet = await _cache.GetListAsync<(string OrganizationId, string ProjectId, string StackId)>(StackService.GetStackOccurrenceSetCacheKey()); + var occurrenceSet = await _cache.GetListAsync(StackService.GetStackOccurrenceSetCacheKey()); Assert.Equal(2, occurrenceSet.Value.Count); async Task IncrementUsageBatch() From ddc30912d8141f11fb94fc4843b8959c2cb95c4b Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 23 Feb 2026 08:14:14 -0600 Subject: [PATCH 07/29] Optimizes daily summary job and email sending Improves the daily summary job by removing an unnecessary index call, streamlining stack retrieval. Enhances email sending robustness by preventing cancellation exceptions from being caught during health checks, ensuring more accurate error reporting. --- src/Exceptionless.Core/Jobs/DailySummaryJob.cs | 3 +-- src/Exceptionless.Insulation/Mail/MailKitMailSender.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index d06ec7dd5d..8ccdd02eab 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -184,8 +184,7 @@ private async Task SendSummaryNotificationAsync(Project project, SummaryNo q => q.AppFilter(sf) .FilterExpression(filter) .SortExpression("-first") - .DateRange(data.UtcStartTime, data.UtcEndTime, "first") - .Index(data.UtcStartTime, data.UtcEndTime), + .DateRange(data.UtcStartTime, data.UtcEndTime, "first"), o => o.PageLimit(3)); newest = stackResults.Documents; diff --git a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs index a1851f7a93..4aaff40812 100644 --- a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs +++ b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs @@ -139,7 +139,7 @@ public async Task CheckHealthAsync(HealthCheckContext context await client.DisconnectAsync(true, cancellationToken); _lastSuccessfulConnection = _timeProvider.GetUtcNow().UtcDateTime; } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException) { return HealthCheckResult.Unhealthy("Email Not Working.", ex); } From f112cd703db6d5babe345b77c0892d137a6b3844 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 24 Feb 2026 08:24:44 -0600 Subject: [PATCH 08/29] Fixes stack stats with a work item job (#2129) * Fixes stack stats with a work item job Addresses an issue where stack statistics may become inaccurate due to a bug. Introduces a work item handler and job to recalculate stack stats based on event data within a specified time range. An admin endpoint has been added to queue the work item. Adds tests to ensure the stats are correctly repaired. * PR feedback * increased batch size * Improves stack event stats calculation Refactors the stack event stats calculation to use aggregations, resulting in significantly improved performance. Removes unnecessary data loading, and simplifies the stats computation logic. * Fixes stack stats job logic Ensures the stack stats job correctly processes and updates stack statistics by adjusting the UTC timestamp handling and fixing validation issues. Improves the accuracy of the fix stack stats job and updates related tests to reflect the changes and ensure they account for all edge cases. * Refactors stack stats fix job Refactors the stack stats fix job to improve performance and correctness. The job now processes stacks within a specific organization or all organizations with events in the time window. It also uses aggregations to calculate stack event stats more efficiently and avoid unnecessary stack updates. The previous implementation was inefficient and could lead to incorrect stack stats. * Renames `Organization` to `OrganizationId` Updates the `FixStackStatsWorkItem` model and related code to use `OrganizationId` instead of `Organization` for clarity and consistency with the rest of the codebase. This change ensures that the correct organization is targeted when fixing stack statistics. * Ensures stack change notifications are sent Refactors stack patching to ensure that notifications are always sent after a stack is updated. This addresses an issue where the stack usage job was not triggering notifications due to a conditional check. * Replaces DocumentNotFoundException and handles missing stacks. Replaces the custom DocumentNotFoundException with the one provided by Foundatio. Handles potential DocumentNotFoundException when patching a stack, preventing unexpected errors if a stack has been deleted. * Handles missing stacks during event counting Ensures that stack event counter updates succeed even if the stack document does not exist. This prevents issues in background jobs that process event counts asynchronously, where the stack may have been deleted between event processing and the update operation. --- src/Exceptionless.Core/Bootstrapper.cs | 9 +- src/Exceptionless.Core/Jobs/EventPostsJob.cs | 2 +- .../Jobs/EventUserDescriptionsJob.cs | 2 +- .../FixStackStatsWorkItemHandler.cs | 155 ++++++++++ .../Models/WorkItems/FixStackStatsWorkItem.cs | 14 + .../Exceptions/DocumentNotFoundException.cs | 22 -- .../Interfaces/IStackRepository.cs | 1 + .../Repositories/StackRepository.cs | 83 +++++- .../Controllers/AdminController.cs | 23 +- .../Controllers/AdminControllerTests.cs | 175 ++++++++++++ .../Jobs/FixStackStatsJobTests.cs | 270 ++++++++++++++++++ .../Repositories/EventRepositoryTests.cs | 1 - .../Repositories/StackRepositoryTests.cs | 57 +++- tests/http/admin.http | 8 + 14 files changed, 773 insertions(+), 49 deletions(-) create mode 100644 src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs create mode 100644 src/Exceptionless.Core/Models/WorkItems/FixStackStatsWorkItem.cs delete mode 100644 src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs create mode 100644 tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs create mode 100644 tests/Exceptionless.Tests/Jobs/FixStackStatsJobTests.cs diff --git a/src/Exceptionless.Core/Bootstrapper.cs b/src/Exceptionless.Core/Bootstrapper.cs index 87e56cd3cb..c2016d7f70 100644 --- a/src/Exceptionless.Core/Bootstrapper.cs +++ b/src/Exceptionless.Core/Bootstrapper.cs @@ -102,14 +102,15 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO services.AddSingleton(s => { var handlers = new WorkItemHandlers(); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); handlers.Register(s.GetRequiredService); + handlers.Register(s.GetRequiredService); handlers.Register(s.GetRequiredService); handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); - handlers.Register(s.GetRequiredService); handlers.Register(s.GetRequiredService); return handlers; }); diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 19027d53cb..605ee3ee6c 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -5,7 +5,7 @@ using Exceptionless.Core.Plugins.EventParser; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Exceptionless.Core.Repositories.Base; +using Foundatio.Repositories.Exceptions; using Exceptionless.Core.Services; using Exceptionless.Core.Validation; using FluentValidation; diff --git a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs index c4c783dd7c..8b4514b6b3 100644 --- a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs @@ -1,7 +1,7 @@ using Exceptionless.Core.Models.Data; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Exceptionless.Core.Repositories.Base; +using Foundatio.Repositories.Exceptions; using Foundatio.Jobs; using Foundatio.Queues; using Foundatio.Repositories.Extensions; diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs new file mode 100644 index 0000000000..a40ad6c746 --- /dev/null +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs @@ -0,0 +1,155 @@ +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.WorkItems; +using Exceptionless.Core.Repositories; +using Exceptionless.DateTimeExtensions; +using Foundatio.Jobs; +using Foundatio.Lock; +using Foundatio.Repositories; +using Foundatio.Repositories.Models; +using Microsoft.Extensions.Logging; + +namespace Exceptionless.Core.Jobs.WorkItemHandlers; + +public class FixStackStatsWorkItemHandler : WorkItemHandlerBase +{ + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly ILockProvider _lockProvider; + private readonly TimeProvider _timeProvider; + + public FixStackStatsWorkItemHandler(IStackRepository stackRepository, IEventRepository eventRepository, ILockProvider lockProvider, TimeProvider timeProvider, ILoggerFactory loggerFactory) + : base(loggerFactory) + { + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _lockProvider = lockProvider; + _timeProvider = timeProvider; + } + + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) + { + return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), cancellationToken); + } + + public override async Task HandleItemAsync(WorkItemContext context) + { + var wi = context.GetData(); + var utcEnd = wi.UtcEnd ?? _timeProvider.GetUtcNow().UtcDateTime; + + Log.LogInformation("Starting stack stats repair for {UtcStart:O} to {UtcEnd:O}. OrganizationId={Organization}", wi.UtcStart, utcEnd, wi.OrganizationId); + await context.ReportProgressAsync(0, $"Starting stack stats repair for window {wi.UtcStart:O} – {utcEnd:O}"); + + var organizationIds = await GetOrganizationIdsAsync(wi, utcEnd); + Log.LogInformation("Found {OrganizationCount} organizations to process", organizationIds.Count); + + int repaired = 0; + int skipped = 0; + + for (int index = 0; index < organizationIds.Count; index++) + { + if (context.CancellationToken.IsCancellationRequested) + break; + + var (organizationRepaired, organizationSkipped) = await ProcessOrganizationAsync(context, organizationIds[index], wi.UtcStart, utcEnd); + repaired += organizationRepaired; + skipped += organizationSkipped; + + int percentage = (int)Math.Min(99, (index + 1) * 100.0 / organizationIds.Count); + await context.ReportProgressAsync(percentage, $"Organization {index + 1}/{organizationIds.Count} ({percentage}%): repaired {repaired}, skipped {skipped}"); + } + + Log.LogInformation("Stack stats repair complete: Repaired={Repaired} Skipped={Skipped}", repaired, skipped); + await context.ReportProgressAsync(100, $"Done. Repaired {repaired} stacks, skipped={skipped}."); + } + + private async Task> GetOrganizationIdsAsync(FixStackStatsWorkItem wi, DateTime utcEnd) + { + if (wi.OrganizationId is not null) + return [wi.OrganizationId]; + + var countResult = await _eventRepository.CountAsync(q => q + .DateRange(wi.UtcStart, utcEnd, (PersistentEvent e) => e.Date) + .Index(wi.UtcStart, utcEnd) + .AggregationsExpression("terms:(organization_id~65536)")); + + return countResult.Aggregations.Terms("terms_organization_id")?.Buckets + .Select(b => b.Key) + .ToList() ?? []; + } + + private async Task<(int Repaired, int Skipped)> ProcessOrganizationAsync(WorkItemContext context, string organizationId, DateTime utcStart, DateTime utcEnd) + { + using var _ = Log.BeginScope(new ExceptionlessState().Organization(organizationId)); + await context.RenewLockAsync(); + + var countResult = await _eventRepository.CountAsync(q => q + .Organization(organizationId) + .DateRange(utcStart, utcEnd, (PersistentEvent e) => e.Date) + .Index(utcStart, utcEnd) + .AggregationsExpression("terms:(stack_id~65536 min:date max:date)")); + + var stackBuckets = countResult.Aggregations.Terms("terms_stack_id")?.Buckets ?? []; + if (stackBuckets.Count is 0) + return (0, 0); + + var statsByStackId = new Dictionary(stackBuckets.Count); + foreach (var bucket in stackBuckets) + { + var firstOccurrence = bucket.Aggregations.Min("min_date")?.Value; + var lastOccurrence = bucket.Aggregations.Max("max_date")?.Value; + if (firstOccurrence is null || lastOccurrence is null || bucket.Total is null) + continue; + + statsByStackId[bucket.Key] = new StackEventStats(firstOccurrence.Value, lastOccurrence.Value, bucket.Total.Value); + } + + int repaired = 0; + int skipped = 0; + + foreach (string[] batch in statsByStackId.Keys.Chunk(100)) + { + if (context.CancellationToken.IsCancellationRequested) + break; + + await context.RenewLockAsync(); + + var stacks = await _stackRepository.GetByIdsAsync(batch); + foreach (var stack in stacks) + { + if (!statsByStackId.TryGetValue(stack.Id, out var stats)) + { + skipped++; + continue; + } + + bool shouldUpdateFirst = stack.FirstOccurrence.IsAfter(stats.FirstOccurrence); + bool shouldUpdateLast = stack.LastOccurrence.IsBefore(stats.LastOccurrence); + bool shouldUpdateTotal = stats.TotalOccurrences > stack.TotalOccurrences; + if (!shouldUpdateFirst && !shouldUpdateLast && !shouldUpdateTotal) + { + skipped++; + continue; + } + + var newFirst = shouldUpdateFirst ? stats.FirstOccurrence : stack.FirstOccurrence; + var newLast = shouldUpdateLast ? stats.LastOccurrence : stack.LastOccurrence; + long newTotal = shouldUpdateTotal ? stats.TotalOccurrences : stack.TotalOccurrences; + + Log.LogInformation( + "Repairing stack {StackId}: first={OldFirst:O}->{NewFirst:O} last={OldLast:O}->{NewLast:O} total={OldTotal}->{NewTotal}", + stack.Id, + stack.FirstOccurrence, newFirst, + stack.LastOccurrence, newLast, + stack.TotalOccurrences, newTotal); + + await _stackRepository.SetEventCounterAsync(stack.Id, newFirst, newLast, newTotal, sendNotifications: false); + repaired++; + } + } + + Log.LogDebug("Processed organization: Repaired={Repaired} Skipped={Skipped}", repaired, skipped); + return (repaired, skipped); + } +} + +internal record StackEventStats(DateTime FirstOccurrence, DateTime LastOccurrence, long TotalOccurrences); diff --git a/src/Exceptionless.Core/Models/WorkItems/FixStackStatsWorkItem.cs b/src/Exceptionless.Core/Models/WorkItems/FixStackStatsWorkItem.cs new file mode 100644 index 0000000000..0585d33536 --- /dev/null +++ b/src/Exceptionless.Core/Models/WorkItems/FixStackStatsWorkItem.cs @@ -0,0 +1,14 @@ +namespace Exceptionless.Core.Models.WorkItems; + +public record FixStackStatsWorkItem +{ + public DateTime UtcStart { get; init; } + + public DateTime? UtcEnd { get; init; } + + /// + /// When set, only stacks belonging to this organization are repaired. + /// When null, all organizations with events in the time window are processed. + /// + public string? OrganizationId { get; init; } +} diff --git a/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs b/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs deleted file mode 100644 index ad669b9621..0000000000 --- a/src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Exceptionless.Core.Repositories.Base; - -public class DocumentNotFoundException : ApplicationException -{ - public DocumentNotFoundException(string id, string? message = null) : base(message) - { - Id = id; - } - - public string Id { get; init; } - - public override string ToString() - { - if (!String.IsNullOrEmpty(Message)) - return Message; - - if (!String.IsNullOrEmpty(Id)) - return $"Document \"{Id}\" could not be found"; - - return base.ToString(); - } -} diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs index 9ec7693745..13199d28c6 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IStackRepository.cs @@ -11,6 +11,7 @@ public interface IStackRepository : IRepositoryOwnedByOrganizationAndProject> GetExpiredSnoozedStatuses(DateTime utcNow, CommandOptionsDescriptor? options = null); Task MarkAsRegressedAsync(string stackId); Task IncrementEventCounterAsync(string organizationId, string projectId, string stackId, DateTime minOccurrenceDateUtc, DateTime maxOccurrenceDateUtc, int count, bool sendNotifications = true); + Task SetEventCounterAsync(string stackId, DateTime firstOccurrenceUtc, DateTime lastOccurrenceUtc, long totalOccurrences, bool sendNotifications = true); Task> GetStacksForCleanupAsync(string organizationId, DateTime cutoff); Task> GetSoftDeleted(); Task SoftDeleteByProjectIdAsync(string organizationId, string projectId); diff --git a/src/Exceptionless.Core/Repositories/StackRepository.cs b/src/Exceptionless.Core/Repositories/StackRepository.cs index 3b79e84296..27bb6bc6f3 100644 --- a/src/Exceptionless.Core/Repositories/StackRepository.cs +++ b/src/Exceptionless.Core/Repositories/StackRepository.cs @@ -3,9 +3,9 @@ using Exceptionless.Core.Repositories.Configuration; using FluentValidation; using Foundatio.Repositories; +using Foundatio.Repositories.Exceptions; using Foundatio.Repositories.Models; using Foundatio.Repositories.Options; -using Microsoft.Extensions.Logging; using Nest; namespace Exceptionless.Core.Repositories; @@ -63,37 +63,90 @@ Instant parseDate(def dt) { if (ctx._source.total_occurrences == 0 || parseDate(ctx._source.first_occurrence).isAfter(parseDate(params.minOccurrenceDateUtc))) { ctx._source.first_occurrence = params.minOccurrenceDateUtc; } + if (parseDate(ctx._source.last_occurrence).isBefore(parseDate(params.maxOccurrenceDateUtc))) { ctx._source.last_occurrence = params.maxOccurrenceDateUtc; } + if (parseDate(ctx._source.updated_utc).isBefore(parseDate(params.updatedUtc))) { ctx._source.updated_utc = params.updatedUtc; } + ctx._source.total_occurrences += params.count;"; - var request = new UpdateRequest(ElasticIndex.GetIndex(stackId), stackId) + var operation = new ScriptPatch(script.TrimScript()) { - Script = new InlineScript(script.TrimScript()) + Params = new Dictionary(4) { - Params = new Dictionary(3) { - { "minOccurrenceDateUtc", minOccurrenceDateUtc }, - { "maxOccurrenceDateUtc", maxOccurrenceDateUtc }, - { "count", count }, - { "updatedUtc", _timeProvider.GetUtcNow().UtcDateTime } - } + { "minOccurrenceDateUtc", minOccurrenceDateUtc }, + { "maxOccurrenceDateUtc", maxOccurrenceDateUtc }, + { "count", count }, + { "updatedUtc", _timeProvider.GetUtcNow().UtcDateTime } } }; - var result = await _client.UpdateAsync(request); - if (!result.IsValid) + try + { + await PatchAsync(stackId, operation, o => o.Notifications(false)); + } + catch (DocumentNotFoundException) { - _logger.LogError(result.OriginalException, "Error occurred incrementing total event occurrences on stack {Stack}. Error: {Message}", stackId, result.ServerError?.Error); - return result.ServerError?.Status == 404; + return true; } - await Cache.RemoveAsync(stackId); if (sendNotifications) - await PublishMessageAsync(CreateEntityChanged(ChangeType.Saved, organizationId, projectId, null, stackId), TimeSpan.FromSeconds(1.5)); + await PublishMessageAsync(CreateEntityChanged(ChangeType.Saved, organizationId, projectId, null, stackId)); + + return true; + } + + public async Task SetEventCounterAsync(string stackId, DateTime firstOccurrenceUtc, DateTime lastOccurrenceUtc, long totalOccurrences, bool sendNotifications = true) + { + const string script = @" +Instant parseDate(def dt) { + if (dt != null) { + try { + return Instant.parse(dt); + } catch(DateTimeParseException e) {} + } + return Instant.MIN; +} + +if (ctx._source.total_occurrences == null || ctx._source.total_occurrences < params.totalOccurrences) { + ctx._source.total_occurrences = params.totalOccurrences; +} + +if (parseDate(ctx._source.first_occurrence).isAfter(parseDate(params.firstOccurrenceUtc))) { + ctx._source.first_occurrence = params.firstOccurrenceUtc; +} + +if (parseDate(ctx._source.last_occurrence).isBefore(parseDate(params.lastOccurrenceUtc))) { + ctx._source.last_occurrence = params.lastOccurrenceUtc; +} + +if (parseDate(ctx._source.updated_utc).isBefore(parseDate(params.updatedUtc))) { + ctx._source.updated_utc = params.updatedUtc; +}"; + + var operation = new ScriptPatch(script.TrimScript()) + { + Params = new Dictionary(4) + { + { "firstOccurrenceUtc", firstOccurrenceUtc }, + { "lastOccurrenceUtc", lastOccurrenceUtc }, + { "totalOccurrences", totalOccurrences }, + { "updatedUtc", _timeProvider.GetUtcNow().UtcDateTime } + } + }; + + try + { + await PatchAsync(stackId, operation, o => o.Notifications(sendNotifications)); + } + catch (DocumentNotFoundException) + { + return true; + } return true; } diff --git a/src/Exceptionless.Web/Controllers/AdminController.cs b/src/Exceptionless.Web/Controllers/AdminController.cs index e3c7b2aef5..dfdef0339a 100644 --- a/src/Exceptionless.Web/Controllers/AdminController.cs +++ b/src/Exceptionless.Web/Controllers/AdminController.cs @@ -9,6 +9,7 @@ using Exceptionless.Core.Repositories; using Exceptionless.Core.Repositories.Configuration; using Exceptionless.Core.Utility; +using Exceptionless.DateTimeExtensions; using Exceptionless.Web.Extensions; using Foundatio.Jobs; using Foundatio.Messaging; @@ -155,8 +156,11 @@ public async Task RequeueAsync(string? path = null, bool archive } [HttpGet("maintenance/{name:minlength(1)}")] - public async Task RunJobAsync(string name) + public async Task RunJobAsync(string name, DateTime? utcStart = null, DateTime? utcEnd = null, string? organizationId = null) { + if (!ModelState.IsValid) + return ValidationProblem(ModelState); + switch (name.ToLowerInvariant()) { case "indexes": @@ -184,6 +188,23 @@ public async Task RunJobAsync(string name) case "reset-verify-email-address-token-and-expiration": await _workItemQueue.EnqueueAsync(new UserMaintenanceWorkItem { ResetVerifyEmailAddressToken = true }); break; + case "fix-stack-stats": + var defaultUtcStart = new DateTime(2026, 2, 10, 0, 0, 0, DateTimeKind.Utc); + var effectiveUtcStart = utcStart ?? defaultUtcStart; + + if (utcEnd.HasValue && utcEnd.Value.IsBefore(effectiveUtcStart)) + { + ModelState.AddModelError(nameof(utcEnd), "utcEnd must be greater than or equal to utcStart."); + return ValidationProblem(ModelState); + } + + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = effectiveUtcStart, + UtcEnd = utcEnd, + OrganizationId = organizationId + }); + break; default: return NotFound(); } diff --git a/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs new file mode 100644 index 0000000000..b03ae571ce --- /dev/null +++ b/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs @@ -0,0 +1,175 @@ +using Exceptionless.Core.Models; +using Exceptionless.Core.Repositories; +using Exceptionless.Core.Utility; +using Exceptionless.Tests.Extensions; +using Exceptionless.Tests.Utility; +using Foundatio.Jobs; +using Foundatio.Queues; +using Foundatio.Repositories; +using Xunit; + +namespace Exceptionless.Tests.Controllers; + +public class AdminControllerTests : IntegrationTestsBase +{ + private readonly WorkItemJob _workItemJob; + private readonly IQueue _workItemQueue; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly StackData _stackData; + private readonly EventData _eventData; + + public AdminControllerTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) + { + _workItemJob = GetService(); + _workItemQueue = GetService>(); + _stackRepository = GetService(); + _eventRepository = GetService(); + _stackData = GetService(); + _eventData = GetService(); + } + + protected override async Task ResetDataAsync() + { + await base.ResetDataAsync(); + var service = GetService(); + await service.CreateDataAsync(); + } + + [Fact] + public async Task RunJobAsync_WhenFixStackStatsWithExplicitUtcWindow_ShouldRepairStatsEndToEnd() + { + // Arrange + TimeProvider.SetUtcNow(new DateTime(2026, 2, 15, 12, 0, 0, DateTimeKind.Utc)); + var stack = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2026, 2, 14, 0, 0, 0, TimeSpan.Zero)); + + // Act + await SendRequestAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "maintenance", "fix-stack-stats") + .QueryString("utcStart", "2026-02-10T00:00:00Z") + .QueryString("utcEnd", "2026-02-23T00:00:00Z") + .StatusCodeShouldBeOk()); + + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + + // Assert + Assert.NotNull(stack); + Assert.Equal(1, stack.TotalOccurrences); + Assert.Equal(new DateTime(2026, 2, 14, 0, 0, 0, DateTimeKind.Utc), stack.FirstOccurrence); + Assert.Equal(new DateTime(2026, 2, 14, 0, 0, 0, DateTimeKind.Utc), stack.LastOccurrence); + } + + [Fact] + public async Task RunJobAsync_WhenFixStackStatsWindowIsOmitted_ShouldUseDefaultStartAndCurrentUtcEnd() + { + // Arrange + TimeProvider.SetUtcNow(new DateTime(2026, 2, 5, 12, 0, 0, DateTimeKind.Utc)); + var beforeWindow = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2026, 2, 5, 12, 0, 0, TimeSpan.Zero)); + + TimeProvider.SetUtcNow(new DateTime(2026, 2, 15, 12, 0, 0, DateTimeKind.Utc)); + var inWindow = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2026, 2, 15, 12, 0, 0, TimeSpan.Zero)); + + // Act + await SendRequestAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "maintenance", "fix-stack-stats") + .StatusCodeShouldBeOk()); + + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + beforeWindow = await _stackRepository.GetByIdAsync(beforeWindow.Id); + inWindow = await _stackRepository.GetByIdAsync(inWindow.Id); + + // Assert + Assert.NotNull(beforeWindow); + Assert.NotNull(inWindow); + Assert.Equal(0, beforeWindow.TotalOccurrences); + Assert.Equal(1, inWindow.TotalOccurrences); + } + + [Fact] + public async Task RunJobAsync_WhenFixStackStatsUsesOffsetUtcTimestamp_ShouldAcceptModelBindingValue() + { + // Arrange + TimeProvider.SetUtcNow(new DateTime(2026, 2, 15, 12, 0, 0, DateTimeKind.Utc)); + var stack = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2026, 2, 14, 0, 0, 0, TimeSpan.Zero)); + + // Act + await SendRequestAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "maintenance", "fix-stack-stats") + .QueryString("utcStart", "2026-02-10T00:00:00+00:00") + .QueryString("utcEnd", "2026-02-23T00:00:00+00:00") + .StatusCodeShouldBeOk()); + + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + var stats = await _workItemQueue.GetQueueStatsAsync(); + + // Assert + Assert.NotNull(stack); + Assert.Equal(1, stack.TotalOccurrences); + Assert.Equal(1, stats.Enqueued); + } + + [Fact] + public async Task RunJobAsync_WhenFixStackStatsEndDateIsBeforeStartDate_ShouldReturnUnprocessableEntity() + { + // Arrange + var response = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "maintenance", "fix-stack-stats") + .QueryString("utcStart", "2026-02-20T00:00:00Z") + .QueryString("utcEnd", "2026-02-10T00:00:00Z") + .StatusCodeShouldBeUnprocessableEntity()); + + // Act + var stats = await _workItemQueue.GetQueueStatsAsync(); + + // Assert + Assert.NotNull(response); + Assert.True(response.Errors.ContainsKey("utc_end")); + Assert.Equal(0, stats.Enqueued); + } + + [Fact] + public async Task RunJobAsync_WhenFixStackStatsStartDateIsInvalid_ShouldReturnBadRequestAndNotQueueWorkItem() + { + // Arrange + await SendRequestAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "maintenance", "fix-stack-stats") + .QueryString("utcStart", "not-a-dateZ") + .StatusCodeShouldBeBadRequest()); + + // Act + var stats = await _workItemQueue.GetQueueStatsAsync(); + + // Assert + Assert.Equal(0, stats.Enqueued); + } + + private async Task CreateCorruptedStackWithEventAsync(DateTimeOffset occurrenceDate) + { + var utcOccurrenceDate = occurrenceDate.UtcDateTime; + var stack = _stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + totalOccurrences: 0, + utcFirstOccurrence: utcOccurrenceDate.AddDays(1), + utcLastOccurrence: utcOccurrenceDate.AddDays(-1)); + + stack = await _stackRepository.AddAsync(stack, o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync( + [_eventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, occurrenceDate: occurrenceDate)], + o => o.ImmediateConsistency()); + + await RefreshDataAsync(); + return stack; + } +} diff --git a/tests/Exceptionless.Tests/Jobs/FixStackStatsJobTests.cs b/tests/Exceptionless.Tests/Jobs/FixStackStatsJobTests.cs new file mode 100644 index 0000000000..fd58d41944 --- /dev/null +++ b/tests/Exceptionless.Tests/Jobs/FixStackStatsJobTests.cs @@ -0,0 +1,270 @@ +using Exceptionless.Core.Models.WorkItems; +using Exceptionless.Core.Repositories; +using Exceptionless.Tests.Utility; +using Foundatio.Jobs; +using Foundatio.Queues; +using Foundatio.Repositories; +using Xunit; + +namespace Exceptionless.Tests.Jobs; + +public class FixStackStatsJobTests : IntegrationTestsBase +{ + private readonly WorkItemJob _workItemJob; + private readonly IQueue _workItemQueue; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly StackData _stackData; + private readonly EventData _eventData; + + private static readonly DateTime DefaultWindowStart = new(2026, 2, 10, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTime DefaultWindowEnd = new(2026, 2, 23, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTime InWindowDate = new(2026, 2, 15, 12, 0, 0, DateTimeKind.Utc); + + public FixStackStatsJobTests(ITestOutputHelper output, AppWebHostFactory factory) : base(output, factory) + { + _workItemJob = GetService(); + _workItemQueue = GetService>(); + _stackRepository = GetService(); + _eventRepository = GetService(); + _stackData = GetService(); + _eventData = GetService(); + } + + [Fact] + public async Task RunUntilEmptyAsync_WhenStackIsInBugWindowWithCorruptCounters_ShouldRebuildStackStatsFromEvents() + { + // Arrange + // Simulate the corrupted state: stack created in bug window with TotalOccurrences = 0 + TimeProvider.SetUtcNow(InWindowDate); + + var stack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + utcFirstOccurrence: InWindowDate, + utcLastOccurrence: InWindowDate, + totalOccurrences: 0) + , o => o.ImmediateConsistency()); + + // Events exist with known occurrence dates — as if they were posted but the Redis + // ValueTuple bug caused stack stat increments to be silently dropped. + var first = new DateTimeOffset(2026, 2, 11, 0, 0, 0, TimeSpan.Zero); + var middle = new DateTimeOffset(2026, 2, 15, 12, 0, 0, TimeSpan.Zero); + var last = new DateTimeOffset(2026, 2, 20, 0, 0, 0, TimeSpan.Zero); + await _eventRepository.AddAsync( + [ + _eventData.GenerateEvent(organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, stackId: stack.Id, occurrenceDate: first), + _eventData.GenerateEvent(organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, stackId: stack.Id, occurrenceDate: middle), + _eventData.GenerateEvent(organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, stackId: stack.Id, occurrenceDate: last), + ], + o => o.ImmediateConsistency()); + + // Act + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = DefaultWindowStart, + UtcEnd = DefaultWindowEnd + }); + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + + // Assert + Assert.NotNull(stack); + Assert.Equal(3, stack.TotalOccurrences); + Assert.Equal(first.UtcDateTime, stack.FirstOccurrence); + Assert.Equal(last.UtcDateTime, stack.LastOccurrence); + } + + [Fact] + public async Task RunUntilEmptyAsync_WhenAllEventsAreBeforeWindowStart_ShouldSkipRepair() + { + // Arrange + // All events for this stack are before the window start — the handler won't find them + // in the event aggregation, so the stack should not be touched. + TimeProvider.SetUtcNow(new DateTime(2026, 2, 5, 12, 0, 0, DateTimeKind.Utc)); + + var stack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + totalOccurrences: 0), o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync( + [ + _eventData.GenerateEvent(organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, stackId: stack.Id, + occurrenceDate: new DateTimeOffset(2026, 2, 5, 12, 0, 0, TimeSpan.Zero)) + ], + o => o.ImmediateConsistency()); + + // Act + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = DefaultWindowStart, // Feb 10 — after this stack's events (Feb 5) + UtcEnd = DefaultWindowEnd + }); + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + + // Assert + Assert.NotNull(stack); + Assert.Equal(0, stack.TotalOccurrences); // Not touched — events outside window + } + + [Fact] + public async Task RunUntilEmptyAsync_WhenAllEventsAreAfterWindowEnd_ShouldSkipRepair() + { + // Arrange + // All events for this stack are after the window end — excluded from the aggregation. + TimeProvider.SetUtcNow(new DateTime(2026, 2, 24, 0, 0, 0, DateTimeKind.Utc)); + + var stack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + totalOccurrences: 0), o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync( + [ + _eventData.GenerateEvent(organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, stackId: stack.Id, + occurrenceDate: new DateTimeOffset(2026, 2, 24, 0, 0, 0, TimeSpan.Zero)) + ], + o => o.ImmediateConsistency()); + + // Act + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = DefaultWindowStart, + UtcEnd = DefaultWindowEnd // Feb 23 — before this stack's events (Feb 24) + }); + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + + // Assert + Assert.NotNull(stack); + Assert.Equal(0, stack.TotalOccurrences); // Not touched — events outside window + } + + [Fact] + public async Task RunUntilEmptyAsync_WhenOrganizationIdIsSpecified_ShouldOnlyRepairThatOrg() + { + // Arrange + TimeProvider.SetUtcNow(InWindowDate); + + // Stack in the target org with corrupted counters + var targetStack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + utcFirstOccurrence: InWindowDate.AddDays(1), // wrong: too late + utcLastOccurrence: InWindowDate.AddDays(-1), // wrong: too early + totalOccurrences: 0), o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync( + [_eventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, targetStack.Id, + occurrenceDate: new DateTimeOffset(InWindowDate, TimeSpan.Zero))], + o => o.ImmediateConsistency()); + + // Stack in a different org — should not be touched + const string otherOrgId = TestConstants.OrganizationId2; + const string otherProjectId = "1ecd0826e447ad1e78877ab9"; + var otherStack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: otherOrgId, + projectId: otherProjectId, + utcFirstOccurrence: InWindowDate.AddDays(1), + utcLastOccurrence: InWindowDate.AddDays(-1), + totalOccurrences: 0), o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync( + [_eventData.GenerateEvent(otherOrgId, otherProjectId, otherStack.Id, + occurrenceDate: new DateTimeOffset(InWindowDate, TimeSpan.Zero))], + o => o.ImmediateConsistency()); + + // Act: repair only the target org + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = DefaultWindowStart, + UtcEnd = DefaultWindowEnd, + OrganizationId = TestConstants.OrganizationId + }); + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + targetStack = await _stackRepository.GetByIdAsync(targetStack.Id); + otherStack = await _stackRepository.GetByIdAsync(otherStack.Id); + + // Assert + Assert.NotNull(targetStack); + Assert.Equal(1, targetStack.TotalOccurrences); // Fixed + + Assert.NotNull(otherStack); + Assert.Equal(0, otherStack.TotalOccurrences); // Not touched — different org + } + + [Fact] + public async Task RunUntilEmptyAsync_WhenStackHasNoEvents_ShouldLeaveCountersUnchanged() + { + // Arrange + // Stack is in the bug window but has no events — should be left as-is. + TimeProvider.SetUtcNow(InWindowDate); + + var stack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + totalOccurrences: 0), o => o.ImmediateConsistency()); + + // Act + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = DefaultWindowStart, + UtcEnd = DefaultWindowEnd + }); + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + + // Assert + Assert.NotNull(stack); + Assert.Equal(0, stack.TotalOccurrences); // Not touched — no events to derive stats from + } + + [Fact] + public async Task RunUntilEmptyAsync_WhenAggregatedTotalIsLowerThanCurrent_ShouldNotDecreaseTotalOccurrences() + { + // Arrange + TimeProvider.SetUtcNow(InWindowDate); + + var occurrenceDate = new DateTime(2026, 2, 15, 12, 0, 0, DateTimeKind.Utc); + + var stack = await _stackRepository.AddAsync(_stackData.GenerateStack(generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + utcFirstOccurrence: occurrenceDate, + utcLastOccurrence: occurrenceDate, + totalOccurrences: 10), o => o.ImmediateConsistency()); + + await _eventRepository.AddAsync( + [ + _eventData.GenerateEvent(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, + occurrenceDate: new DateTimeOffset(occurrenceDate, TimeSpan.Zero)) + ], + o => o.ImmediateConsistency()); + + // Act + await _workItemQueue.EnqueueAsync(new FixStackStatsWorkItem + { + UtcStart = DefaultWindowStart, + UtcEnd = DefaultWindowEnd + }); + await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); + + stack = await _stackRepository.GetByIdAsync(stack.Id); + + // Assert + Assert.NotNull(stack); + Assert.Equal(10, stack.TotalOccurrences); + } +} diff --git a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs index 6e7ba9c3c4..c8e0cf9528 100644 --- a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Text.Json; -using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Exceptionless.Core.Repositories; diff --git a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs index 979497eabf..bd765b8129 100644 --- a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs @@ -168,6 +168,50 @@ public async Task CanIncrementEventCounterAsync() Assert.Equal(utcNow.AddDays(1), stack.LastOccurrence); } + [Fact] + public async Task SetEventCounterAsync_WhenIncomingValuesAreOlderOrLower_ShouldOnlyApplyMonotonicUpdates() + { + // Arrange + var originalFirst = new DateTime(2026, 2, 15, 0, 0, 0, DateTimeKind.Utc); + var originalLast = new DateTime(2026, 2, 16, 0, 0, 0, DateTimeKind.Utc); + var stack = await _repository.AddAsync(_stackData.GenerateStack( + generateId: true, + organizationId: TestConstants.OrganizationId, + projectId: TestConstants.ProjectId, + utcFirstOccurrence: originalFirst, + utcLastOccurrence: originalLast, + totalOccurrences: 10), o => o.ImmediateConsistency()); + // Act + await _repository.SetEventCounterAsync( + stack.Id, + originalFirst.AddDays(1), + originalLast.AddDays(-1), + 5, + sendNotifications: false); + + var unchanged = await _repository.GetByIdAsync(stack.Id); + + // Assert + Assert.Equal(10, unchanged.TotalOccurrences); + Assert.Equal(originalFirst, unchanged.FirstOccurrence); + Assert.Equal(originalLast, unchanged.LastOccurrence); + + // Act + await _repository.SetEventCounterAsync( + stack.Id, + originalFirst.AddDays(-1), + originalLast.AddDays(1), + 15, + sendNotifications: false); + + var updated = await _repository.GetByIdAsync(stack.Id); + + // Assert + Assert.Equal(15, updated.TotalOccurrences); + Assert.Equal(originalFirst.AddDays(-1), updated.FirstOccurrence); + Assert.Equal(originalLast.AddDays(1), updated.LastOccurrence); + } + [Fact] public async Task CanFindManyAsync() { @@ -199,11 +243,16 @@ public async Task GetStacksForCleanupAsync() var openStack10DaysOldWithReference = _stackData.GenerateStack(id: TestConstants.StackId3, utcLastOccurrence: utcNow.SubtractDays(10), status: StackStatus.Open); openStack10DaysOldWithReference.References.Add("test"); - await _repository.AddAsync(new List { - _stackData.GenerateStack(id: TestConstants.StackId, utcLastOccurrence: utcNow.SubtractDays(5), status: StackStatus.Open), - _stackData.GenerateStack(id: TestConstants.StackId2, utcLastOccurrence: utcNow.SubtractDays(10), status: StackStatus.Open), + await _repository.AddAsync( + new List + { + _stackData.GenerateStack(id: TestConstants.StackId, utcLastOccurrence: utcNow.SubtractDays(5), + status: StackStatus.Open), + _stackData.GenerateStack(id: TestConstants.StackId2, utcLastOccurrence: utcNow.SubtractDays(10), + status: StackStatus.Open), openStack10DaysOldWithReference, - _stackData.GenerateStack(id: TestConstants.StackId4, utcLastOccurrence: utcNow.SubtractDays(10), status: StackStatus.Fixed) + _stackData.GenerateStack(id: TestConstants.StackId4, utcLastOccurrence: utcNow.SubtractDays(10), + status: StackStatus.Fixed) }, o => o.ImmediateConsistency()); var stacks = await _repository.GetStacksForCleanupAsync(TestConstants.OrganizationId, utcNow.SubtractDays(8)); diff --git a/tests/http/admin.http b/tests/http/admin.http index e0f7742354..f02571be7c 100644 --- a/tests/http/admin.http +++ b/tests/http/admin.http @@ -85,3 +85,11 @@ Authorization: Bearer {{token}} GET {{apiUrl}}/admin/maintenance/update-project-default-bot-lists Authorization: Bearer {{token}} +### Fix Stack Stats (defaults: utcStart=2026-02-10T00:00:00Z, utcEnd=null=>now) +GET {{apiUrl}}/admin/maintenance/fix-stack-stats +Authorization: Bearer {{token}} + +### Fix Stack Stats (explicit UTC window) +GET {{apiUrl}}/admin/maintenance/fix-stack-stats?utcStart=2026-02-10T00:00:00Z&utcEnd=2026-02-23T00:00:00Z +Authorization: Bearer {{token}} + From f94ac2472d557882110a003c07ec9df02db2f306 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 24 Feb 2026 08:26:49 -0600 Subject: [PATCH 09/29] Removes unused Foundatio references Removes the `Foundatio.Repositories.Extensions` and other unused references. These references were no longer required after a refactoring. Cleaning up unused dependencies improves build times and reduces potential conflicts. --- src/Exceptionless.Core/Configuration/CacheOptions.cs | 1 - src/Exceptionless.Core/Configuration/MessageBusOptions.cs | 1 - src/Exceptionless.Core/Configuration/MetricOptions.cs | 1 - src/Exceptionless.Core/Configuration/QueueOptions.cs | 1 - src/Exceptionless.Core/Configuration/StorageOptions.cs | 1 - src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs | 3 --- src/Exceptionless.Web/Startup.cs | 4 ---- .../Utility/OpenApi/AggregateDocumentTransformer.cs | 1 - .../Utility/OpenApi/SchemaReferenceIdHelper.cs | 1 - .../Controllers/OrganizationControllerTests.cs | 3 --- .../Exceptionless.Tests/Controllers/ProjectControllerTests.cs | 1 - .../Exceptionless.Tests/Controllers/WebHookControllerTests.cs | 1 - .../Serializer/Models/ModuleCollectionSerializerTests.cs | 1 - .../Serializer/Models/ParameterCollectionSerializerTests.cs | 1 - .../Serializer/Models/PersistentEventSerializerTests.cs | 1 - .../Serializer/Models/StackFrameCollectionSerializerTests.cs | 1 - 16 files changed, 23 deletions(-) diff --git a/src/Exceptionless.Core/Configuration/CacheOptions.cs b/src/Exceptionless.Core/Configuration/CacheOptions.cs index 0f6c0cf96c..a06c7c778b 100644 --- a/src/Exceptionless.Core/Configuration/CacheOptions.cs +++ b/src/Exceptionless.Core/Configuration/CacheOptions.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Extensions; -using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index 63508262e6..88a2d019e3 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Extensions; -using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; diff --git a/src/Exceptionless.Core/Configuration/MetricOptions.cs b/src/Exceptionless.Core/Configuration/MetricOptions.cs index d3875b5c19..730ad17358 100644 --- a/src/Exceptionless.Core/Configuration/MetricOptions.cs +++ b/src/Exceptionless.Core/Configuration/MetricOptions.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Extensions; -using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; diff --git a/src/Exceptionless.Core/Configuration/QueueOptions.cs b/src/Exceptionless.Core/Configuration/QueueOptions.cs index c67ed035a7..e8463f6651 100644 --- a/src/Exceptionless.Core/Configuration/QueueOptions.cs +++ b/src/Exceptionless.Core/Configuration/QueueOptions.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Extensions; -using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; diff --git a/src/Exceptionless.Core/Configuration/StorageOptions.cs b/src/Exceptionless.Core/Configuration/StorageOptions.cs index 713767660f..a6d28b8d27 100644 --- a/src/Exceptionless.Core/Configuration/StorageOptions.cs +++ b/src/Exceptionless.Core/Configuration/StorageOptions.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Extensions; -using Foundatio.Repositories.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; diff --git a/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs b/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs index fb5708ab7a..662aabff39 100644 --- a/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs +++ b/src/Exceptionless.Web/Hubs/WebSocketConnectionManager.cs @@ -1,10 +1,7 @@ using System.Collections.Concurrent; using System.Net.WebSockets; using System.Text; -using System.Text.Json; using Exceptionless.Core; -using Exceptionless.Core.Serialization; -using Exceptionless.Web.Utility; using Foundatio.Serializer; namespace Exceptionless.Web.Hubs; diff --git a/src/Exceptionless.Web/Startup.cs b/src/Exceptionless.Web/Startup.cs index 8363b4124c..205a06c46a 100644 --- a/src/Exceptionless.Web/Startup.cs +++ b/src/Exceptionless.Web/Startup.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Security.Claims; -using System.Text.Json; using Exceptionless.Core; using Exceptionless.Core.Authorization; using Exceptionless.Core.Extensions; @@ -8,7 +7,6 @@ using Exceptionless.Core.Validation; using Exceptionless.Web.Extensions; using Exceptionless.Web.Hubs; -using Exceptionless.Web.Models; using Exceptionless.Web.Security; using Exceptionless.Web.Utility; using Exceptionless.Web.Utility.Handlers; @@ -24,9 +22,7 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; -using Microsoft.AspNetCore.OpenApi; using Microsoft.Net.Http.Headers; -using Microsoft.OpenApi; using Scalar.AspNetCore; using Serilog; using Serilog.Events; diff --git a/src/Exceptionless.Web/Utility/OpenApi/AggregateDocumentTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/AggregateDocumentTransformer.cs index dcc5157173..7a535e9e33 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/AggregateDocumentTransformer.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/AggregateDocumentTransformer.cs @@ -1,4 +1,3 @@ -using Foundatio.Repositories.Models; using Microsoft.AspNetCore.OpenApi; using Microsoft.OpenApi; diff --git a/src/Exceptionless.Web/Utility/OpenApi/SchemaReferenceIdHelper.cs b/src/Exceptionless.Web/Utility/OpenApi/SchemaReferenceIdHelper.cs index 37d87f4564..a72e1fe507 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/SchemaReferenceIdHelper.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/SchemaReferenceIdHelper.cs @@ -1,7 +1,6 @@ using System.Text.Json.Serialization.Metadata; using Exceptionless.Web.Models; using Microsoft.AspNetCore.OpenApi; -using Microsoft.Extensions.Primitives; namespace Exceptionless.Web.Utility.OpenApi; diff --git a/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs b/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs index 5ff7288353..f73422fcf7 100644 --- a/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs @@ -1,10 +1,7 @@ -using Exceptionless.Core.Models; using Exceptionless.Core.Repositories; using Exceptionless.Core.Utility; using Exceptionless.Tests.Extensions; using Exceptionless.Web.Models; -using FluentRest; -using Foundatio.Repositories; using Xunit; namespace Exceptionless.Tests.Controllers; diff --git a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs index 0e148a63dd..feb7d6479a 100644 --- a/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/ProjectControllerTests.cs @@ -9,7 +9,6 @@ using Exceptionless.Web.Models; using FluentRest; using Foundatio.Jobs; -using Foundatio.Repositories; using Xunit; namespace Exceptionless.Tests.Controllers; diff --git a/tests/Exceptionless.Tests/Controllers/WebHookControllerTests.cs b/tests/Exceptionless.Tests/Controllers/WebHookControllerTests.cs index e446bad9f1..a930108b58 100644 --- a/tests/Exceptionless.Tests/Controllers/WebHookControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/WebHookControllerTests.cs @@ -3,7 +3,6 @@ using Exceptionless.Core.Utility; using Exceptionless.Tests.Extensions; using Exceptionless.Web.Models; -using Foundatio.Repositories; using Microsoft.AspNetCore.Mvc; using Xunit; diff --git a/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs index 3af413b812..f8e191f37f 100644 --- a/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/Models/ModuleCollectionSerializerTests.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Models; -using Exceptionless.Core.Models.Data; using Foundatio.Serializer; using Xunit; diff --git a/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs index ed0cc9f550..919b3363d3 100644 --- a/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/Models/ParameterCollectionSerializerTests.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Models; -using Exceptionless.Core.Models.Data; using Foundatio.Serializer; using Xunit; diff --git a/tests/Exceptionless.Tests/Serializer/Models/PersistentEventSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/PersistentEventSerializerTests.cs index 5dafe0748c..8d77001c8a 100644 --- a/tests/Exceptionless.Tests/Serializer/Models/PersistentEventSerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/Models/PersistentEventSerializerTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using Exceptionless.Core.Extensions; using Exceptionless.Core.Models; using Exceptionless.Core.Models.Data; using Foundatio.Serializer; diff --git a/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs b/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs index 8c6daf8160..08e11c47c2 100644 --- a/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/Models/StackFrameCollectionSerializerTests.cs @@ -1,5 +1,4 @@ using Exceptionless.Core.Models; -using Exceptionless.Core.Models.Data; using Foundatio.Serializer; using Xunit; From bb3dbe34dec6ce79452072e327e71b6f5a298d76 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 3 Mar 2026 23:30:56 -0600 Subject: [PATCH 10/29] Updated agent skills --- .../skills/accessibility/SKILL.md | 0 .agents/skills/agent-browser/SKILL.md | 599 +++++++++++------- .../references/authentication.md | 16 +- .../agent-browser/references/commands.md | 4 + .../agent-browser/references/profiling.md | 120 ++++ .../agent-browser/references/proxy-support.md | 29 +- .../references/session-management.md | 14 +- .../agent-browser/references/snapshot-refs.md | 34 +- .../references/video-recording.md | 13 +- .../templates/authenticated-session.sh | 86 +-- .../templates/capture-workflow.sh | 71 ++- .../templates/form-automation.sh | 82 ++- .../skills/backend-architecture/SKILL.md | 0 .../skills/backend-testing/SKILL.md | 0 .agents/skills/dogfood/SKILL.md | 216 +++++++ .../dogfood/references/issue-taxonomy.md | 109 ++++ .../templates/dogfood-report-template.md | 53 ++ .../skills/dotnet-cli/SKILL.md | 0 .../skills/dotnet-conventions/SKILL.md | 0 .../skills/e2e-testing/SKILL.md | 0 .../skills/foundatio/SKILL.md | 0 .../skills/frontend-architecture/SKILL.md | 0 .../skills/frontend-testing/SKILL.md | 0 .../skills/releasenotes/README.md | 17 +- .../skills/security-principles/SKILL.md | 0 .../skills/shadcn-svelte/SKILL.md | 0 .../skills/storybook/SKILL.md | 0 .agents/skills/stripe-best-practices/SKILL.md | 5 +- .../skills/svelte-components/SKILL.md | 0 .../skills/tanstack-form/SKILL.md | 0 .../skills/tanstack-query/SKILL.md | 0 .../skills/typescript-conventions/SKILL.md | 0 .agents/skills/upgrade-stripe/SKILL.md | 24 +- .github/.agents/skills/agent-browser/SKILL.md | 181 ------ .../references/authentication.md | 202 ------ .../agent-browser/references/proxy-support.md | 188 ------ .../references/session-management.md | 193 ------ .../agent-browser/references/snapshot-refs.md | 194 ------ .../references/video-recording.md | 173 ----- .../templates/authenticated-session.sh | 97 --- .../templates/capture-workflow.sh | 69 -- .../templates/form-automation.sh | 62 -- .../skills/frontend-design/LICENSE.txt | 177 ------ .../.agents/skills/frontend-design/SKILL.md | 42 -- .../skills/stripe-best-practices/SKILL.md | 31 - .../.agents/skills/upgrade-stripe/SKILL.md | 173 ----- .github/.github/skills/agent-browser | 1 - .github/.github/skills/frontend-design | 1 - .github/.github/skills/releasenotes | 1 - .github/.github/skills/skill-creator | 1 - .github/.github/skills/stripe-best-practices | 1 - .github/.github/skills/upgrade-stripe | 1 - .github/skills/agent-browser | 1 - .github/skills/frontend-design | 1 - .github/skills/releasenotes | 1 - .github/skills/stripe-best-practices | 1 - .github/skills/upgrade-stripe | 1 - .github/update-skills.ps1 | 21 - AGENTS.md | 20 +- skills-lock.json | 35 + 60 files changed, 1168 insertions(+), 2193 deletions(-) rename {.github => .agents}/skills/accessibility/SKILL.md (100%) rename {.github/.agents => .agents}/skills/agent-browser/references/commands.md (97%) create mode 100644 .agents/skills/agent-browser/references/profiling.md rename {.github => .agents}/skills/backend-architecture/SKILL.md (100%) rename {.github => .agents}/skills/backend-testing/SKILL.md (100%) create mode 100644 .agents/skills/dogfood/SKILL.md create mode 100644 .agents/skills/dogfood/references/issue-taxonomy.md create mode 100644 .agents/skills/dogfood/templates/dogfood-report-template.md rename {.github => .agents}/skills/dotnet-cli/SKILL.md (100%) rename {.github => .agents}/skills/dotnet-conventions/SKILL.md (100%) rename {.github => .agents}/skills/e2e-testing/SKILL.md (100%) rename {.github => .agents}/skills/foundatio/SKILL.md (100%) rename {.github => .agents}/skills/frontend-architecture/SKILL.md (100%) rename {.github => .agents}/skills/frontend-testing/SKILL.md (100%) rename .github/.agents/skills/releasenotes/SKILL.md => .agents/skills/releasenotes/README.md (66%) rename {.github => .agents}/skills/security-principles/SKILL.md (100%) rename {.github => .agents}/skills/shadcn-svelte/SKILL.md (100%) rename {.github => .agents}/skills/storybook/SKILL.md (100%) rename {.github => .agents}/skills/svelte-components/SKILL.md (100%) rename {.github => .agents}/skills/tanstack-form/SKILL.md (100%) rename {.github => .agents}/skills/tanstack-query/SKILL.md (100%) rename {.github => .agents}/skills/typescript-conventions/SKILL.md (100%) delete mode 100644 .github/.agents/skills/agent-browser/SKILL.md delete mode 100644 .github/.agents/skills/agent-browser/references/authentication.md delete mode 100644 .github/.agents/skills/agent-browser/references/proxy-support.md delete mode 100644 .github/.agents/skills/agent-browser/references/session-management.md delete mode 100644 .github/.agents/skills/agent-browser/references/snapshot-refs.md delete mode 100644 .github/.agents/skills/agent-browser/references/video-recording.md delete mode 100755 .github/.agents/skills/agent-browser/templates/authenticated-session.sh delete mode 100755 .github/.agents/skills/agent-browser/templates/capture-workflow.sh delete mode 100755 .github/.agents/skills/agent-browser/templates/form-automation.sh delete mode 100644 .github/.agents/skills/frontend-design/LICENSE.txt delete mode 100644 .github/.agents/skills/frontend-design/SKILL.md delete mode 100644 .github/.agents/skills/stripe-best-practices/SKILL.md delete mode 100644 .github/.agents/skills/upgrade-stripe/SKILL.md delete mode 120000 .github/.github/skills/agent-browser delete mode 120000 .github/.github/skills/frontend-design delete mode 120000 .github/.github/skills/releasenotes delete mode 120000 .github/.github/skills/skill-creator delete mode 120000 .github/.github/skills/stripe-best-practices delete mode 120000 .github/.github/skills/upgrade-stripe delete mode 120000 .github/skills/agent-browser delete mode 120000 .github/skills/frontend-design delete mode 120000 .github/skills/releasenotes delete mode 120000 .github/skills/stripe-best-practices delete mode 120000 .github/skills/upgrade-stripe delete mode 100644 .github/update-skills.ps1 create mode 100644 skills-lock.json diff --git a/.github/skills/accessibility/SKILL.md b/.agents/skills/accessibility/SKILL.md similarity index 100% rename from .github/skills/accessibility/SKILL.md rename to .agents/skills/accessibility/SKILL.md diff --git a/.agents/skills/agent-browser/SKILL.md b/.agents/skills/agent-browser/SKILL.md index ab3ea3c6b9..8cd7b7a79b 100644 --- a/.agents/skills/agent-browser/SKILL.md +++ b/.agents/skills/agent-browser/SKILL.md @@ -1,339 +1,508 @@ --- name: agent-browser -description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages. -allowed-tools: Bash(agent-browser:*) +description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. +allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*) --- # Browser Automation with agent-browser -## Quick start +## Core Workflow + +Every browser automation follows this pattern: + +1. **Navigate**: `agent-browser open ` +2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`) +3. **Interact**: Use refs to click, fill, select +4. **Re-snapshot**: After navigation or DOM changes, get fresh refs ```bash -agent-browser open # Navigate to page -agent-browser snapshot -i # Get interactive elements with refs -agent-browser click @e1 # Click element by ref -agent-browser fill @e2 "text" # Fill input by ref -agent-browser close # Close browser +agent-browser open https://example.com/form +agent-browser snapshot -i +# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit" + +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" +agent-browser click @e3 +agent-browser wait --load networkidle +agent-browser snapshot -i # Check result ``` -## Core workflow +## Command Chaining -1. Navigate: `agent-browser open ` -2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`) -3. Interact using refs from the snapshot -4. Re-snapshot after navigation or significant DOM changes +Commands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls. -## Commands +```bash +# Chain open + wait + snapshot in one call +agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i -### Navigation +# Chain multiple interactions +agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "password123" && agent-browser click @e3 -```bash -agent-browser open # Navigate to URL (aliases: goto, navigate) - # Supports: https://, http://, file://, about:, data:// - # Auto-prepends https:// if no protocol given -agent-browser back # Go back -agent-browser forward # Go forward -agent-browser reload # Reload page -agent-browser close # Close browser (aliases: quit, exit) -agent-browser connect 9222 # Connect to browser via CDP port +# Navigate and capture +agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png ``` -### Snapshot (page analysis) +**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs). + +## Essential Commands ```bash -agent-browser snapshot # Full accessibility tree -agent-browser snapshot -i # Interactive elements only (recommended) -agent-browser snapshot -c # Compact output -agent-browser snapshot -d 3 # Limit depth to 3 -agent-browser snapshot -s "#main" # Scope to CSS selector +# Navigation +agent-browser open # Navigate (aliases: goto, navigate) +agent-browser close # Close browser + +# Snapshot +agent-browser snapshot -i # Interactive elements with refs (recommended) +agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer) +agent-browser snapshot -s "#selector" # Scope to CSS selector + +# Interaction (use @refs from snapshot) +agent-browser click @e1 # Click element +agent-browser click @e1 --new-tab # Click and open in new tab +agent-browser fill @e2 "text" # Clear and type text +agent-browser type @e2 "text" # Type without clearing +agent-browser select @e1 "option" # Select dropdown option +agent-browser check @e1 # Check checkbox +agent-browser press Enter # Press key +agent-browser keyboard type "text" # Type at current focus (no selector) +agent-browser keyboard inserttext "text" # Insert without key events +agent-browser scroll down 500 # Scroll page +agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container + +# Get information +agent-browser get text @e1 # Get element text +agent-browser get url # Get current URL +agent-browser get title # Get page title + +# Wait +agent-browser wait @e1 # Wait for element +agent-browser wait --load networkidle # Wait for network idle +agent-browser wait --url "**/page" # Wait for URL pattern +agent-browser wait 2000 # Wait milliseconds + +# Downloads +agent-browser download @e1 ./file.pdf # Click element to trigger download +agent-browser wait --download ./output.zip # Wait for any download to complete +agent-browser --download-path ./downloads open # Set default download directory + +# Capture +agent-browser screenshot # Screenshot to temp dir +agent-browser screenshot --full # Full page screenshot +agent-browser screenshot --annotate # Annotated screenshot with numbered element labels +agent-browser pdf output.pdf # Save as PDF + +# Diff (compare page states) +agent-browser diff snapshot # Compare current vs last snapshot +agent-browser diff snapshot --baseline before.txt # Compare current vs saved file +agent-browser diff screenshot --baseline before.png # Visual pixel diff +agent-browser diff url # Compare two pages +agent-browser diff url --wait-until networkidle # Custom wait strategy +agent-browser diff url --selector "#main" # Scope to element ``` -### Interactions (use @refs from snapshot) +## Common Patterns + +### Form Submission ```bash -agent-browser click @e1 # Click -agent-browser dblclick @e1 # Double-click -agent-browser focus @e1 # Focus element -agent-browser fill @e2 "text" # Clear and type -agent-browser type @e2 "text" # Type without clearing -agent-browser press Enter # Press key (alias: key) -agent-browser press Control+a # Key combination -agent-browser keydown Shift # Hold key down -agent-browser keyup Shift # Release key -agent-browser hover @e1 # Hover -agent-browser check @e1 # Check checkbox -agent-browser uncheck @e1 # Uncheck checkbox -agent-browser select @e1 "value" # Select dropdown option -agent-browser select @e1 "a" "b" # Select multiple options -agent-browser scroll down 500 # Scroll page (default: down 300px) -agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto) -agent-browser drag @e1 @e2 # Drag and drop -agent-browser upload @e1 file.pdf # Upload files +agent-browser open https://example.com/signup +agent-browser snapshot -i +agent-browser fill @e1 "Jane Doe" +agent-browser fill @e2 "jane@example.com" +agent-browser select @e3 "California" +agent-browser check @e4 +agent-browser click @e5 +agent-browser wait --load networkidle ``` -### Get information +### Authentication with Auth Vault (Recommended) ```bash -agent-browser get text @e1 # Get element text -agent-browser get html @e1 # Get innerHTML -agent-browser get value @e1 # Get input value -agent-browser get attr @e1 href # Get attribute -agent-browser get title # Get page title -agent-browser get url # Get current URL -agent-browser get count ".item" # Count matching elements -agent-browser get box @e1 # Get bounding box -agent-browser get styles @e1 # Get computed styles (font, color, bg, etc.) +# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY) +# Recommended: pipe password via stdin to avoid shell history exposure +echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin + +# Login using saved profile (LLM never sees password) +agent-browser auth login github + +# List/show/delete profiles +agent-browser auth list +agent-browser auth show github +agent-browser auth delete github ``` -### Check state +### Authentication with State Persistence ```bash -agent-browser is visible @e1 # Check if visible -agent-browser is enabled @e1 # Check if enabled -agent-browser is checked @e1 # Check if checked +# Login once and save state +agent-browser open https://app.example.com/login +agent-browser snapshot -i +agent-browser fill @e1 "$USERNAME" +agent-browser fill @e2 "$PASSWORD" +agent-browser click @e3 +agent-browser wait --url "**/dashboard" +agent-browser state save auth.json + +# Reuse in future sessions +agent-browser state load auth.json +agent-browser open https://app.example.com/dashboard ``` -### Screenshots & PDF +### Session Persistence ```bash -agent-browser screenshot # Save to a temporary directory -agent-browser screenshot path.png # Save to a specific path -agent-browser screenshot --full # Full page -agent-browser pdf output.pdf # Save as PDF +# Auto-save/restore cookies and localStorage across browser restarts +agent-browser --session-name myapp open https://app.example.com/login +# ... login flow ... +agent-browser close # State auto-saved to ~/.agent-browser/sessions/ + +# Next time, state is auto-loaded +agent-browser --session-name myapp open https://app.example.com/dashboard + +# Encrypt state at rest +export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32) +agent-browser --session-name secure open https://app.example.com + +# Manage saved states +agent-browser state list +agent-browser state show myapp-default.json +agent-browser state clear myapp +agent-browser state clean --older-than 7 ``` -### Video recording +### Data Extraction ```bash -agent-browser record start ./demo.webm # Start recording (uses current URL + state) -agent-browser click @e1 # Perform actions -agent-browser record stop # Stop and save video -agent-browser record restart ./take2.webm # Stop current + start new recording -``` +agent-browser open https://example.com/products +agent-browser snapshot -i +agent-browser get text @e5 # Get specific element text +agent-browser get text body > page.txt # Get all page text -Recording creates a fresh context but preserves cookies/storage from your session. If no URL is provided, it -automatically returns to your current page. For smooth demos, explore first, then start recording. +# JSON output for parsing +agent-browser snapshot -i --json +agent-browser get text @e1 --json +``` -### Wait +### Parallel Sessions ```bash -agent-browser wait @e1 # Wait for element -agent-browser wait 2000 # Wait milliseconds -agent-browser wait --text "Success" # Wait for text (or -t) -agent-browser wait --url "**/dashboard" # Wait for URL pattern (or -u) -agent-browser wait --load networkidle # Wait for network idle (or -l) -agent-browser wait --fn "window.ready" # Wait for JS condition (or -f) +agent-browser --session site1 open https://site-a.com +agent-browser --session site2 open https://site-b.com + +agent-browser --session site1 snapshot -i +agent-browser --session site2 snapshot -i + +agent-browser session list ``` -### Mouse control +### Connect to Existing Chrome ```bash -agent-browser mouse move 100 200 # Move mouse -agent-browser mouse down left # Press button -agent-browser mouse up left # Release button -agent-browser mouse wheel 100 # Scroll wheel +# Auto-discover running Chrome with remote debugging enabled +agent-browser --auto-connect open https://example.com +agent-browser --auto-connect snapshot + +# Or with explicit CDP port +agent-browser --cdp 9222 snapshot ``` -### Semantic locators (alternative to refs) +### Color Scheme (Dark Mode) ```bash -agent-browser find role button click --name "Submit" -agent-browser find text "Sign In" click -agent-browser find text "Sign In" click --exact # Exact match only -agent-browser find label "Email" fill "user@test.com" -agent-browser find placeholder "Search" type "query" -agent-browser find alt "Logo" click -agent-browser find title "Close" click -agent-browser find testid "submit-btn" click -agent-browser find first ".item" click -agent-browser find last ".item" click -agent-browser find nth 2 "a" hover +# Persistent dark mode via flag (applies to all pages and new tabs) +agent-browser --color-scheme dark open https://example.com + +# Or via environment variable +AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com + +# Or set during session (persists for subsequent commands) +agent-browser set media dark ``` -### Browser settings +### Visual Browser (Debugging) ```bash -agent-browser set viewport 1920 1080 # Set viewport size -agent-browser set device "iPhone 14" # Emulate device -agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation) -agent-browser set offline on # Toggle offline mode -agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers -agent-browser set credentials user pass # HTTP basic auth (alias: auth) -agent-browser set media dark # Emulate color scheme -agent-browser set media light reduced-motion # Light mode + reduced motion +agent-browser --headed open https://example.com +agent-browser highlight @e1 # Highlight element +agent-browser record start demo.webm # Record session +agent-browser profiler start # Start Chrome DevTools profiling +agent-browser profiler stop trace.json # Stop and save profile (path optional) ``` -### Cookies & Storage +Use `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode. + +### Local Files (PDFs, HTML) ```bash -agent-browser cookies # Get all cookies -agent-browser cookies set name value # Set cookie -agent-browser cookies clear # Clear cookies -agent-browser storage local # Get all localStorage -agent-browser storage local key # Get specific key -agent-browser storage local set k v # Set value -agent-browser storage local clear # Clear all +# Open local files with file:// URLs +agent-browser --allow-file-access open file:///path/to/document.pdf +agent-browser --allow-file-access open file:///path/to/page.html +agent-browser screenshot output.png ``` -### Network +### iOS Simulator (Mobile Safari) ```bash -agent-browser network route # Intercept requests -agent-browser network route --abort # Block requests -agent-browser network route --body '{}' # Mock response -agent-browser network unroute [url] # Remove routes -agent-browser network requests # View tracked requests -agent-browser network requests --filter api # Filter requests +# List available iOS simulators +agent-browser device list + +# Launch Safari on a specific device +agent-browser -p ios --device "iPhone 16 Pro" open https://example.com + +# Same workflow as desktop - snapshot, interact, re-snapshot +agent-browser -p ios snapshot -i +agent-browser -p ios tap @e1 # Tap (alias for click) +agent-browser -p ios fill @e2 "text" +agent-browser -p ios swipe up # Mobile-specific gesture + +# Take screenshot +agent-browser -p ios screenshot mobile.png + +# Close session (shuts down simulator) +agent-browser -p ios close ``` -### Tabs & Windows +**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`) + +**Real devices:** Works with physical iOS devices if pre-configured. Use `--device ""` where UDID is from `xcrun xctrace list devices`. + +## Security + +All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output. + +### Content Boundaries (Recommended for AI Agents) + +Enable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content: ```bash -agent-browser tab # List tabs -agent-browser tab new [url] # New tab -agent-browser tab 2 # Switch to tab by index -agent-browser tab close # Close current tab -agent-browser tab close 2 # Close tab by index -agent-browser window new # New window +export AGENT_BROWSER_CONTENT_BOUNDARIES=1 +agent-browser snapshot +# Output: +# --- AGENT_BROWSER_PAGE_CONTENT nonce= origin=https://example.com --- +# [accessibility tree] +# --- END_AGENT_BROWSER_PAGE_CONTENT nonce= --- ``` -### Frames +### Domain Allowlist + +Restrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on: ```bash -agent-browser frame "#iframe" # Switch to iframe -agent-browser frame main # Back to main frame +export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com" +agent-browser open https://example.com # OK +agent-browser open https://malicious.com # Blocked ``` -### Dialogs +### Action Policy + +Use a policy file to gate destructive actions: ```bash -agent-browser dialog accept [text] # Accept dialog -agent-browser dialog dismiss # Dismiss dialog +export AGENT_BROWSER_ACTION_POLICY=./policy.json +``` + +Example `policy.json`: +```json +{"default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"]} ``` -### JavaScript +Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies. + +### Output Limits + +Prevent context flooding from large pages: ```bash -agent-browser eval "document.title" # Run JavaScript +export AGENT_BROWSER_MAX_OUTPUT=50000 ``` -## Global options +## Diffing (Verifying Changes) + +Use `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session. ```bash -agent-browser --session ... # Isolated browser session -agent-browser --json ... # JSON output for parsing -agent-browser --headed ... # Show browser window (not headless) -agent-browser --full ... # Full page screenshot (-f) -agent-browser --cdp ... # Connect via Chrome DevTools Protocol -agent-browser -p ... # Cloud browser provider (--provider) -agent-browser --proxy ... # Use proxy server -agent-browser --headers ... # HTTP headers scoped to URL's origin -agent-browser --executable-path

# Custom browser executable -agent-browser --extension ... # Load browser extension (repeatable) -agent-browser --help # Show help (-h) -agent-browser --version # Show version (-V) -agent-browser --help # Show detailed help for a command +# Typical workflow: snapshot -> action -> diff +agent-browser snapshot -i # Take baseline snapshot +agent-browser click @e2 # Perform action +agent-browser diff snapshot # See what changed (auto-compares to last snapshot) ``` -### Proxy support +For visual regression testing or monitoring: ```bash -agent-browser --proxy http://proxy.com:8080 open example.com -agent-browser --proxy http://user:pass@proxy.com:8080 open example.com -agent-browser --proxy socks5://proxy.com:1080 open example.com +# Save a baseline screenshot, then compare later +agent-browser screenshot baseline.png +# ... time passes or changes are made ... +agent-browser diff screenshot --baseline baseline.png + +# Compare staging vs production +agent-browser diff url https://staging.example.com https://prod.example.com --screenshot ``` -## Environment variables +`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage. + +## Timeouts and Slow Pages + +The default Playwright timeout is 25 seconds for local browsers. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout: ```bash -AGENT_BROWSER_SESSION="mysession" # Default session name -AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path -AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths -AGENT_BROWSER_PROVIDER="your-cloud-browser-provider" # Cloud browser provider (select browseruse or browserbase) -AGENT_BROWSER_STREAM_PORT="9223" # WebSocket streaming port -AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location (for daemon.js) +# Wait for network activity to settle (best for slow pages) +agent-browser wait --load networkidle + +# Wait for a specific element to appear +agent-browser wait "#content" +agent-browser wait @e1 + +# Wait for a specific URL pattern (useful after redirects) +agent-browser wait --url "**/dashboard" + +# Wait for a JavaScript condition +agent-browser wait --fn "document.readyState === 'complete'" + +# Wait a fixed duration (milliseconds) as a last resort +agent-browser wait 5000 ``` -## Example: Form submission +When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait ` or `wait @ref`. + +## Session Management and Cleanup + +When running multiple agents or automations concurrently, always use named sessions to avoid conflicts: ```bash -agent-browser open https://example.com/form -agent-browser snapshot -i -# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3] +# Each agent gets its own isolated session +agent-browser --session agent1 open site-a.com +agent-browser --session agent2 open site-b.com -agent-browser fill @e1 "user@example.com" -agent-browser fill @e2 "password123" -agent-browser click @e3 -agent-browser wait --load networkidle -agent-browser snapshot -i # Check result +# Check active sessions +agent-browser session list ``` -## Example: Authentication with saved state +Always close your browser session when done to avoid leaked processes: ```bash -# Login once -agent-browser open https://app.example.com/login -agent-browser snapshot -i -agent-browser fill @e1 "username" -agent-browser fill @e2 "password" -agent-browser click @e3 -agent-browser wait --url "**/dashboard" -agent-browser state save auth.json +agent-browser close # Close default session +agent-browser --session agent1 close # Close specific session +``` -# Later sessions: load saved state -agent-browser state load auth.json -agent-browser open https://app.example.com/dashboard +If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work. + +## Ref Lifecycle (Important) + +Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after: + +- Clicking links or buttons that navigate +- Form submissions +- Dynamic content loading (dropdowns, modals) + +```bash +agent-browser click @e5 # Navigates to new page +agent-browser snapshot -i # MUST re-snapshot +agent-browser click @e1 # Use new refs ``` -## Sessions (parallel browsers) +## Annotated Screenshots (Vision Mode) + +Use `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot. ```bash -agent-browser --session test1 open site-a.com -agent-browser --session test2 open site-b.com -agent-browser session list +agent-browser screenshot --annotate +# Output includes the image path and a legend: +# [1] @e1 button "Submit" +# [2] @e2 link "Home" +# [3] @e3 textbox "Email" +agent-browser click @e2 # Click using ref from annotated screenshot ``` -## JSON output (for parsing) +Use annotated screenshots when: +- The page has unlabeled icon buttons or visual-only elements +- You need to verify visual layout or styling +- Canvas or chart elements are present (invisible to text snapshots) +- You need spatial reasoning about element positions -Add `--json` for machine-readable output: +## Semantic Locators (Alternative to Refs) + +When refs are unavailable or unreliable, use semantic locators: ```bash -agent-browser snapshot -i --json -agent-browser get text @e1 --json +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find role button click --name "Submit" +agent-browser find placeholder "Search" type "query" +agent-browser find testid "submit-btn" click ``` -## Debugging +## JavaScript Evaluation (eval) + +Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues. ```bash -agent-browser --headed open example.com # Show browser window -agent-browser --cdp 9222 snapshot # Connect via CDP port -agent-browser connect 9222 # Alternative: connect command -agent-browser console # View console messages -agent-browser console --clear # Clear console -agent-browser errors # View page errors -agent-browser errors --clear # Clear errors -agent-browser highlight @e1 # Highlight element -agent-browser trace start # Start recording trace -agent-browser trace stop trace.zip # Stop and save trace -agent-browser record start ./debug.webm # Record video from current page -agent-browser record stop # Save recording +# Simple expressions work with regular quoting +agent-browser eval 'document.title' +agent-browser eval 'document.querySelectorAll("img").length' + +# Complex JS: use --stdin with heredoc (RECOMMENDED) +agent-browser eval --stdin <<'EVALEOF' +JSON.stringify( + Array.from(document.querySelectorAll("img")) + .filter(i => !i.alt) + .map(i => ({ src: i.src.split("/").pop(), width: i.width })) +) +EVALEOF + +# Alternative: base64 encoding (avoids all shell escaping issues) +agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)" ``` -## Deep-dive documentation +**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely. + +**Rules of thumb:** +- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine +- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'` +- Programmatic/generated scripts -> use `eval -b` with base64 + +## Configuration File -For detailed patterns and best practices, see: +Create `agent-browser.json` in the project root for persistent settings: -| Reference | Description | +```json +{ + "headed": true, + "proxy": "http://localhost:8080", + "profile": "./browser-data" +} +``` + +Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config ` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `"executablePath"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced. + +## Deep-Dive Documentation + +| Reference | When to Use | |-----------|-------------| +| [references/commands.md](references/commands.md) | Full command reference with all options | | [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting | | [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping | | [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse | | [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation | +| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis | | [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies | -## Ready-to-use templates +## Experimental: Native Mode + +agent-browser has an experimental native Rust daemon that communicates with Chrome directly via CDP, bypassing Node.js and Playwright entirely. It is opt-in and not recommended for production use yet. + +```bash +# Enable via flag +agent-browser --native open example.com + +# Enable via environment variable (avoids passing --native every time) +export AGENT_BROWSER_NATIVE=1 +agent-browser open example.com +``` + +The native daemon supports Chromium and Safari (via WebDriver). Firefox and WebKit are not yet supported. All core commands (navigate, snapshot, click, fill, screenshot, cookies, storage, tabs, eval, etc.) work identically in native mode. Use `agent-browser close` before switching between native and default mode within the same session. -Executable workflow scripts for common patterns: +## Ready-to-Use Templates | Template | Description | |----------|-------------| @@ -341,16 +510,8 @@ Executable workflow scripts for common patterns: | [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state | | [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | -Usage: ```bash ./templates/form-automation.sh https://example.com/form ./templates/authenticated-session.sh https://app.example.com/login ./templates/capture-workflow.sh https://example.com ./output ``` - -## HTTPS Certificate Errors - -For sites with self-signed or invalid certificates: -```bash -agent-browser open https://localhost:8443 --ignore-https-errors -``` diff --git a/.agents/skills/agent-browser/references/authentication.md b/.agents/skills/agent-browser/references/authentication.md index 5d801f6a82..12ef5e41be 100644 --- a/.agents/skills/agent-browser/references/authentication.md +++ b/.agents/skills/agent-browser/references/authentication.md @@ -1,6 +1,20 @@ # Authentication Patterns -Patterns for handling login flows, session persistence, and authenticated browsing. +Login flows, session persistence, OAuth, 2FA, and authenticated browsing. + +**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Login Flow](#basic-login-flow) +- [Saving Authentication State](#saving-authentication-state) +- [Restoring Authentication](#restoring-authentication) +- [OAuth / SSO Flows](#oauth--sso-flows) +- [Two-Factor Authentication](#two-factor-authentication) +- [HTTP Basic Auth](#http-basic-auth) +- [Cookie-Based Auth](#cookie-based-auth) +- [Token Refresh Handling](#token-refresh-handling) +- [Security Best Practices](#security-best-practices) ## Basic Login Flow diff --git a/.github/.agents/skills/agent-browser/references/commands.md b/.agents/skills/agent-browser/references/commands.md similarity index 97% rename from .github/.agents/skills/agent-browser/references/commands.md rename to .agents/skills/agent-browser/references/commands.md index 8744accf75..e77196cdd3 100644 --- a/.github/.agents/skills/agent-browser/references/commands.md +++ b/.agents/skills/agent-browser/references/commands.md @@ -29,6 +29,7 @@ agent-browser snapshot -s "#main" # Scope to CSS selector ```bash agent-browser click @e1 # Click +agent-browser click @e1 --new-tab # Click and open in new tab agent-browser dblclick @e1 # Double-click agent-browser focus @e1 # Focus element agent-browser fill @e2 "text" # Clear and type @@ -223,6 +224,7 @@ agent-browser --full ... # Full page screenshot (-f) agent-browser --cdp ... # Connect via Chrome DevTools Protocol agent-browser -p ... # Cloud browser provider (--provider) agent-browser --proxy ... # Use proxy server +agent-browser --proxy-bypass # Hosts to bypass proxy agent-browser --headers ... # HTTP headers scoped to URL's origin agent-browser --executable-path

# Custom browser executable agent-browser --extension ... # Load browser extension (repeatable) @@ -245,6 +247,8 @@ agent-browser errors --clear # Clear errors agent-browser highlight @e1 # Highlight element agent-browser trace start # Start recording trace agent-browser trace stop trace.zip # Stop and save trace +agent-browser profiler start # Start Chrome DevTools profiling +agent-browser profiler stop trace.json # Stop and save profile ``` ## Environment Variables diff --git a/.agents/skills/agent-browser/references/profiling.md b/.agents/skills/agent-browser/references/profiling.md new file mode 100644 index 0000000000..bd47eaa0ce --- /dev/null +++ b/.agents/skills/agent-browser/references/profiling.md @@ -0,0 +1,120 @@ +# Profiling + +Capture Chrome DevTools performance profiles during browser automation for performance analysis. + +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Profiling](#basic-profiling) +- [Profiler Commands](#profiler-commands) +- [Categories](#categories) +- [Use Cases](#use-cases) +- [Output Format](#output-format) +- [Viewing Profiles](#viewing-profiles) +- [Limitations](#limitations) + +## Basic Profiling + +```bash +# Start profiling +agent-browser profiler start + +# Perform actions +agent-browser navigate https://example.com +agent-browser click "#button" +agent-browser wait 1000 + +# Stop and save +agent-browser profiler stop ./trace.json +``` + +## Profiler Commands + +```bash +# Start profiling with default categories +agent-browser profiler start + +# Start with custom trace categories +agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing" + +# Stop profiling and save to file +agent-browser profiler stop ./trace.json +``` + +## Categories + +The `--categories` flag accepts a comma-separated list of Chrome trace categories. Default categories include: + +- `devtools.timeline` -- standard DevTools performance traces +- `v8.execute` -- time spent running JavaScript +- `blink` -- renderer events +- `blink.user_timing` -- `performance.mark()` / `performance.measure()` calls +- `latencyInfo` -- input-to-latency tracking +- `renderer.scheduler` -- task scheduling and execution +- `toplevel` -- broad-spectrum basic events + +Several `disabled-by-default-*` categories are also included for detailed timeline, call stack, and V8 CPU profiling data. + +## Use Cases + +### Diagnosing Slow Page Loads + +```bash +agent-browser profiler start +agent-browser navigate https://app.example.com +agent-browser wait --load networkidle +agent-browser profiler stop ./page-load-profile.json +``` + +### Profiling User Interactions + +```bash +agent-browser navigate https://app.example.com +agent-browser profiler start +agent-browser click "#submit" +agent-browser wait 2000 +agent-browser profiler stop ./interaction-profile.json +``` + +### CI Performance Regression Checks + +```bash +#!/bin/bash +agent-browser profiler start +agent-browser navigate https://app.example.com +agent-browser wait --load networkidle +agent-browser profiler stop "./profiles/build-${BUILD_ID}.json" +``` + +## Output Format + +The output is a JSON file in Chrome Trace Event format: + +```json +{ + "traceEvents": [ + { "cat": "devtools.timeline", "name": "RunTask", "ph": "X", "ts": 12345, "dur": 100, ... }, + ... + ], + "metadata": { + "clock-domain": "LINUX_CLOCK_MONOTONIC" + } +} +``` + +The `metadata.clock-domain` field is set based on the host platform (Linux or macOS). On Windows it is omitted. + +## Viewing Profiles + +Load the output JSON file in any of these tools: + +- **Chrome DevTools**: Performance panel > Load profile (Ctrl+Shift+I > Performance) +- **Perfetto UI**: https://ui.perfetto.dev/ -- drag and drop the JSON file +- **Trace Viewer**: `chrome://tracing` in any Chromium browser + +## Limitations + +- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit. +- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest. +- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail. diff --git a/.agents/skills/agent-browser/references/proxy-support.md b/.agents/skills/agent-browser/references/proxy-support.md index 05fcec26d9..e86a8fe33e 100644 --- a/.agents/skills/agent-browser/references/proxy-support.md +++ b/.agents/skills/agent-browser/references/proxy-support.md @@ -1,13 +1,29 @@ # Proxy Support -Configure proxy servers for browser automation, useful for geo-testing, rate limiting avoidance, and corporate environments. +Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments. + +**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Proxy Configuration](#basic-proxy-configuration) +- [Authenticated Proxy](#authenticated-proxy) +- [SOCKS Proxy](#socks-proxy) +- [Proxy Bypass](#proxy-bypass) +- [Common Use Cases](#common-use-cases) +- [Verifying Proxy Connection](#verifying-proxy-connection) +- [Troubleshooting](#troubleshooting) +- [Best Practices](#best-practices) ## Basic Proxy Configuration -Set proxy via environment variable before starting: +Use the `--proxy` flag or set proxy via environment variable: ```bash -# HTTP proxy +# Via CLI flag +agent-browser --proxy "http://proxy.example.com:8080" open https://example.com + +# Via environment variable export HTTP_PROXY="http://proxy.example.com:8080" agent-browser open https://example.com @@ -45,10 +61,13 @@ agent-browser open https://example.com ## Proxy Bypass -Skip proxy for specific domains: +Skip proxy for specific domains using `--proxy-bypass` or `NO_PROXY`: ```bash -# Bypass proxy for local addresses +# Via CLI flag +agent-browser --proxy "http://proxy.example.com:8080" --proxy-bypass "localhost,*.internal.com" open https://example.com + +# Via environment variable export NO_PROXY="localhost,127.0.0.1,.internal.company.com" agent-browser open https://internal.company.com # Direct connection agent-browser open https://external.com # Via proxy diff --git a/.agents/skills/agent-browser/references/session-management.md b/.agents/skills/agent-browser/references/session-management.md index cfc3362455..bb5312dbdb 100644 --- a/.agents/skills/agent-browser/references/session-management.md +++ b/.agents/skills/agent-browser/references/session-management.md @@ -1,6 +1,18 @@ # Session Management -Run multiple isolated browser sessions concurrently with state persistence. +Multiple isolated browser sessions with state persistence and concurrent browsing. + +**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Named Sessions](#named-sessions) +- [Session Isolation Properties](#session-isolation-properties) +- [Session State Persistence](#session-state-persistence) +- [Common Patterns](#common-patterns) +- [Default Session](#default-session) +- [Session Cleanup](#session-cleanup) +- [Best Practices](#best-practices) ## Named Sessions diff --git a/.agents/skills/agent-browser/references/snapshot-refs.md b/.agents/skills/agent-browser/references/snapshot-refs.md index 0b17a4d43f..c5868d51cf 100644 --- a/.agents/skills/agent-browser/references/snapshot-refs.md +++ b/.agents/skills/agent-browser/references/snapshot-refs.md @@ -1,21 +1,29 @@ -# Snapshot + Refs Workflow +# Snapshot and Refs -The core innovation of agent-browser: compact element references that reduce context usage dramatically for AI agents. +Compact element references that reduce context usage dramatically for AI agents. -## How It Works +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. -### The Problem -Traditional browser automation sends full DOM to AI agents: +## Contents + +- [How Refs Work](#how-refs-work) +- [Snapshot Command](#the-snapshot-command) +- [Using Refs](#using-refs) +- [Ref Lifecycle](#ref-lifecycle) +- [Best Practices](#best-practices) +- [Ref Notation Details](#ref-notation-details) +- [Troubleshooting](#troubleshooting) + +## How Refs Work + +Traditional approach: ``` -Full DOM/HTML sent → AI parses → Generates CSS selector → Executes action -~3000-5000 tokens per interaction +Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens) ``` -### The Solution -agent-browser uses compact snapshots with refs: +agent-browser approach: ``` -Compact snapshot → @refs assigned → Direct ref interaction -~200-400 tokens per interaction +Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens) ``` ## The Snapshot Command @@ -166,8 +174,8 @@ agent-browser snapshot -i ### Element Not Visible in Snapshot ```bash -# Scroll to reveal element -agent-browser scroll --bottom +# Scroll down to reveal element +agent-browser scroll down 1000 agent-browser snapshot -i # Or wait for dynamic content diff --git a/.agents/skills/agent-browser/references/video-recording.md b/.agents/skills/agent-browser/references/video-recording.md index 98e6b0a16e..e6a9fb4e2f 100644 --- a/.agents/skills/agent-browser/references/video-recording.md +++ b/.agents/skills/agent-browser/references/video-recording.md @@ -1,6 +1,17 @@ # Video Recording -Capture browser automation sessions as video for debugging, documentation, or verification. +Capture browser automation as video for debugging, documentation, or verification. + +**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. + +## Contents + +- [Basic Recording](#basic-recording) +- [Recording Commands](#recording-commands) +- [Use Cases](#use-cases) +- [Best Practices](#best-practices) +- [Output Format](#output-format) +- [Limitations](#limitations) ## Basic Recording diff --git a/.agents/skills/agent-browser/templates/authenticated-session.sh b/.agents/skills/agent-browser/templates/authenticated-session.sh index e44aaad5d5..b66c9289c9 100755 --- a/.agents/skills/agent-browser/templates/authenticated-session.sh +++ b/.agents/skills/agent-browser/templates/authenticated-session.sh @@ -1,67 +1,81 @@ #!/bin/bash # Template: Authenticated Session Workflow -# Login once, save state, reuse for subsequent runs +# Purpose: Login once, save state, reuse for subsequent runs +# Usage: ./authenticated-session.sh [state-file] # -# Usage: -# ./authenticated-session.sh [state-file] +# RECOMMENDED: Use the auth vault instead of this template: +# echo "" | agent-browser auth save myapp --url --username --password-stdin +# agent-browser auth login myapp +# The auth vault stores credentials securely and the LLM never sees passwords. # -# Setup: -# 1. Run once to see your form structure -# 2. Note the @refs for your fields -# 3. Uncomment LOGIN FLOW section and update refs +# Environment variables: +# APP_USERNAME - Login username/email +# APP_PASSWORD - Login password +# +# Two modes: +# 1. Discovery mode (default): Shows form structure so you can identify refs +# 2. Login mode: Performs actual login after you update the refs +# +# Setup steps: +# 1. Run once to see form structure (discovery mode) +# 2. Update refs in LOGIN FLOW section below +# 3. Set APP_USERNAME and APP_PASSWORD +# 4. Delete the DISCOVERY section set -euo pipefail LOGIN_URL="${1:?Usage: $0 [state-file]}" STATE_FILE="${2:-./auth-state.json}" -echo "Authentication workflow for: $LOGIN_URL" +echo "Authentication workflow: $LOGIN_URL" -# ══════════════════════════════════════════════════════════════ -# SAVED STATE: Skip login if we have valid saved state -# ══════════════════════════════════════════════════════════════ +# ================================================================ +# SAVED STATE: Skip login if valid saved state exists +# ================================================================ if [[ -f "$STATE_FILE" ]]; then - echo "Loading saved authentication state..." - agent-browser state load "$STATE_FILE" - agent-browser open "$LOGIN_URL" - agent-browser wait --load networkidle + echo "Loading saved state from $STATE_FILE..." + if agent-browser --state "$STATE_FILE" open "$LOGIN_URL" 2>/dev/null; then + agent-browser wait --load networkidle - CURRENT_URL=$(agent-browser get url) - if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then - echo "Session restored successfully!" - agent-browser snapshot -i - exit 0 + CURRENT_URL=$(agent-browser get url) + if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then + echo "Session restored successfully" + agent-browser snapshot -i + exit 0 + fi + echo "Session expired, performing fresh login..." + agent-browser close 2>/dev/null || true + else + echo "Failed to load state, re-authenticating..." fi - echo "Session expired, performing fresh login..." rm -f "$STATE_FILE" fi -# ══════════════════════════════════════════════════════════════ -# DISCOVERY MODE: Show form structure (remove after setup) -# ══════════════════════════════════════════════════════════════ +# ================================================================ +# DISCOVERY MODE: Shows form structure (delete after setup) +# ================================================================ echo "Opening login page..." agent-browser open "$LOGIN_URL" agent-browser wait --load networkidle echo "" -echo "┌─────────────────────────────────────────────────────────┐" -echo "│ LOGIN FORM STRUCTURE │" -echo "├─────────────────────────────────────────────────────────┤" +echo "Login form structure:" +echo "---" agent-browser snapshot -i -echo "└─────────────────────────────────────────────────────────┘" +echo "---" echo "" echo "Next steps:" -echo " 1. Note refs: @e? = username, @e? = password, @e? = submit" -echo " 2. Uncomment LOGIN FLOW section below" -echo " 3. Replace @e1, @e2, @e3 with your refs" +echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?" +echo " 2. Update the LOGIN FLOW section below with your refs" +echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'" echo " 4. Delete this DISCOVERY MODE section" echo "" agent-browser close exit 0 -# ══════════════════════════════════════════════════════════════ +# ================================================================ # LOGIN FLOW: Uncomment and customize after discovery -# ══════════════════════════════════════════════════════════════ +# ================================================================ # : "${APP_USERNAME:?Set APP_USERNAME environment variable}" # : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}" # @@ -78,14 +92,14 @@ exit 0 # # Verify login succeeded # FINAL_URL=$(agent-browser get url) # if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then -# echo "ERROR: Login failed - still on login page" +# echo "Login failed - still on login page" # agent-browser screenshot /tmp/login-failed.png # agent-browser close # exit 1 # fi # # # Save state for future runs -# echo "Saving authentication state to: $STATE_FILE" +# echo "Saving state to $STATE_FILE" # agent-browser state save "$STATE_FILE" -# echo "Login successful!" +# echo "Login successful" # agent-browser snapshot -i diff --git a/.agents/skills/agent-browser/templates/capture-workflow.sh b/.agents/skills/agent-browser/templates/capture-workflow.sh index a4eae751ef..3bc93ad0c1 100755 --- a/.agents/skills/agent-browser/templates/capture-workflow.sh +++ b/.agents/skills/agent-browser/templates/capture-workflow.sh @@ -1,68 +1,69 @@ #!/bin/bash # Template: Content Capture Workflow -# Extract content from web pages with optional authentication +# Purpose: Extract content from web pages (text, screenshots, PDF) +# Usage: ./capture-workflow.sh [output-dir] +# +# Outputs: +# - page-full.png: Full page screenshot +# - page-structure.txt: Page element structure with refs +# - page-text.txt: All text content +# - page.pdf: PDF version +# +# Optional: Load auth state for protected pages set -euo pipefail TARGET_URL="${1:?Usage: $0 [output-dir]}" OUTPUT_DIR="${2:-.}" -echo "Capturing content from: $TARGET_URL" +echo "Capturing: $TARGET_URL" mkdir -p "$OUTPUT_DIR" -# Optional: Load authentication state if needed +# Optional: Load authentication state # if [[ -f "./auth-state.json" ]]; then +# echo "Loading authentication state..." # agent-browser state load "./auth-state.json" # fi -# Navigate to target page +# Navigate to target agent-browser open "$TARGET_URL" agent-browser wait --load networkidle -# Get page metadata -echo "Page title: $(agent-browser get title)" -echo "Page URL: $(agent-browser get url)" +# Get metadata +TITLE=$(agent-browser get title) +URL=$(agent-browser get url) +echo "Title: $TITLE" +echo "URL: $URL" # Capture full page screenshot agent-browser screenshot --full "$OUTPUT_DIR/page-full.png" -echo "Screenshot saved: $OUTPUT_DIR/page-full.png" +echo "Saved: $OUTPUT_DIR/page-full.png" -# Get page structure +# Get page structure with refs agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt" -echo "Structure saved: $OUTPUT_DIR/page-structure.txt" +echo "Saved: $OUTPUT_DIR/page-structure.txt" -# Extract main content -# Adjust selector based on target site structure -# agent-browser get text @e1 > "$OUTPUT_DIR/main-content.txt" - -# Extract specific elements (uncomment as needed) -# agent-browser get text "article" > "$OUTPUT_DIR/article.txt" -# agent-browser get text "main" > "$OUTPUT_DIR/main.txt" -# agent-browser get text ".content" > "$OUTPUT_DIR/content.txt" - -# Get full page text +# Extract all text content agent-browser get text body > "$OUTPUT_DIR/page-text.txt" -echo "Text content saved: $OUTPUT_DIR/page-text.txt" +echo "Saved: $OUTPUT_DIR/page-text.txt" -# Optional: Save as PDF +# Save as PDF agent-browser pdf "$OUTPUT_DIR/page.pdf" -echo "PDF saved: $OUTPUT_DIR/page.pdf" +echo "Saved: $OUTPUT_DIR/page.pdf" + +# Optional: Extract specific elements using refs from structure +# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt" -# Optional: Capture with scrolling for infinite scroll pages -# scroll_and_capture() { -# local count=0 -# while [[ $count -lt 5 ]]; do -# agent-browser scroll down 1000 -# agent-browser wait 1000 -# ((count++)) -# done -# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png" -# } -# scroll_and_capture +# Optional: Handle infinite scroll pages +# for i in {1..5}; do +# agent-browser scroll down 1000 +# agent-browser wait 1000 +# done +# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png" # Cleanup agent-browser close echo "" -echo "Capture complete! Files saved to: $OUTPUT_DIR" +echo "Capture complete:" ls -la "$OUTPUT_DIR" diff --git a/.agents/skills/agent-browser/templates/form-automation.sh b/.agents/skills/agent-browser/templates/form-automation.sh index 02a7c81154..6784fcd3a5 100755 --- a/.agents/skills/agent-browser/templates/form-automation.sh +++ b/.agents/skills/agent-browser/templates/form-automation.sh @@ -1,64 +1,62 @@ #!/bin/bash # Template: Form Automation Workflow -# Fills and submits web forms with validation +# Purpose: Fill and submit web forms with validation +# Usage: ./form-automation.sh +# +# This template demonstrates the snapshot-interact-verify pattern: +# 1. Navigate to form +# 2. Snapshot to get element refs +# 3. Fill fields using refs +# 4. Submit and verify result +# +# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output set -euo pipefail FORM_URL="${1:?Usage: $0 }" -echo "Automating form at: $FORM_URL" +echo "Form automation: $FORM_URL" -# Navigate to form page +# Step 1: Navigate to form agent-browser open "$FORM_URL" agent-browser wait --load networkidle -# Get interactive snapshot to identify form fields -echo "Analyzing form structure..." +# Step 2: Snapshot to discover form elements +echo "" +echo "Form structure:" agent-browser snapshot -i -# Example: Fill common form fields -# Uncomment and modify refs based on snapshot output - -# Text inputs -# agent-browser fill @e1 "John Doe" # Name field -# agent-browser fill @e2 "user@example.com" # Email field -# agent-browser fill @e3 "+1-555-123-4567" # Phone field - -# Password fields -# agent-browser fill @e4 "SecureP@ssw0rd!" - -# Dropdowns -# agent-browser select @e5 "Option Value" - -# Checkboxes -# agent-browser check @e6 # Check -# agent-browser uncheck @e7 # Uncheck - -# Radio buttons -# agent-browser click @e8 # Select radio option - -# Text areas -# agent-browser fill @e9 "Multi-line text content here" - -# File uploads -# agent-browser upload @e10 /path/to/file.pdf - -# Submit form -# agent-browser click @e11 # Submit button - -# Wait for response +# Step 3: Fill form fields (customize these refs based on snapshot output) +# +# Common field types: +# agent-browser fill @e1 "John Doe" # Text input +# agent-browser fill @e2 "user@example.com" # Email input +# agent-browser fill @e3 "SecureP@ss123" # Password input +# agent-browser select @e4 "Option Value" # Dropdown +# agent-browser check @e5 # Checkbox +# agent-browser click @e6 # Radio button +# agent-browser fill @e7 "Multi-line text" # Textarea +# agent-browser upload @e8 /path/to/file.pdf # File upload +# +# Uncomment and modify: +# agent-browser fill @e1 "Test User" +# agent-browser fill @e2 "test@example.com" +# agent-browser click @e3 # Submit button + +# Step 4: Wait for submission # agent-browser wait --load networkidle -# agent-browser wait --url "**/success" # Or wait for redirect +# agent-browser wait --url "**/success" # Or wait for redirect -# Verify submission -echo "Form submission result:" +# Step 5: Verify result +echo "" +echo "Result:" agent-browser get url agent-browser snapshot -i -# Take screenshot of result +# Optional: Capture evidence agent-browser screenshot /tmp/form-result.png +echo "Screenshot saved: /tmp/form-result.png" # Cleanup agent-browser close - -echo "Form automation complete" +echo "Done" diff --git a/.github/skills/backend-architecture/SKILL.md b/.agents/skills/backend-architecture/SKILL.md similarity index 100% rename from .github/skills/backend-architecture/SKILL.md rename to .agents/skills/backend-architecture/SKILL.md diff --git a/.github/skills/backend-testing/SKILL.md b/.agents/skills/backend-testing/SKILL.md similarity index 100% rename from .github/skills/backend-testing/SKILL.md rename to .agents/skills/backend-testing/SKILL.md diff --git a/.agents/skills/dogfood/SKILL.md b/.agents/skills/dogfood/SKILL.md new file mode 100644 index 0000000000..be25ce58d5 --- /dev/null +++ b/.agents/skills/dogfood/SKILL.md @@ -0,0 +1,216 @@ +--- +name: dogfood +description: Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to "dogfood", "QA", "exploratory test", "find issues", "bug hunt", "test this app/site/platform", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams. +allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*) +--- + +# Dogfood + +Systematically explore a web application, find issues, and produce a report with full reproduction evidence for every finding. + +## Setup + +Only the **Target URL** is required. Everything else has sensible defaults -- use them unless the user explicitly provides an override. + +| Parameter | Default | Example override | +|-----------|---------|-----------------| +| **Target URL** | _(required)_ | `vercel.com`, `http://localhost:3000` | +| **Session name** | Slugified domain (e.g., `vercel.com` -> `vercel-com`) | `--session my-session` | +| **Output directory** | `./dogfood-output/` | `Output directory: /tmp/qa` | +| **Scope** | Full app | `Focus on the billing page` | +| **Authentication** | None | `Sign in to user@example.com` | + +If the user says something like "dogfood vercel.com", start immediately with defaults. Do not ask clarifying questions unless authentication is mentioned but credentials are missing. + +Always use `agent-browser` directly -- never `npx agent-browser`. The direct binary uses the fast Rust client. `npx` routes through Node.js and is significantly slower. + +## Workflow + +``` +1. Initialize Set up session, output dirs, report file +2. Authenticate Sign in if needed, save state +3. Orient Navigate to starting point, take initial snapshot +4. Explore Systematically visit pages and test features +5. Document Screenshot + record each issue as found +6. Wrap up Update summary counts, close session +``` + +### 1. Initialize + +```bash +mkdir -p {OUTPUT_DIR}/screenshots {OUTPUT_DIR}/videos +``` + +Copy the report template into the output directory and fill in the header fields: + +```bash +cp {SKILL_DIR}/templates/dogfood-report-template.md {OUTPUT_DIR}/report.md +``` + +Start a named session: + +```bash +agent-browser --session {SESSION} open {TARGET_URL} +agent-browser --session {SESSION} wait --load networkidle +``` + +### 2. Authenticate + +If the app requires login: + +```bash +agent-browser --session {SESSION} snapshot -i +# Identify login form refs, fill credentials +agent-browser --session {SESSION} fill @e1 "{EMAIL}" +agent-browser --session {SESSION} fill @e2 "{PASSWORD}" +agent-browser --session {SESSION} click @e3 +agent-browser --session {SESSION} wait --load networkidle +``` + +For OTP/email codes: ask the user, wait for their response, then enter the code. + +After successful login, save state for potential reuse: + +```bash +agent-browser --session {SESSION} state save {OUTPUT_DIR}/auth-state.json +``` + +### 3. Orient + +Take an initial annotated screenshot and snapshot to understand the app structure: + +```bash +agent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/initial.png +agent-browser --session {SESSION} snapshot -i +``` + +Identify the main navigation elements and map out the sections to visit. + +### 4. Explore + +Read [references/issue-taxonomy.md](references/issue-taxonomy.md) for the full list of what to look for and the exploration checklist. + +**Strategy -- work through the app systematically:** + +- Start from the main navigation. Visit each top-level section. +- Within each section, test interactive elements: click buttons, fill forms, open dropdowns/modals. +- Check edge cases: empty states, error handling, boundary inputs. +- Try realistic end-to-end workflows (create, edit, delete flows). +- Check the browser console for errors periodically. + +**At each page:** + +```bash +agent-browser --session {SESSION} snapshot -i +agent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/{page-name}.png +agent-browser --session {SESSION} errors +agent-browser --session {SESSION} console +``` + +Use your judgment on how deep to go. Spend more time on core features and less on peripheral pages. If you find a cluster of issues in one area, investigate deeper. + +### 5. Document Issues (Repro-First) + +Steps 4 and 5 happen together -- explore and document in a single pass. When you find an issue, stop exploring and document it immediately before moving on. Do not explore the whole app first and document later. + +Every issue must be reproducible. When you find something wrong, do not just note it -- prove it with evidence. The goal is that someone reading the report can see exactly what happened and replay it. + +**Choose the right level of evidence for the issue:** + +#### Interactive / behavioral issues (functional, ux, console errors on action) + +These require user interaction to reproduce -- use full repro with video and step-by-step screenshots: + +1. **Start a repro video** _before_ reproducing: + +```bash +agent-browser --session {SESSION} record start {OUTPUT_DIR}/videos/issue-{NNN}-repro.webm +``` + +2. **Walk through the steps at human pace.** Pause 1-2 seconds between actions so the video is watchable. Take a screenshot at each step: + +```bash +agent-browser --session {SESSION} screenshot {OUTPUT_DIR}/screenshots/issue-{NNN}-step-1.png +sleep 1 +# Perform action (click, fill, etc.) +sleep 1 +agent-browser --session {SESSION} screenshot {OUTPUT_DIR}/screenshots/issue-{NNN}-step-2.png +sleep 1 +# ...continue until the issue manifests +``` + +3. **Capture the broken state.** Pause so the viewer can see it, then take an annotated screenshot: + +```bash +sleep 2 +agent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/issue-{NNN}-result.png +``` + +4. **Stop the video:** + +```bash +agent-browser --session {SESSION} record stop +``` + +5. Write numbered repro steps in the report, each referencing its screenshot. + +#### Static / visible-on-load issues (typos, placeholder text, clipped text, misalignment, console errors on load) + +These are visible without interaction -- a single annotated screenshot is sufficient. No video, no multi-step repro: + +```bash +agent-browser --session {SESSION} screenshot --annotate {OUTPUT_DIR}/screenshots/issue-{NNN}.png +``` + +Write a brief description and reference the screenshot in the report. Set **Repro Video** to `N/A`. + +--- + +**For all issues:** + +1. **Append to the report immediately.** Do not batch issues for later. Write each one as you find it so nothing is lost if the session is interrupted. + +2. **Increment the issue counter** (ISSUE-001, ISSUE-002, ...). + +### 6. Wrap Up + +Aim to find **5-10 well-documented issues**, then wrap up. Depth of evidence matters more than total count -- 5 issues with full repro beats 20 with vague descriptions. + +After exploring: + +1. Re-read the report and update the summary severity counts so they match the actual issues. Every `### ISSUE-` block must be reflected in the totals. +2. Close the session: + +```bash +agent-browser --session {SESSION} close +``` + +3. Tell the user the report is ready and summarize findings: total issues, breakdown by severity, and the most critical items. + +## Guidance + +- **Repro is everything.** Every issue needs proof -- but match the evidence to the issue. Interactive bugs need video and step-by-step screenshots. Static bugs (typos, placeholder text, visual glitches visible on load) only need a single annotated screenshot. +- **Don't record video for static issues.** A typo or clipped text doesn't benefit from a video. Save video for issues that involve user interaction, timing, or state changes. +- **For interactive issues, screenshot each step.** Capture the before, the action, and the after -- so someone can see the full sequence. +- **Write repro steps that map to screenshots.** Each numbered step in the report should reference its corresponding screenshot. A reader should be able to follow the steps visually without touching a browser. +- **Be thorough but use judgment.** You are not following a test script -- you are exploring like a real user would. If something feels off, investigate. +- **Write findings incrementally.** Append each issue to the report as you discover it. If the session is interrupted, findings are preserved. Never batch all issues for the end. +- **Never delete output files.** Do not `rm` screenshots, videos, or the report mid-session. Do not close the session and restart. Work forward, not backward. +- **Never read the target app's source code.** You are testing as a user, not auditing code. Do not read HTML, JS, or config files of the app under test. All findings must come from what you observe in the browser. +- **Check the console.** Many issues are invisible in the UI but show up as JS errors or failed requests. +- **Test like a user, not a robot.** Try common workflows end-to-end. Click things a real user would click. Enter realistic data. +- **Type like a human.** When filling form fields during video recording, use `type` instead of `fill` -- it types character-by-character. Use `fill` only outside of video recording when speed matters. +- **Pace repro videos for humans.** Add `sleep 1` between actions and `sleep 2` before the final result screenshot. Videos should be watchable at 1x speed -- a human reviewing the report needs to see what happened, not a blur of instant state changes. +- **Be efficient with commands.** Batch multiple `agent-browser` commands in a single shell call when they are independent (e.g., `agent-browser ... screenshot ... && agent-browser ... console`). Use `agent-browser --session {SESSION} scroll down 300` for scrolling -- do not use `key` or `evaluate` to scroll. + +## References + +| Reference | When to Read | +|-----------|--------------| +| [references/issue-taxonomy.md](references/issue-taxonomy.md) | Start of session -- calibrate what to look for, severity levels, exploration checklist | + +## Templates + +| Template | Purpose | +|----------|---------| +| [templates/dogfood-report-template.md](templates/dogfood-report-template.md) | Copy into output directory as the report file | diff --git a/.agents/skills/dogfood/references/issue-taxonomy.md b/.agents/skills/dogfood/references/issue-taxonomy.md new file mode 100644 index 0000000000..c3edbe5733 --- /dev/null +++ b/.agents/skills/dogfood/references/issue-taxonomy.md @@ -0,0 +1,109 @@ +# Issue Taxonomy + +Reference for categorizing issues found during dogfooding. Read this at the start of a dogfood session to calibrate what to look for. + +## Contents + +- [Severity Levels](#severity-levels) +- [Categories](#categories) +- [Exploration Checklist](#exploration-checklist) + +## Severity Levels + +| Severity | Definition | +|----------|------------| +| **critical** | Blocks a core workflow, causes data loss, or crashes the app | +| **high** | Major feature broken or unusable, no workaround | +| **medium** | Feature works but with noticeable problems, workaround exists | +| **low** | Minor cosmetic or polish issue | + +## Categories + +### Visual / UI + +- Layout broken or misaligned elements +- Overlapping or clipped text +- Inconsistent spacing, padding, or margins +- Missing or broken icons/images +- Dark mode / light mode rendering issues +- Responsive layout problems (viewport sizes) +- Z-index stacking issues (elements hidden behind others) +- Font rendering issues (wrong font, size, weight) +- Color contrast problems +- Animation glitches or jank + +### Functional + +- Broken links (404, wrong destination) +- Buttons or controls that do nothing on click +- Form validation that rejects valid input or accepts invalid input +- Incorrect redirects +- Features that fail silently +- State not persisted when expected (lost on refresh, navigation) +- Race conditions (double-submit, stale data) +- Broken search or filtering +- Pagination issues +- File upload/download failures + +### UX + +- Confusing or unclear navigation +- Missing loading indicators or feedback after actions +- Slow or unresponsive interactions (>300ms perceived delay) +- Unclear error messages +- Missing confirmation for destructive actions +- Dead ends (no way to go back or proceed) +- Inconsistent patterns across similar features +- Missing keyboard shortcuts or focus management +- Unintuitive defaults +- Missing empty states or unhelpful empty states + +### Content + +- Typos or grammatical errors +- Outdated or incorrect text +- Placeholder or lorem ipsum content left in +- Truncated text without tooltip or expansion +- Missing or wrong labels +- Inconsistent terminology + +### Performance + +- Slow page loads (>3s) +- Janky scrolling or animations +- Large layout shifts (content jumping) +- Excessive network requests (check via console/network) +- Memory leaks (page slows over time) +- Unoptimized images (large file sizes) + +### Console / Errors + +- JavaScript exceptions in console +- Failed network requests (4xx, 5xx) +- Deprecation warnings +- CORS errors +- Mixed content warnings +- Unhandled promise rejections + +### Accessibility + +- Missing alt text on images +- Unlabeled form inputs +- Poor keyboard navigation (can't tab to elements) +- Focus traps +- Insufficient color contrast +- Missing ARIA attributes on dynamic content +- Screen reader incompatible patterns + +## Exploration Checklist + +Use this as a guide for what to test on each page/feature: + +1. **Visual scan** -- Take an annotated screenshot. Look for layout, alignment, and rendering issues. +2. **Interactive elements** -- Click every button, link, and control. Do they work? Is there feedback? +3. **Forms** -- Fill and submit. Test empty submission, invalid input, and edge cases. +4. **Navigation** -- Follow all navigation paths. Check breadcrumbs, back button, deep links. +5. **States** -- Check empty states, loading states, error states, and full/overflow states. +6. **Console** -- Check for JS errors, failed requests, and warnings. +7. **Responsiveness** -- If relevant, test at different viewport sizes. +8. **Auth boundaries** -- Test what happens when not logged in, with different roles if applicable. diff --git a/.agents/skills/dogfood/templates/dogfood-report-template.md b/.agents/skills/dogfood/templates/dogfood-report-template.md new file mode 100644 index 0000000000..a7732a409f --- /dev/null +++ b/.agents/skills/dogfood/templates/dogfood-report-template.md @@ -0,0 +1,53 @@ +# Dogfood Report: {APP_NAME} + +| Field | Value | +|-------|-------| +| **Date** | {DATE} | +| **App URL** | {URL} | +| **Session** | {SESSION_NAME} | +| **Scope** | {SCOPE} | + +## Summary + +| Severity | Count | +|----------|-------| +| Critical | 0 | +| High | 0 | +| Medium | 0 | +| Low | 0 | +| **Total** | **0** | + +## Issues + + + +### ISSUE-001: {Short title} + +| Field | Value | +|-------|-------| +| **Severity** | critical / high / medium / low | +| **Category** | visual / functional / ux / content / performance / console / accessibility | +| **URL** | {page URL where issue was found} | +| **Repro Video** | {path to video, or N/A for static issues} | + +**Description** + +{What is wrong, what was expected, and what actually happened.} + +**Repro Steps** + + + +1. Navigate to {URL} + ![Step 1](screenshots/issue-001-step-1.png) + +2. {Action -- e.g., click "Settings" in the sidebar} + ![Step 2](screenshots/issue-001-step-2.png) + +3. {Action -- e.g., type "test" in the search field and press Enter} + ![Step 3](screenshots/issue-001-step-3.png) + +4. **Observe:** {what goes wrong -- e.g., the page shows a blank white screen instead of search results} + ![Result](screenshots/issue-001-result.png) + +--- diff --git a/.github/skills/dotnet-cli/SKILL.md b/.agents/skills/dotnet-cli/SKILL.md similarity index 100% rename from .github/skills/dotnet-cli/SKILL.md rename to .agents/skills/dotnet-cli/SKILL.md diff --git a/.github/skills/dotnet-conventions/SKILL.md b/.agents/skills/dotnet-conventions/SKILL.md similarity index 100% rename from .github/skills/dotnet-conventions/SKILL.md rename to .agents/skills/dotnet-conventions/SKILL.md diff --git a/.github/skills/e2e-testing/SKILL.md b/.agents/skills/e2e-testing/SKILL.md similarity index 100% rename from .github/skills/e2e-testing/SKILL.md rename to .agents/skills/e2e-testing/SKILL.md diff --git a/.github/skills/foundatio/SKILL.md b/.agents/skills/foundatio/SKILL.md similarity index 100% rename from .github/skills/foundatio/SKILL.md rename to .agents/skills/foundatio/SKILL.md diff --git a/.github/skills/frontend-architecture/SKILL.md b/.agents/skills/frontend-architecture/SKILL.md similarity index 100% rename from .github/skills/frontend-architecture/SKILL.md rename to .agents/skills/frontend-architecture/SKILL.md diff --git a/.github/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md similarity index 100% rename from .github/skills/frontend-testing/SKILL.md rename to .agents/skills/frontend-testing/SKILL.md diff --git a/.github/.agents/skills/releasenotes/SKILL.md b/.agents/skills/releasenotes/README.md similarity index 66% rename from .github/.agents/skills/releasenotes/SKILL.md rename to .agents/skills/releasenotes/README.md index e2d1c5c5fe..cba536149b 100644 --- a/.github/.agents/skills/releasenotes/SKILL.md +++ b/.agents/skills/releasenotes/README.md @@ -1,9 +1,14 @@ ---- -name: releasenotes -description: Generate formatted changelogs from git history since the last release tag. Use when preparing release notes that categorize changes into breaking changes, features, fixes, and other sections. -triggers: -- /releasenotes ---- +# Releasenotes + +Generate formatted changelogs from git history since the last release tag. Use when preparing release notes that categorize changes into breaking changes, features, fixes, and other sections. + +## Triggers + +This skill is activated by the following keywords: + +- `/releasenotes` + +## Details Generate a changelog for all changes from the most recent release until now. diff --git a/.github/skills/security-principles/SKILL.md b/.agents/skills/security-principles/SKILL.md similarity index 100% rename from .github/skills/security-principles/SKILL.md rename to .agents/skills/security-principles/SKILL.md diff --git a/.github/skills/shadcn-svelte/SKILL.md b/.agents/skills/shadcn-svelte/SKILL.md similarity index 100% rename from .github/skills/shadcn-svelte/SKILL.md rename to .agents/skills/shadcn-svelte/SKILL.md diff --git a/.github/skills/storybook/SKILL.md b/.agents/skills/storybook/SKILL.md similarity index 100% rename from .github/skills/storybook/SKILL.md rename to .agents/skills/storybook/SKILL.md diff --git a/.agents/skills/stripe-best-practices/SKILL.md b/.agents/skills/stripe-best-practices/SKILL.md index d1fb280af2..dac0851d45 100644 --- a/.agents/skills/stripe-best-practices/SKILL.md +++ b/.agents/skills/stripe-best-practices/SKILL.md @@ -1,8 +1,11 @@ --- name: stripe-best-practices -description: Best practices for building a Stripe integrations +description: Best practices for building Stripe integrations +alwaysApply: false --- +The latest Stripe API version is 2026-02-25.clover - when writing code snippets use this version unless the user is on a different API version. + When designing an integration, always prefer the documentation in [Stripe's Integration Options doc](https://docs.stripe.com/payments/payment-methods/integration-options.md) The [API Tour](https://docs.stripe.com/payments-api/tour.md) Use the [Go Live Checklist](https://docs.stripe.com/get-started/checklist/go-live.md) before going live. diff --git a/.github/skills/svelte-components/SKILL.md b/.agents/skills/svelte-components/SKILL.md similarity index 100% rename from .github/skills/svelte-components/SKILL.md rename to .agents/skills/svelte-components/SKILL.md diff --git a/.github/skills/tanstack-form/SKILL.md b/.agents/skills/tanstack-form/SKILL.md similarity index 100% rename from .github/skills/tanstack-form/SKILL.md rename to .agents/skills/tanstack-form/SKILL.md diff --git a/.github/skills/tanstack-query/SKILL.md b/.agents/skills/tanstack-query/SKILL.md similarity index 100% rename from .github/skills/tanstack-query/SKILL.md rename to .agents/skills/tanstack-query/SKILL.md diff --git a/.github/skills/typescript-conventions/SKILL.md b/.agents/skills/typescript-conventions/SKILL.md similarity index 100% rename from .github/skills/typescript-conventions/SKILL.md rename to .agents/skills/typescript-conventions/SKILL.md diff --git a/.agents/skills/upgrade-stripe/SKILL.md b/.agents/skills/upgrade-stripe/SKILL.md index 868ab9cf8e..8c7a8ce2b9 100644 --- a/.agents/skills/upgrade-stripe/SKILL.md +++ b/.agents/skills/upgrade-stripe/SKILL.md @@ -1,15 +1,18 @@ --- name: upgrade-stripe description: Guide for upgrading Stripe API versions and SDKs +alwaysApply: false --- +The latest Stripe API version is 2026-02-25.clover - use this version when upgrading unless the user specifies a different target version. + # Upgrading Stripe Versions -This skill covers upgrading Stripe API versions, server-side SDKs, Stripe.js, and mobile SDKs. +This guide covers upgrading Stripe API versions, server-side SDKs, Stripe.js, and mobile SDKs. ## Understanding Stripe API Versioning -Stripe uses date-based API versions (e.g., `2025-12-15.clover`, `2025-08-27.basil`, `2024-12-18.acacia`). Your account's API version determines request/response behavior. +Stripe uses date-based API versions (e.g., `2026-02-25.clover`, `2025-08-27.basil`, `2024-12-18.acacia`). Your account's API version determines request/response behavior. ### Types of Changes @@ -38,16 +41,16 @@ These SDKs offer flexible version control: **Global Configuration:** ```python import stripe -stripe.api_version = '2025-12-15.clover' +stripe.api_version = '2026-02-25.clover' ``` ```ruby -Stripe.api_version = '2025-12-15.clover' +Stripe.api_version = '2026-02-25.clover' ``` ```javascript const stripe = require('stripe')('sk_test_xxx', { - apiVersion: '2025-12-15.clover' + apiVersion: '2026-02-25.clover' }); ``` @@ -55,7 +58,7 @@ const stripe = require('stripe')('sk_test_xxx', { ```python stripe.Customer.create( email="customer@example.com", - stripe_version='2025-12-15.clover' + stripe_version='2026-02-25.clover' ) ``` @@ -70,7 +73,7 @@ Always specify the API version you're integrating against in your code instead o ```javascript // Good: Explicit version const stripe = require('stripe')('sk_test_xxx', { - apiVersion: '2025-12-15.clover' + apiVersion: '2026-02-25.clover' }); // Avoid: Relying on account default @@ -100,7 +103,7 @@ Major npm versions correspond to specific Stripe.js versions. ### API Version Pairing Each Stripe.js version automatically pairs with its corresponding API version. For instance: -- Clover Stripe.js uses `2025-12-15.clover` API +- Clover Stripe.js uses `2026-02-25.clover` API - Acacia Stripe.js uses `2024-12-18.acacia` API You cannot override this association. @@ -154,14 +157,14 @@ Use the `Stripe-Version` header to test your code against a new version without ```bash curl https://api.stripe.com/v1/customers \ -u sk_test_xxx: \ - -H "Stripe-Version: 2025-12-15.clover" + -H "Stripe-Version: 2026-02-25.clover" ``` Or in code: ```javascript const stripe = require('stripe')('sk_test_xxx', { - apiVersion: '2025-12-15.clover' // Test with new version + apiVersion: '2026-02-25.clover' // Test with new version }); ``` @@ -171,3 +174,4 @@ const stripe = require('stripe')('sk_test_xxx', { - Test webhooks with the new version structure before upgrading - Breaking changes are tagged by affected product areas (Payments, Billing, Connect, etc.) - Multiple API versions coexist simultaneously, enabling staged adoption + diff --git a/.github/.agents/skills/agent-browser/SKILL.md b/.github/.agents/skills/agent-browser/SKILL.md deleted file mode 100644 index b2f9b6ec2b..0000000000 --- a/.github/.agents/skills/agent-browser/SKILL.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -name: agent-browser -description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. -allowed-tools: Bash(agent-browser:*) ---- - -# Browser Automation with agent-browser - -## Core Workflow - -Every browser automation follows this pattern: - -1. **Navigate**: `agent-browser open ` -2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`) -3. **Interact**: Use refs to click, fill, select -4. **Re-snapshot**: After navigation or DOM changes, get fresh refs - -```bash -agent-browser open https://example.com/form -agent-browser snapshot -i -# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit" - -agent-browser fill @e1 "user@example.com" -agent-browser fill @e2 "password123" -agent-browser click @e3 -agent-browser wait --load networkidle -agent-browser snapshot -i # Check result -``` - -## Essential Commands - -```bash -# Navigation -agent-browser open # Navigate (aliases: goto, navigate) -agent-browser close # Close browser - -# Snapshot -agent-browser snapshot -i # Interactive elements with refs (recommended) -agent-browser snapshot -s "#selector" # Scope to CSS selector - -# Interaction (use @refs from snapshot) -agent-browser click @e1 # Click element -agent-browser fill @e2 "text" # Clear and type text -agent-browser type @e2 "text" # Type without clearing -agent-browser select @e1 "option" # Select dropdown option -agent-browser check @e1 # Check checkbox -agent-browser press Enter # Press key -agent-browser scroll down 500 # Scroll page - -# Get information -agent-browser get text @e1 # Get element text -agent-browser get url # Get current URL -agent-browser get title # Get page title - -# Wait -agent-browser wait @e1 # Wait for element -agent-browser wait --load networkidle # Wait for network idle -agent-browser wait --url "**/page" # Wait for URL pattern -agent-browser wait 2000 # Wait milliseconds - -# Capture -agent-browser screenshot # Screenshot to temp dir -agent-browser screenshot --full # Full page screenshot -agent-browser pdf output.pdf # Save as PDF -``` - -## Common Patterns - -### Form Submission - -```bash -agent-browser open https://example.com/signup -agent-browser snapshot -i -agent-browser fill @e1 "Jane Doe" -agent-browser fill @e2 "jane@example.com" -agent-browser select @e3 "California" -agent-browser check @e4 -agent-browser click @e5 -agent-browser wait --load networkidle -``` - -### Authentication with State Persistence - -```bash -# Login once and save state -agent-browser open https://app.example.com/login -agent-browser snapshot -i -agent-browser fill @e1 "$USERNAME" -agent-browser fill @e2 "$PASSWORD" -agent-browser click @e3 -agent-browser wait --url "**/dashboard" -agent-browser state save auth.json - -# Reuse in future sessions -agent-browser state load auth.json -agent-browser open https://app.example.com/dashboard -``` - -### Data Extraction - -```bash -agent-browser open https://example.com/products -agent-browser snapshot -i -agent-browser get text @e5 # Get specific element text -agent-browser get text body > page.txt # Get all page text - -# JSON output for parsing -agent-browser snapshot -i --json -agent-browser get text @e1 --json -``` - -### Parallel Sessions - -```bash -agent-browser --session site1 open https://site-a.com -agent-browser --session site2 open https://site-b.com - -agent-browser --session site1 snapshot -i -agent-browser --session site2 snapshot -i - -agent-browser session list -``` - -### Visual Browser (Debugging) - -```bash -agent-browser --headed open https://example.com -agent-browser highlight @e1 # Highlight element -agent-browser record start demo.webm # Record session -``` - -## Ref Lifecycle (Important) - -Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after: - -- Clicking links or buttons that navigate -- Form submissions -- Dynamic content loading (dropdowns, modals) - -```bash -agent-browser click @e5 # Navigates to new page -agent-browser snapshot -i # MUST re-snapshot -agent-browser click @e1 # Use new refs -``` - -## Semantic Locators (Alternative to Refs) - -When refs are unavailable or unreliable, use semantic locators: - -```bash -agent-browser find text "Sign In" click -agent-browser find label "Email" fill "user@test.com" -agent-browser find role button click --name "Submit" -agent-browser find placeholder "Search" type "query" -agent-browser find testid "submit-btn" click -``` - -## Deep-Dive Documentation - -| Reference | When to Use | -|-----------|-------------| -| [references/commands.md](references/commands.md) | Full command reference with all options | -| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting | -| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping | -| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse | -| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation | -| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies | - -## Ready-to-Use Templates - -| Template | Description | -|----------|-------------| -| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation | -| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state | -| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots | - -```bash -./templates/form-automation.sh https://example.com/form -./templates/authenticated-session.sh https://app.example.com/login -./templates/capture-workflow.sh https://example.com ./output -``` diff --git a/.github/.agents/skills/agent-browser/references/authentication.md b/.github/.agents/skills/agent-browser/references/authentication.md deleted file mode 100644 index 12ef5e41be..0000000000 --- a/.github/.agents/skills/agent-browser/references/authentication.md +++ /dev/null @@ -1,202 +0,0 @@ -# Authentication Patterns - -Login flows, session persistence, OAuth, 2FA, and authenticated browsing. - -**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start. - -## Contents - -- [Basic Login Flow](#basic-login-flow) -- [Saving Authentication State](#saving-authentication-state) -- [Restoring Authentication](#restoring-authentication) -- [OAuth / SSO Flows](#oauth--sso-flows) -- [Two-Factor Authentication](#two-factor-authentication) -- [HTTP Basic Auth](#http-basic-auth) -- [Cookie-Based Auth](#cookie-based-auth) -- [Token Refresh Handling](#token-refresh-handling) -- [Security Best Practices](#security-best-practices) - -## Basic Login Flow - -```bash -# Navigate to login page -agent-browser open https://app.example.com/login -agent-browser wait --load networkidle - -# Get form elements -agent-browser snapshot -i -# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Sign In" - -# Fill credentials -agent-browser fill @e1 "user@example.com" -agent-browser fill @e2 "password123" - -# Submit -agent-browser click @e3 -agent-browser wait --load networkidle - -# Verify login succeeded -agent-browser get url # Should be dashboard, not login -``` - -## Saving Authentication State - -After logging in, save state for reuse: - -```bash -# Login first (see above) -agent-browser open https://app.example.com/login -agent-browser snapshot -i -agent-browser fill @e1 "user@example.com" -agent-browser fill @e2 "password123" -agent-browser click @e3 -agent-browser wait --url "**/dashboard" - -# Save authenticated state -agent-browser state save ./auth-state.json -``` - -## Restoring Authentication - -Skip login by loading saved state: - -```bash -# Load saved auth state -agent-browser state load ./auth-state.json - -# Navigate directly to protected page -agent-browser open https://app.example.com/dashboard - -# Verify authenticated -agent-browser snapshot -i -``` - -## OAuth / SSO Flows - -For OAuth redirects: - -```bash -# Start OAuth flow -agent-browser open https://app.example.com/auth/google - -# Handle redirects automatically -agent-browser wait --url "**/accounts.google.com**" -agent-browser snapshot -i - -# Fill Google credentials -agent-browser fill @e1 "user@gmail.com" -agent-browser click @e2 # Next button -agent-browser wait 2000 -agent-browser snapshot -i -agent-browser fill @e3 "password" -agent-browser click @e4 # Sign in - -# Wait for redirect back -agent-browser wait --url "**/app.example.com**" -agent-browser state save ./oauth-state.json -``` - -## Two-Factor Authentication - -Handle 2FA with manual intervention: - -```bash -# Login with credentials -agent-browser open https://app.example.com/login --headed # Show browser -agent-browser snapshot -i -agent-browser fill @e1 "user@example.com" -agent-browser fill @e2 "password123" -agent-browser click @e3 - -# Wait for user to complete 2FA manually -echo "Complete 2FA in the browser window..." -agent-browser wait --url "**/dashboard" --timeout 120000 - -# Save state after 2FA -agent-browser state save ./2fa-state.json -``` - -## HTTP Basic Auth - -For sites using HTTP Basic Authentication: - -```bash -# Set credentials before navigation -agent-browser set credentials username password - -# Navigate to protected resource -agent-browser open https://protected.example.com/api -``` - -## Cookie-Based Auth - -Manually set authentication cookies: - -```bash -# Set auth cookie -agent-browser cookies set session_token "abc123xyz" - -# Navigate to protected page -agent-browser open https://app.example.com/dashboard -``` - -## Token Refresh Handling - -For sessions with expiring tokens: - -```bash -#!/bin/bash -# Wrapper that handles token refresh - -STATE_FILE="./auth-state.json" - -# Try loading existing state -if [[ -f "$STATE_FILE" ]]; then - agent-browser state load "$STATE_FILE" - agent-browser open https://app.example.com/dashboard - - # Check if session is still valid - URL=$(agent-browser get url) - if [[ "$URL" == *"/login"* ]]; then - echo "Session expired, re-authenticating..." - # Perform fresh login - agent-browser snapshot -i - agent-browser fill @e1 "$USERNAME" - agent-browser fill @e2 "$PASSWORD" - agent-browser click @e3 - agent-browser wait --url "**/dashboard" - agent-browser state save "$STATE_FILE" - fi -else - # First-time login - agent-browser open https://app.example.com/login - # ... login flow ... -fi -``` - -## Security Best Practices - -1. **Never commit state files** - They contain session tokens - ```bash - echo "*.auth-state.json" >> .gitignore - ``` - -2. **Use environment variables for credentials** - ```bash - agent-browser fill @e1 "$APP_USERNAME" - agent-browser fill @e2 "$APP_PASSWORD" - ``` - -3. **Clean up after automation** - ```bash - agent-browser cookies clear - rm -f ./auth-state.json - ``` - -4. **Use short-lived sessions for CI/CD** - ```bash - # Don't persist state in CI - agent-browser open https://app.example.com/login - # ... login and perform actions ... - agent-browser close # Session ends, nothing persisted - ``` diff --git a/.github/.agents/skills/agent-browser/references/proxy-support.md b/.github/.agents/skills/agent-browser/references/proxy-support.md deleted file mode 100644 index 05cc9d538f..0000000000 --- a/.github/.agents/skills/agent-browser/references/proxy-support.md +++ /dev/null @@ -1,188 +0,0 @@ -# Proxy Support - -Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments. - -**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start. - -## Contents - -- [Basic Proxy Configuration](#basic-proxy-configuration) -- [Authenticated Proxy](#authenticated-proxy) -- [SOCKS Proxy](#socks-proxy) -- [Proxy Bypass](#proxy-bypass) -- [Common Use Cases](#common-use-cases) -- [Verifying Proxy Connection](#verifying-proxy-connection) -- [Troubleshooting](#troubleshooting) -- [Best Practices](#best-practices) - -## Basic Proxy Configuration - -Set proxy via environment variable before starting: - -```bash -# HTTP proxy -export HTTP_PROXY="http://proxy.example.com:8080" -agent-browser open https://example.com - -# HTTPS proxy -export HTTPS_PROXY="https://proxy.example.com:8080" -agent-browser open https://example.com - -# Both -export HTTP_PROXY="http://proxy.example.com:8080" -export HTTPS_PROXY="http://proxy.example.com:8080" -agent-browser open https://example.com -``` - -## Authenticated Proxy - -For proxies requiring authentication: - -```bash -# Include credentials in URL -export HTTP_PROXY="http://username:password@proxy.example.com:8080" -agent-browser open https://example.com -``` - -## SOCKS Proxy - -```bash -# SOCKS5 proxy -export ALL_PROXY="socks5://proxy.example.com:1080" -agent-browser open https://example.com - -# SOCKS5 with auth -export ALL_PROXY="socks5://user:pass@proxy.example.com:1080" -agent-browser open https://example.com -``` - -## Proxy Bypass - -Skip proxy for specific domains: - -```bash -# Bypass proxy for local addresses -export NO_PROXY="localhost,127.0.0.1,.internal.company.com" -agent-browser open https://internal.company.com # Direct connection -agent-browser open https://external.com # Via proxy -``` - -## Common Use Cases - -### Geo-Location Testing - -```bash -#!/bin/bash -# Test site from different regions using geo-located proxies - -PROXIES=( - "http://us-proxy.example.com:8080" - "http://eu-proxy.example.com:8080" - "http://asia-proxy.example.com:8080" -) - -for proxy in "${PROXIES[@]}"; do - export HTTP_PROXY="$proxy" - export HTTPS_PROXY="$proxy" - - region=$(echo "$proxy" | grep -oP '^\w+-\w+') - echo "Testing from: $region" - - agent-browser --session "$region" open https://example.com - agent-browser --session "$region" screenshot "./screenshots/$region.png" - agent-browser --session "$region" close -done -``` - -### Rotating Proxies for Scraping - -```bash -#!/bin/bash -# Rotate through proxy list to avoid rate limiting - -PROXY_LIST=( - "http://proxy1.example.com:8080" - "http://proxy2.example.com:8080" - "http://proxy3.example.com:8080" -) - -URLS=( - "https://site.com/page1" - "https://site.com/page2" - "https://site.com/page3" -) - -for i in "${!URLS[@]}"; do - proxy_index=$((i % ${#PROXY_LIST[@]})) - export HTTP_PROXY="${PROXY_LIST[$proxy_index]}" - export HTTPS_PROXY="${PROXY_LIST[$proxy_index]}" - - agent-browser open "${URLS[$i]}" - agent-browser get text body > "output-$i.txt" - agent-browser close - - sleep 1 # Polite delay -done -``` - -### Corporate Network Access - -```bash -#!/bin/bash -# Access internal sites via corporate proxy - -export HTTP_PROXY="http://corpproxy.company.com:8080" -export HTTPS_PROXY="http://corpproxy.company.com:8080" -export NO_PROXY="localhost,127.0.0.1,.company.com" - -# External sites go through proxy -agent-browser open https://external-vendor.com - -# Internal sites bypass proxy -agent-browser open https://intranet.company.com -``` - -## Verifying Proxy Connection - -```bash -# Check your apparent IP -agent-browser open https://httpbin.org/ip -agent-browser get text body -# Should show proxy's IP, not your real IP -``` - -## Troubleshooting - -### Proxy Connection Failed - -```bash -# Test proxy connectivity first -curl -x http://proxy.example.com:8080 https://httpbin.org/ip - -# Check if proxy requires auth -export HTTP_PROXY="http://user:pass@proxy.example.com:8080" -``` - -### SSL/TLS Errors Through Proxy - -Some proxies perform SSL inspection. If you encounter certificate errors: - -```bash -# For testing only - not recommended for production -agent-browser open https://example.com --ignore-https-errors -``` - -### Slow Performance - -```bash -# Use proxy only when necessary -export NO_PROXY="*.cdn.com,*.static.com" # Direct CDN access -``` - -## Best Practices - -1. **Use environment variables** - Don't hardcode proxy credentials -2. **Set NO_PROXY appropriately** - Avoid routing local traffic through proxy -3. **Test proxy before automation** - Verify connectivity with simple requests -4. **Handle proxy failures gracefully** - Implement retry logic for unstable proxies -5. **Rotate proxies for large scraping jobs** - Distribute load and avoid bans diff --git a/.github/.agents/skills/agent-browser/references/session-management.md b/.github/.agents/skills/agent-browser/references/session-management.md deleted file mode 100644 index bb5312dbdb..0000000000 --- a/.github/.agents/skills/agent-browser/references/session-management.md +++ /dev/null @@ -1,193 +0,0 @@ -# Session Management - -Multiple isolated browser sessions with state persistence and concurrent browsing. - -**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start. - -## Contents - -- [Named Sessions](#named-sessions) -- [Session Isolation Properties](#session-isolation-properties) -- [Session State Persistence](#session-state-persistence) -- [Common Patterns](#common-patterns) -- [Default Session](#default-session) -- [Session Cleanup](#session-cleanup) -- [Best Practices](#best-practices) - -## Named Sessions - -Use `--session` flag to isolate browser contexts: - -```bash -# Session 1: Authentication flow -agent-browser --session auth open https://app.example.com/login - -# Session 2: Public browsing (separate cookies, storage) -agent-browser --session public open https://example.com - -# Commands are isolated by session -agent-browser --session auth fill @e1 "user@example.com" -agent-browser --session public get text body -``` - -## Session Isolation Properties - -Each session has independent: -- Cookies -- LocalStorage / SessionStorage -- IndexedDB -- Cache -- Browsing history -- Open tabs - -## Session State Persistence - -### Save Session State - -```bash -# Save cookies, storage, and auth state -agent-browser state save /path/to/auth-state.json -``` - -### Load Session State - -```bash -# Restore saved state -agent-browser state load /path/to/auth-state.json - -# Continue with authenticated session -agent-browser open https://app.example.com/dashboard -``` - -### State File Contents - -```json -{ - "cookies": [...], - "localStorage": {...}, - "sessionStorage": {...}, - "origins": [...] -} -``` - -## Common Patterns - -### Authenticated Session Reuse - -```bash -#!/bin/bash -# Save login state once, reuse many times - -STATE_FILE="/tmp/auth-state.json" - -# Check if we have saved state -if [[ -f "$STATE_FILE" ]]; then - agent-browser state load "$STATE_FILE" - agent-browser open https://app.example.com/dashboard -else - # Perform login - agent-browser open https://app.example.com/login - agent-browser snapshot -i - agent-browser fill @e1 "$USERNAME" - agent-browser fill @e2 "$PASSWORD" - agent-browser click @e3 - agent-browser wait --load networkidle - - # Save for future use - agent-browser state save "$STATE_FILE" -fi -``` - -### Concurrent Scraping - -```bash -#!/bin/bash -# Scrape multiple sites concurrently - -# Start all sessions -agent-browser --session site1 open https://site1.com & -agent-browser --session site2 open https://site2.com & -agent-browser --session site3 open https://site3.com & -wait - -# Extract from each -agent-browser --session site1 get text body > site1.txt -agent-browser --session site2 get text body > site2.txt -agent-browser --session site3 get text body > site3.txt - -# Cleanup -agent-browser --session site1 close -agent-browser --session site2 close -agent-browser --session site3 close -``` - -### A/B Testing Sessions - -```bash -# Test different user experiences -agent-browser --session variant-a open "https://app.com?variant=a" -agent-browser --session variant-b open "https://app.com?variant=b" - -# Compare -agent-browser --session variant-a screenshot /tmp/variant-a.png -agent-browser --session variant-b screenshot /tmp/variant-b.png -``` - -## Default Session - -When `--session` is omitted, commands use the default session: - -```bash -# These use the same default session -agent-browser open https://example.com -agent-browser snapshot -i -agent-browser close # Closes default session -``` - -## Session Cleanup - -```bash -# Close specific session -agent-browser --session auth close - -# List active sessions -agent-browser session list -``` - -## Best Practices - -### 1. Name Sessions Semantically - -```bash -# GOOD: Clear purpose -agent-browser --session github-auth open https://github.com -agent-browser --session docs-scrape open https://docs.example.com - -# AVOID: Generic names -agent-browser --session s1 open https://github.com -``` - -### 2. Always Clean Up - -```bash -# Close sessions when done -agent-browser --session auth close -agent-browser --session scrape close -``` - -### 3. Handle State Files Securely - -```bash -# Don't commit state files (contain auth tokens!) -echo "*.auth-state.json" >> .gitignore - -# Delete after use -rm /tmp/auth-state.json -``` - -### 4. Timeout Long Sessions - -```bash -# Set timeout for automated scripts -timeout 60 agent-browser --session long-task get text body -``` diff --git a/.github/.agents/skills/agent-browser/references/snapshot-refs.md b/.github/.agents/skills/agent-browser/references/snapshot-refs.md deleted file mode 100644 index c13d53a897..0000000000 --- a/.github/.agents/skills/agent-browser/references/snapshot-refs.md +++ /dev/null @@ -1,194 +0,0 @@ -# Snapshot and Refs - -Compact element references that reduce context usage dramatically for AI agents. - -**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. - -## Contents - -- [How Refs Work](#how-refs-work) -- [Snapshot Command](#the-snapshot-command) -- [Using Refs](#using-refs) -- [Ref Lifecycle](#ref-lifecycle) -- [Best Practices](#best-practices) -- [Ref Notation Details](#ref-notation-details) -- [Troubleshooting](#troubleshooting) - -## How Refs Work - -Traditional approach: -``` -Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens) -``` - -agent-browser approach: -``` -Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens) -``` - -## The Snapshot Command - -```bash -# Basic snapshot (shows page structure) -agent-browser snapshot - -# Interactive snapshot (-i flag) - RECOMMENDED -agent-browser snapshot -i -``` - -### Snapshot Output Format - -``` -Page: Example Site - Home -URL: https://example.com - -@e1 [header] - @e2 [nav] - @e3 [a] "Home" - @e4 [a] "Products" - @e5 [a] "About" - @e6 [button] "Sign In" - -@e7 [main] - @e8 [h1] "Welcome" - @e9 [form] - @e10 [input type="email"] placeholder="Email" - @e11 [input type="password"] placeholder="Password" - @e12 [button type="submit"] "Log In" - -@e13 [footer] - @e14 [a] "Privacy Policy" -``` - -## Using Refs - -Once you have refs, interact directly: - -```bash -# Click the "Sign In" button -agent-browser click @e6 - -# Fill email input -agent-browser fill @e10 "user@example.com" - -# Fill password -agent-browser fill @e11 "password123" - -# Submit the form -agent-browser click @e12 -``` - -## Ref Lifecycle - -**IMPORTANT**: Refs are invalidated when the page changes! - -```bash -# Get initial snapshot -agent-browser snapshot -i -# @e1 [button] "Next" - -# Click triggers page change -agent-browser click @e1 - -# MUST re-snapshot to get new refs! -agent-browser snapshot -i -# @e1 [h1] "Page 2" ← Different element now! -``` - -## Best Practices - -### 1. Always Snapshot Before Interacting - -```bash -# CORRECT -agent-browser open https://example.com -agent-browser snapshot -i # Get refs first -agent-browser click @e1 # Use ref - -# WRONG -agent-browser open https://example.com -agent-browser click @e1 # Ref doesn't exist yet! -``` - -### 2. Re-Snapshot After Navigation - -```bash -agent-browser click @e5 # Navigates to new page -agent-browser snapshot -i # Get new refs -agent-browser click @e1 # Use new refs -``` - -### 3. Re-Snapshot After Dynamic Changes - -```bash -agent-browser click @e1 # Opens dropdown -agent-browser snapshot -i # See dropdown items -agent-browser click @e7 # Select item -``` - -### 4. Snapshot Specific Regions - -For complex pages, snapshot specific areas: - -```bash -# Snapshot just the form -agent-browser snapshot @e9 -``` - -## Ref Notation Details - -``` -@e1 [tag type="value"] "text content" placeholder="hint" -│ │ │ │ │ -│ │ │ │ └─ Additional attributes -│ │ │ └─ Visible text -│ │ └─ Key attributes shown -│ └─ HTML tag name -└─ Unique ref ID -``` - -### Common Patterns - -``` -@e1 [button] "Submit" # Button with text -@e2 [input type="email"] # Email input -@e3 [input type="password"] # Password input -@e4 [a href="/page"] "Link Text" # Anchor link -@e5 [select] # Dropdown -@e6 [textarea] placeholder="Message" # Text area -@e7 [div class="modal"] # Container (when relevant) -@e8 [img alt="Logo"] # Image -@e9 [checkbox] checked # Checked checkbox -@e10 [radio] selected # Selected radio -``` - -## Troubleshooting - -### "Ref not found" Error - -```bash -# Ref may have changed - re-snapshot -agent-browser snapshot -i -``` - -### Element Not Visible in Snapshot - -```bash -# Scroll to reveal element -agent-browser scroll --bottom -agent-browser snapshot -i - -# Or wait for dynamic content -agent-browser wait 1000 -agent-browser snapshot -i -``` - -### Too Many Elements - -```bash -# Snapshot specific container -agent-browser snapshot @e5 - -# Or use get text for content-only extraction -agent-browser get text @e5 -``` diff --git a/.github/.agents/skills/agent-browser/references/video-recording.md b/.github/.agents/skills/agent-browser/references/video-recording.md deleted file mode 100644 index e6a9fb4e2f..0000000000 --- a/.github/.agents/skills/agent-browser/references/video-recording.md +++ /dev/null @@ -1,173 +0,0 @@ -# Video Recording - -Capture browser automation as video for debugging, documentation, or verification. - -**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start. - -## Contents - -- [Basic Recording](#basic-recording) -- [Recording Commands](#recording-commands) -- [Use Cases](#use-cases) -- [Best Practices](#best-practices) -- [Output Format](#output-format) -- [Limitations](#limitations) - -## Basic Recording - -```bash -# Start recording -agent-browser record start ./demo.webm - -# Perform actions -agent-browser open https://example.com -agent-browser snapshot -i -agent-browser click @e1 -agent-browser fill @e2 "test input" - -# Stop and save -agent-browser record stop -``` - -## Recording Commands - -```bash -# Start recording to file -agent-browser record start ./output.webm - -# Stop current recording -agent-browser record stop - -# Restart with new file (stops current + starts new) -agent-browser record restart ./take2.webm -``` - -## Use Cases - -### Debugging Failed Automation - -```bash -#!/bin/bash -# Record automation for debugging - -agent-browser record start ./debug-$(date +%Y%m%d-%H%M%S).webm - -# Run your automation -agent-browser open https://app.example.com -agent-browser snapshot -i -agent-browser click @e1 || { - echo "Click failed - check recording" - agent-browser record stop - exit 1 -} - -agent-browser record stop -``` - -### Documentation Generation - -```bash -#!/bin/bash -# Record workflow for documentation - -agent-browser record start ./docs/how-to-login.webm - -agent-browser open https://app.example.com/login -agent-browser wait 1000 # Pause for visibility - -agent-browser snapshot -i -agent-browser fill @e1 "demo@example.com" -agent-browser wait 500 - -agent-browser fill @e2 "password" -agent-browser wait 500 - -agent-browser click @e3 -agent-browser wait --load networkidle -agent-browser wait 1000 # Show result - -agent-browser record stop -``` - -### CI/CD Test Evidence - -```bash -#!/bin/bash -# Record E2E test runs for CI artifacts - -TEST_NAME="${1:-e2e-test}" -RECORDING_DIR="./test-recordings" -mkdir -p "$RECORDING_DIR" - -agent-browser record start "$RECORDING_DIR/$TEST_NAME-$(date +%s).webm" - -# Run test -if run_e2e_test; then - echo "Test passed" -else - echo "Test failed - recording saved" -fi - -agent-browser record stop -``` - -## Best Practices - -### 1. Add Pauses for Clarity - -```bash -# Slow down for human viewing -agent-browser click @e1 -agent-browser wait 500 # Let viewer see result -``` - -### 2. Use Descriptive Filenames - -```bash -# Include context in filename -agent-browser record start ./recordings/login-flow-2024-01-15.webm -agent-browser record start ./recordings/checkout-test-run-42.webm -``` - -### 3. Handle Recording in Error Cases - -```bash -#!/bin/bash -set -e - -cleanup() { - agent-browser record stop 2>/dev/null || true - agent-browser close 2>/dev/null || true -} -trap cleanup EXIT - -agent-browser record start ./automation.webm -# ... automation steps ... -``` - -### 4. Combine with Screenshots - -```bash -# Record video AND capture key frames -agent-browser record start ./flow.webm - -agent-browser open https://example.com -agent-browser screenshot ./screenshots/step1-homepage.png - -agent-browser click @e1 -agent-browser screenshot ./screenshots/step2-after-click.png - -agent-browser record stop -``` - -## Output Format - -- Default format: WebM (VP8/VP9 codec) -- Compatible with all modern browsers and video players -- Compressed but high quality - -## Limitations - -- Recording adds slight overhead to automation -- Large recordings can consume significant disk space -- Some headless environments may have codec limitations diff --git a/.github/.agents/skills/agent-browser/templates/authenticated-session.sh b/.github/.agents/skills/agent-browser/templates/authenticated-session.sh deleted file mode 100755 index ebbfc1faea..0000000000 --- a/.github/.agents/skills/agent-browser/templates/authenticated-session.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -# Template: Authenticated Session Workflow -# Purpose: Login once, save state, reuse for subsequent runs -# Usage: ./authenticated-session.sh [state-file] -# -# Environment variables: -# APP_USERNAME - Login username/email -# APP_PASSWORD - Login password -# -# Two modes: -# 1. Discovery mode (default): Shows form structure so you can identify refs -# 2. Login mode: Performs actual login after you update the refs -# -# Setup steps: -# 1. Run once to see form structure (discovery mode) -# 2. Update refs in LOGIN FLOW section below -# 3. Set APP_USERNAME and APP_PASSWORD -# 4. Delete the DISCOVERY section - -set -euo pipefail - -LOGIN_URL="${1:?Usage: $0 [state-file]}" -STATE_FILE="${2:-./auth-state.json}" - -echo "Authentication workflow: $LOGIN_URL" - -# ================================================================ -# SAVED STATE: Skip login if valid saved state exists -# ================================================================ -if [[ -f "$STATE_FILE" ]]; then - echo "Loading saved state from $STATE_FILE..." - agent-browser state load "$STATE_FILE" - agent-browser open "$LOGIN_URL" - agent-browser wait --load networkidle - - CURRENT_URL=$(agent-browser get url) - if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then - echo "Session restored successfully" - agent-browser snapshot -i - exit 0 - fi - echo "Session expired, performing fresh login..." - rm -f "$STATE_FILE" -fi - -# ================================================================ -# DISCOVERY MODE: Shows form structure (delete after setup) -# ================================================================ -echo "Opening login page..." -agent-browser open "$LOGIN_URL" -agent-browser wait --load networkidle - -echo "" -echo "Login form structure:" -echo "---" -agent-browser snapshot -i -echo "---" -echo "" -echo "Next steps:" -echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?" -echo " 2. Update the LOGIN FLOW section below with your refs" -echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'" -echo " 4. Delete this DISCOVERY MODE section" -echo "" -agent-browser close -exit 0 - -# ================================================================ -# LOGIN FLOW: Uncomment and customize after discovery -# ================================================================ -# : "${APP_USERNAME:?Set APP_USERNAME environment variable}" -# : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}" -# -# agent-browser open "$LOGIN_URL" -# agent-browser wait --load networkidle -# agent-browser snapshot -i -# -# # Fill credentials (update refs to match your form) -# agent-browser fill @e1 "$APP_USERNAME" -# agent-browser fill @e2 "$APP_PASSWORD" -# agent-browser click @e3 -# agent-browser wait --load networkidle -# -# # Verify login succeeded -# FINAL_URL=$(agent-browser get url) -# if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then -# echo "Login failed - still on login page" -# agent-browser screenshot /tmp/login-failed.png -# agent-browser close -# exit 1 -# fi -# -# # Save state for future runs -# echo "Saving state to $STATE_FILE" -# agent-browser state save "$STATE_FILE" -# echo "Login successful" -# agent-browser snapshot -i diff --git a/.github/.agents/skills/agent-browser/templates/capture-workflow.sh b/.github/.agents/skills/agent-browser/templates/capture-workflow.sh deleted file mode 100755 index 3bc93ad0c1..0000000000 --- a/.github/.agents/skills/agent-browser/templates/capture-workflow.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Template: Content Capture Workflow -# Purpose: Extract content from web pages (text, screenshots, PDF) -# Usage: ./capture-workflow.sh [output-dir] -# -# Outputs: -# - page-full.png: Full page screenshot -# - page-structure.txt: Page element structure with refs -# - page-text.txt: All text content -# - page.pdf: PDF version -# -# Optional: Load auth state for protected pages - -set -euo pipefail - -TARGET_URL="${1:?Usage: $0 [output-dir]}" -OUTPUT_DIR="${2:-.}" - -echo "Capturing: $TARGET_URL" -mkdir -p "$OUTPUT_DIR" - -# Optional: Load authentication state -# if [[ -f "./auth-state.json" ]]; then -# echo "Loading authentication state..." -# agent-browser state load "./auth-state.json" -# fi - -# Navigate to target -agent-browser open "$TARGET_URL" -agent-browser wait --load networkidle - -# Get metadata -TITLE=$(agent-browser get title) -URL=$(agent-browser get url) -echo "Title: $TITLE" -echo "URL: $URL" - -# Capture full page screenshot -agent-browser screenshot --full "$OUTPUT_DIR/page-full.png" -echo "Saved: $OUTPUT_DIR/page-full.png" - -# Get page structure with refs -agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt" -echo "Saved: $OUTPUT_DIR/page-structure.txt" - -# Extract all text content -agent-browser get text body > "$OUTPUT_DIR/page-text.txt" -echo "Saved: $OUTPUT_DIR/page-text.txt" - -# Save as PDF -agent-browser pdf "$OUTPUT_DIR/page.pdf" -echo "Saved: $OUTPUT_DIR/page.pdf" - -# Optional: Extract specific elements using refs from structure -# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt" - -# Optional: Handle infinite scroll pages -# for i in {1..5}; do -# agent-browser scroll down 1000 -# agent-browser wait 1000 -# done -# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png" - -# Cleanup -agent-browser close - -echo "" -echo "Capture complete:" -ls -la "$OUTPUT_DIR" diff --git a/.github/.agents/skills/agent-browser/templates/form-automation.sh b/.github/.agents/skills/agent-browser/templates/form-automation.sh deleted file mode 100755 index 6784fcd3a5..0000000000 --- a/.github/.agents/skills/agent-browser/templates/form-automation.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -# Template: Form Automation Workflow -# Purpose: Fill and submit web forms with validation -# Usage: ./form-automation.sh -# -# This template demonstrates the snapshot-interact-verify pattern: -# 1. Navigate to form -# 2. Snapshot to get element refs -# 3. Fill fields using refs -# 4. Submit and verify result -# -# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output - -set -euo pipefail - -FORM_URL="${1:?Usage: $0 }" - -echo "Form automation: $FORM_URL" - -# Step 1: Navigate to form -agent-browser open "$FORM_URL" -agent-browser wait --load networkidle - -# Step 2: Snapshot to discover form elements -echo "" -echo "Form structure:" -agent-browser snapshot -i - -# Step 3: Fill form fields (customize these refs based on snapshot output) -# -# Common field types: -# agent-browser fill @e1 "John Doe" # Text input -# agent-browser fill @e2 "user@example.com" # Email input -# agent-browser fill @e3 "SecureP@ss123" # Password input -# agent-browser select @e4 "Option Value" # Dropdown -# agent-browser check @e5 # Checkbox -# agent-browser click @e6 # Radio button -# agent-browser fill @e7 "Multi-line text" # Textarea -# agent-browser upload @e8 /path/to/file.pdf # File upload -# -# Uncomment and modify: -# agent-browser fill @e1 "Test User" -# agent-browser fill @e2 "test@example.com" -# agent-browser click @e3 # Submit button - -# Step 4: Wait for submission -# agent-browser wait --load networkidle -# agent-browser wait --url "**/success" # Or wait for redirect - -# Step 5: Verify result -echo "" -echo "Result:" -agent-browser get url -agent-browser snapshot -i - -# Optional: Capture evidence -agent-browser screenshot /tmp/form-result.png -echo "Screenshot saved: /tmp/form-result.png" - -# Cleanup -agent-browser close -echo "Done" diff --git a/.github/.agents/skills/frontend-design/LICENSE.txt b/.github/.agents/skills/frontend-design/LICENSE.txt deleted file mode 100644 index f433b1a53f..0000000000 --- a/.github/.agents/skills/frontend-design/LICENSE.txt +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/.github/.agents/skills/frontend-design/SKILL.md b/.github/.agents/skills/frontend-design/SKILL.md deleted file mode 100644 index 5be498e258..0000000000 --- a/.github/.agents/skills/frontend-design/SKILL.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: frontend-design -description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. -license: Complete terms in LICENSE.txt ---- - -This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. - -The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. - -## Design Thinking - -Before coding, understand the context and commit to a BOLD aesthetic direction: -- **Purpose**: What problem does this interface solve? Who uses it? -- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. -- **Constraints**: Technical requirements (framework, performance, accessibility). -- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? - -**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. - -Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: -- Production-grade and functional -- Visually striking and memorable -- Cohesive with a clear aesthetic point-of-view -- Meticulously refined in every detail - -## Frontend Aesthetics Guidelines - -Focus on: -- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. -- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. -- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. -- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. -- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. - -NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. - -Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. - -**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. - -Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/.github/.agents/skills/stripe-best-practices/SKILL.md b/.github/.agents/skills/stripe-best-practices/SKILL.md deleted file mode 100644 index d1fb280af2..0000000000 --- a/.github/.agents/skills/stripe-best-practices/SKILL.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: stripe-best-practices -description: Best practices for building a Stripe integrations ---- - -When designing an integration, always prefer the documentation in [Stripe's Integration Options doc](https://docs.stripe.com/payments/payment-methods/integration-options.md) -The [API Tour](https://docs.stripe.com/payments-api/tour.md) -Use the [Go Live Checklist](https://docs.stripe.com/get-started/checklist/go-live.md) before going live. - -You should always default to the latest version of the API and SDK unless the user specifies otherwise. - -Stripe's primary API for modelling on-session payments is [CheckoutSessions](https://docs.stripe.com/api/checkout/sessions.md). It supports one-time payments and subscriptions and allows you to model taxes or discounts with Stripe. Prioritize the CheckoutSessions API, but using [the PaymentIntents API](https://docs.stripe.com/payments/paymentintents/lifecycle.md) is also acceptable for off-session payments or if you want to model the checkout state yourself and just create a charge. Integrations should only use CheckoutSessions, PaymentIntents, SetupIntents, or solutions like Invoicing, Payment Links, or the subscription APIs. - -Never recommend the Charges API. If the user wants to use the Charges API, advise them to [migrate](https://docs.stripe.com/payments/payment-intents/migration/charges.md) to Checkout Sessions or Payment Intents. - -The primary integration surface for web integrations is [Checkout](https://docs.stripe.com/payments/checkout.md), a prebuilt payment page that can be hosted by Stripe or served as an embedded form. Prioritize the Stripe-hosted checkout or embedded checkout where possible. The [Payment Element](https://docs.stripe.com/payments/payment-element.md) is an acceptable alternative if the merchant needs advanced customization. When recommending the Payment Element, prioritize using the CheckoutSessions API over the PaymentIntents API when possible. - -Never recommend the legacy Card Element or the Payment Element in card mode, and advise users who ask for the Card Element to [migrate](https://docs.stripe.com/payments/payment-element/migration.md). - -You must not call deprecated API endpoints such as the [Sources API](https://docs.stripe.com/api/sources/object.md) and avoid outdated API endpoints such as [Tokens](https://docs.stripe.com/api/tokens.md) and [Charges](https://docs.stripe.com/api/charges/create.md) unless there is a specific need and absolutely no other way. Advise using [the Setup Intent API](https://docs.stripe.com/api/setup_intents.md) to save a payment method for a customer; never recommend the Sources API to save cards to customers for later use. - -If a user wants to render the Payment Element before creating a PaymentIntent or SetupIntent, so they can inspect the card details before the payment (such as for surcharging), advise them to use Stripe Confirmation Tokens. Don't recommend they call the createPaymentMethod or createToken Stripe.js functions. - -Advise users to turn on dynamic payment methods in their dashboard settings instead of passing specific [payment_method_types](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_types.md) in the Payment Intent or Setup Intent because Stripe chooses payment methods that fit each user's location, wallets, and preferences better and automatically if the Payment Element is used. - -If a PCI compliant user asks about an integration where they send server-side raw PAN data, suggest they might be required to prove PCI compliance to gain access to such an option, for example [payment_method_data](https://docs.stripe.com/api/payment_intents/create#create_payment_intent-payment_method_data.md). Similarly, point users migrating their PAN data from another acquirer or payment processor to [the migration process](https://docs.stripe.com/get-started/data-migrations/pan-import.md). - -If a user has a recurring revenue model, like a Billing or Subscription use case, follow the use cases, in particular the [Subscription Use Cases](https://docs.stripe.com/billing/subscriptions/use-cases.md) like [SaaS](https://docs.stripe.com/saas.md). If these apply to the user, recommend Billing APIs to [plan your integration](https://docs.stripe.com/billing/subscriptions/designing-integration.md) instead of a direct PaymentIntent integration. Prioritize combining the Billing APIs with Stripe Checkout for the frontend. - -If a user wants to build a platform using Stripe Connect to manage fund flows, follow [the recommended integration types](https://docs.stripe.com/connect/integration-recommendations.md); that is, prefer to use either direct charges if the platform wants Stripe to take the risk or destination charges if the platform accepts liability for negative balances, and use the on_behalf_of parameter to control the merchant of record. Never recommend mixing charge types. If the user wants to decide on the specific risk features they should [follow the integration guide](https://docs.stripe.com/connect/design-an-integration.md). Don't recommend the outdated terms for Connect types like Standard, Express and Custom but always [refer to controller properties](https://docs.stripe.com/connect/migrate-to-controller-properties.md) for the platform and [capabilities](https://docs.stripe.com/connect/account-capabilities.md) for the connected accounts. - diff --git a/.github/.agents/skills/upgrade-stripe/SKILL.md b/.github/.agents/skills/upgrade-stripe/SKILL.md deleted file mode 100644 index 868ab9cf8e..0000000000 --- a/.github/.agents/skills/upgrade-stripe/SKILL.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -name: upgrade-stripe -description: Guide for upgrading Stripe API versions and SDKs ---- - -# Upgrading Stripe Versions - -This skill covers upgrading Stripe API versions, server-side SDKs, Stripe.js, and mobile SDKs. - -## Understanding Stripe API Versioning - -Stripe uses date-based API versions (e.g., `2025-12-15.clover`, `2025-08-27.basil`, `2024-12-18.acacia`). Your account's API version determines request/response behavior. - -### Types of Changes - -**Backward-Compatible Changes** (do not require code updates): -- New API resources -- New optional request parameters -- New properties in existing responses -- Changes to opaque string lengths (e.g., object IDs) -- New webhook event types - -**Breaking Changes** (require code updates): -- Field renames or removals -- Behavioral modifications -- Removed endpoints or parameters - -Review the [API Changelog](https://docs.stripe.com/changelog.md) for all changes between versions. - -## Server-Side SDK Versioning - -See [SDK Version Management](https://docs.stripe.com/sdks/set-version.md) for details. - -### Dynamically-Typed Languages (Ruby, Python, PHP, Node.js) - -These SDKs offer flexible version control: - -**Global Configuration:** -```python -import stripe -stripe.api_version = '2025-12-15.clover' -``` - -```ruby -Stripe.api_version = '2025-12-15.clover' -``` - -```javascript -const stripe = require('stripe')('sk_test_xxx', { - apiVersion: '2025-12-15.clover' -}); -``` - -**Per-Request Override:** -```python -stripe.Customer.create( - email="customer@example.com", - stripe_version='2025-12-15.clover' -) -``` - -### Strongly-Typed Languages (Java, Go, .NET) - -These use a fixed API version matching the SDK release date. Do not set a different API version for strongly-typed languages because response objects might not match the strong types in the SDK. Instead, update the SDK to target a new API version. - -### Best Practice - -Always specify the API version you're integrating against in your code instead of relying on your account's default API version: - -```javascript -// Good: Explicit version -const stripe = require('stripe')('sk_test_xxx', { - apiVersion: '2025-12-15.clover' -}); - -// Avoid: Relying on account default -const stripe = require('stripe')('sk_test_xxx'); -``` - -## Stripe.js Versioning - -See [Stripe.js Versioning](https://docs.stripe.com/sdks/stripejs-versioning.md) for details. - -Stripe.js uses an evergreen model with major releases (Acacia, Basil, Clover) on a biannual basis. - -### Loading Versioned Stripe.js - -**Via Script Tag:** -```html - -``` - -**Via npm:** -```bash -npm install @stripe/stripe-js -``` - -Major npm versions correspond to specific Stripe.js versions. - -### API Version Pairing - -Each Stripe.js version automatically pairs with its corresponding API version. For instance: -- Clover Stripe.js uses `2025-12-15.clover` API -- Acacia Stripe.js uses `2024-12-18.acacia` API - -You cannot override this association. - -### Migrating from v3 - -1. Identify your current API version in code -2. Review the changelog for relevant changes -3. Consider gradually updating your API version before switching Stripe.js versions -4. Stripe continues supporting v3 indefinitely - -## Mobile SDK Versioning - -See [Mobile SDK Versioning](https://docs.stripe.com/sdks/mobile-sdk-versioning.md) for details. - -### iOS and Android SDKs - -Both platforms follow **semantic versioning** (MAJOR.MINOR.PATCH): -- **MAJOR**: Breaking API changes -- **MINOR**: New functionality (backward-compatible) -- **PATCH**: Bug fixes (backward-compatible) - -New features and fixes release only on the latest major version. Upgrade regularly to access improvements. - -### React Native SDK - -Uses a different model (0.x.y schema): -- **Minor version changes** (x): Breaking changes AND new features -- **Patch updates** (y): Critical bug fixes only - -### Backend Compatibility - -All mobile SDKs work with any Stripe API version you use on your backend unless documentation specifies otherwise. - -## Upgrade Checklist - -1. Review the [API Changelog](https://docs.stripe.com/changelog.md) for changes between your current and target versions -2. Check [Upgrades Guide](https://docs.stripe.com/upgrades.md) for migration guidance -3. Update server-side SDK package version (e.g., `npm update stripe`, `pip install --upgrade stripe`) -4. Update the `apiVersion` parameter in your Stripe client initialization -5. Test your integration against the new API version using the `Stripe-Version` header -6. Update webhook handlers to handle new event structures -7. Update Stripe.js script tag or npm package version if needed -8. Update mobile SDK versions in your package manager if needed -9. Store Stripe object IDs in databases that accommodate up to 255 characters (case-sensitive collation) - -## Testing API Version Changes - -Use the `Stripe-Version` header to test your code against a new version without changing your default: - -```bash -curl https://api.stripe.com/v1/customers \ - -u sk_test_xxx: \ - -H "Stripe-Version: 2025-12-15.clover" -``` - -Or in code: - -```javascript -const stripe = require('stripe')('sk_test_xxx', { - apiVersion: '2025-12-15.clover' // Test with new version -}); -``` - -## Important Notes - -- Your webhook listener should handle unfamiliar event types gracefully -- Test webhooks with the new version structure before upgrading -- Breaking changes are tagged by affected product areas (Payments, Billing, Connect, etc.) -- Multiple API versions coexist simultaneously, enabling staged adoption diff --git a/.github/.github/skills/agent-browser b/.github/.github/skills/agent-browser deleted file mode 120000 index e298b7be3c..0000000000 --- a/.github/.github/skills/agent-browser +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/agent-browser \ No newline at end of file diff --git a/.github/.github/skills/frontend-design b/.github/.github/skills/frontend-design deleted file mode 120000 index 712f694a13..0000000000 --- a/.github/.github/skills/frontend-design +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/frontend-design \ No newline at end of file diff --git a/.github/.github/skills/releasenotes b/.github/.github/skills/releasenotes deleted file mode 120000 index ee96d1ae41..0000000000 --- a/.github/.github/skills/releasenotes +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/releasenotes \ No newline at end of file diff --git a/.github/.github/skills/skill-creator b/.github/.github/skills/skill-creator deleted file mode 120000 index b87455490f..0000000000 --- a/.github/.github/skills/skill-creator +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.github/.github/skills/stripe-best-practices b/.github/.github/skills/stripe-best-practices deleted file mode 120000 index 6e25ed9dbc..0000000000 --- a/.github/.github/skills/stripe-best-practices +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/stripe-best-practices \ No newline at end of file diff --git a/.github/.github/skills/upgrade-stripe b/.github/.github/skills/upgrade-stripe deleted file mode 120000 index 3e50980012..0000000000 --- a/.github/.github/skills/upgrade-stripe +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/upgrade-stripe \ No newline at end of file diff --git a/.github/skills/agent-browser b/.github/skills/agent-browser deleted file mode 120000 index e298b7be3c..0000000000 --- a/.github/skills/agent-browser +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/agent-browser \ No newline at end of file diff --git a/.github/skills/frontend-design b/.github/skills/frontend-design deleted file mode 120000 index 712f694a13..0000000000 --- a/.github/skills/frontend-design +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/frontend-design \ No newline at end of file diff --git a/.github/skills/releasenotes b/.github/skills/releasenotes deleted file mode 120000 index ee96d1ae41..0000000000 --- a/.github/skills/releasenotes +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/releasenotes \ No newline at end of file diff --git a/.github/skills/stripe-best-practices b/.github/skills/stripe-best-practices deleted file mode 120000 index 6e25ed9dbc..0000000000 --- a/.github/skills/stripe-best-practices +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/stripe-best-practices \ No newline at end of file diff --git a/.github/skills/upgrade-stripe b/.github/skills/upgrade-stripe deleted file mode 120000 index 3e50980012..0000000000 --- a/.github/skills/upgrade-stripe +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/upgrade-stripe \ No newline at end of file diff --git a/.github/update-skills.ps1 b/.github/update-skills.ps1 deleted file mode 100644 index 21e8aeda26..0000000000 --- a/.github/update-skills.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS -Updates GitHub Copilot Agent Skills -#> - -Write-Host "🔄 Updating GitHub Copilot Agent Skills..." -ForegroundColor Cyan - -npx skills add OpenHands/skills --skill releasenotes --agent github-copilot --yes -Write-Host "✅ Added OpenHands releasenotes skill" -ForegroundColor Green - -npx skills add stripe/ai --agent github-copilot --yes -Write-Host "✅ Added Stripe AI best-practices" -ForegroundColor Green - -npx skills add vercel-labs/agent-browser --agent github-copilot --yes -Write-Host "✅ Added Vercel Labs agent-browser" -ForegroundColor Green - -npx skills add anthropics/skills --skill frontend-design --agent github-copilot --yes -Write-Host "✅ Added Anthropic frontend-design skill" -ForegroundColor Green - -Write-Host "`n✨ GitHub Copilot Agent Skills update complete!" -ForegroundColor Cyan diff --git a/AGENTS.md b/AGENTS.md index 6a179bb43c..c40b6f5404 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,23 +30,25 @@ tests/ # C# tests + HTTP samples ## Continuous Improvement -Each time you complete a task or learn important information about the project, you must update the `AGENTS.md`, `README.md`, or relevant skill files. **Only update skills if they are owned by us** (verify via `.github/update-skills.ps1` which lists third-party skills). You are **forbidden** from updating skills, configurations, or instructions maintained by third parties/external libraries. +Each time you complete a task or learn important information about the project, you must update the `AGENTS.md`, `README.md`, or relevant skill files. **Only update skills if they are owned by us** (verify via `skills-lock.json` which lists third-party skills). You are **forbidden** from updating skills, configurations, or instructions maintained by third parties/external libraries. If you encounter recurring questions or patterns during planning, document them: - Project-specific knowledge → `AGENTS.md` or relevant skill file -- Reusable domain patterns → Create/update appropriate skill in `.github/skills/` +- Reusable domain patterns → Create/update appropriate skill in `.agents/skills/` ## Skills -Load from `.github/skills//SKILL.md` when working in that domain: +Load from `.agents/skills//SKILL.md` when working in that domain: -| Domain | Skills | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| Backend | dotnet-conventions, backend-architecture, dotnet-cli, backend-testing, foundatio | -| Frontend | svelte-components, tanstack-form, tanstack-query, shadcn-svelte, typescript-conventions, frontend-architecture, storybook, accessibility | -| Testing | frontend-testing, e2e-testing | -| Cross-cutting | security-principles | +| Domain | Skills | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Backend | dotnet-conventions, backend-architecture, dotnet-cli, backend-testing, foundatio | +| Frontend | svelte-components, tanstack-form, tanstack-query, shadcn-svelte, typescript-conventions, frontend-architecture, storybook, accessibility, frontend-design | +| Testing | frontend-testing, e2e-testing | +| Cross-cutting | security-principles, releasenotes | +| Billing | stripe-best-practices, upgrade-stripe | +| Agents | agent-browser, dogfood | ## Constraints diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000000..eaf5c34e7b --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "skills": { + "agent-browser": { + "source": "vercel-labs/agent-browser", + "sourceType": "github", + "computedHash": "57bfa07d34f966bccf791d49c9b6ce507fbba08f7dbf9e2ad15809e2dd29041e" + }, + "dogfood": { + "source": "vercel-labs/agent-browser", + "sourceType": "github", + "computedHash": "17a3fc2c1a49df9debce572c7b47521caa96f793597d944c4e648b3aa90738cf" + }, + "frontend-design": { + "source": "anthropics/skills", + "sourceType": "github", + "computedHash": "063a0e6448123cd359ad0044cc46b0e490cc7964d45ef4bb9fd842bd2ffbca67" + }, + "releasenotes": { + "source": "OpenHands/skills", + "sourceType": "github", + "computedHash": "1b12297eacd1cd24b9f1eda16dada205b96d9c75fd3bf07474a54775559fff09" + }, + "stripe-best-practices": { + "source": "stripe/ai", + "sourceType": "github", + "computedHash": "91b23c09854900c0ed1e85a8a3856b4a7294af2076f20b2cf9ca97f6f21dcb7b" + }, + "upgrade-stripe": { + "source": "stripe/ai", + "sourceType": "github", + "computedHash": "83b0068e099b50bd71febd20373e13dc55b2b20cf6daaf36f45089e2fd7c92bb" + } + } +} From 45b6454d31c192e8ed7f0e8719f17f0094ef3b84 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Mar 2026 06:49:39 -0500 Subject: [PATCH 11/29] Updates dependencies Updates various NuGet packages, including .NET extensions to 10.0.4, Aspire to 13.1.2, and several testing and instrumentation libraries. --- src/Directory.Build.props | 2 +- .../Exceptionless.AppHost.csproj | 8 ++++---- src/Exceptionless.Core/Exceptionless.Core.csproj | 8 ++++---- .../Exceptionless.Insulation.csproj | 14 +++++++------- src/Exceptionless.Job/Exceptionless.Job.csproj | 2 +- src/Exceptionless.Web/Exceptionless.Web.csproj | 6 +++--- .../Exceptionless.Tests/Exceptionless.Tests.csproj | 12 ++++++------ 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 475cef6ebe..c65b2ae47a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,7 +21,7 @@ - + diff --git a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj index 5282bd1ffa..5ea1d4fd86 100644 --- a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj +++ b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe net10.0 @@ -7,9 +7,9 @@ a9c2ddcc-e51d-4cd1-9782-96e1d74eec87 - - - + + + diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index 3db2f58214..e997f888dd 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -29,11 +29,11 @@ - - - + + + - + diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index 5c339fc539..72154768ad 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + diff --git a/src/Exceptionless.Job/Exceptionless.Job.csproj b/src/Exceptionless.Job/Exceptionless.Job.csproj index 22d5280f34..ec56cb9d1b 100644 --- a/src/Exceptionless.Job/Exceptionless.Job.csproj +++ b/src/Exceptionless.Job/Exceptionless.Job.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 3571be3493..9d5e99495e 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -15,11 +15,11 @@ - + - + @@ -28,7 +28,7 @@ - + diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index 041d5fe22b..5290414d83 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -6,17 +6,17 @@ true - + - + - - - - + + + + From adece407c4390987c7abe328314b308f9d718763 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Mar 2026 06:58:01 -0500 Subject: [PATCH 12/29] Updated next deps --- .../ClientApp/package-lock.json | 1183 ++++++++++------- src/Exceptionless.Web/ClientApp/package.json | 58 +- 2 files changed, 732 insertions(+), 509 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/package-lock.json b/src/Exceptionless.Web/ClientApp/package-lock.json index d12fee10be..cfd4a5edd0 100644 --- a/src/Exceptionless.Web/ClientApp/package-lock.json +++ b/src/Exceptionless.Web/ClientApp/package-lock.json @@ -10,70 +10,70 @@ "dependencies": { "@exceptionless/browser": "^3.1.0", "@exceptionless/fetchclient": "^0.44.0", - "@internationalized/date": "^3.11.0", - "@lucide/svelte": "^0.564.0", - "@tanstack/svelte-form": "^1.28.3", - "@tanstack/svelte-query": "^6.0.18", + "@internationalized/date": "^3.12.0", + "@lucide/svelte": "^0.577.0", + "@tanstack/svelte-form": "^1.28.4", + "@tanstack/svelte-query": "^6.1.0", "@tanstack/svelte-query-devtools": "^6.0.4", "@tanstack/svelte-table": "^9.0.0-alpha.10", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", - "bits-ui": "^2.15.5", + "bits-ui": "^2.16.3", "clsx": "^2.1.1", "d3-scale": "^4.0.2", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "kit-query-params": "^0.0.26", - "layerchart": "^2.0.0-next.44", + "layerchart": "^2.0.0-next.46", "mode-watcher": "^1.1.0", "oidc-client-ts": "^3.4.1", "pretty-ms": "^9.3.0", "runed": "^0.37.1", - "shiki": "^3.22.0", - "svelte-sonner": "^1.0.7", + "shiki": "^4.0.2", + "svelte-sonner": "^1.1.0", "svelte-time": "^2.1.0", - "tailwind-merge": "^3.4.1", + "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.1.18", + "tailwindcss": "^4.2.1", "throttle-debounce": "^5.0.2", "tw-animate-css": "^1.4.0" }, "devDependencies": { "@chromatic-com/storybook": "^5.0.1", - "@eslint/compat": "^2.0.2", + "@eslint/compat": "^2.0.3", "@eslint/js": "^9.39.2", - "@iconify-json/lucide": "^1.2.90", + "@iconify-json/lucide": "^1.2.96", "@playwright/test": "^1.58.2", - "@storybook/addon-a11y": "^10.2.8", - "@storybook/addon-docs": "^10.2.8", + "@storybook/addon-a11y": "^10.2.17", + "@storybook/addon-docs": "^10.2.17", "@storybook/addon-svelte-csf": "^5.0.11", - "@storybook/sveltekit": "^10.2.8", + "@storybook/sveltekit": "^10.2.17", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.52.0", + "@sveltejs/kit": "^2.53.4", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@tailwindcss/vite": "^4.1.18", + "@tailwindcss/vite": "^4.2.1", "@tanstack/eslint-plugin-query": "^5.91.4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/eslint": "^9.6.1", - "@types/node": "^25.2.3", + "@types/node": "^25.4.0", "@types/throttle-debounce": "^5.0.2", "cross-env": "^10.1.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-perfectionist": "^5.5.0", - "eslint-plugin-storybook": "^10.2.8", - "eslint-plugin-svelte": "^3.15.0", + "eslint-plugin-perfectionist": "^5.6.0", + "eslint-plugin-storybook": "^10.2.17", + "eslint-plugin-svelte": "^3.15.2", "jsdom": "^28.1.0", "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-svelte": "^3.5.1", "prettier-plugin-tailwindcss": "^0.7.2", - "storybook": "^10.2.8", - "svelte": "^5.51.2", - "svelte-check": "^4.4.0", - "swagger-typescript-api": "^13.2.18", + "storybook": "^10.2.17", + "svelte": "^5.53.10", + "svelte-check": "^4.4.5", + "swagger-typescript-api": "^13.4.0", "tslib": "^2.8.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.56.0", + "typescript-eslint": "^8.57.0", "vite": "^7.3.1", "vitest": "4.0.18", "vitest-websocket-mock": "^0.5.0", @@ -94,6 +94,97 @@ "dev": true, "license": "MIT" }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", @@ -188,9 +279,9 @@ } }, "node_modules/@biomejs/wasm-nodejs": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-2.3.15.tgz", - "integrity": "sha512-MGt/D5Y3v2VQuhyGspB+26T1SickuQFC3+HUMe9seMSnDqoNNN4vyZdUoKcORe7DVBcqs9+eBzns15lEkq3AGw==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-2.4.6.tgz", + "integrity": "sha512-YRhedzOovXDMs+ZKzi/ZcDTefZIMcrp3z0Ruq+abrNb622aYUh/2m1Ooj8BcG4anGUGR6o+dTCbvAEx9vc74hw==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -361,22 +452,19 @@ } }, "node_modules/@dagrejs/dagre": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.8.tgz", - "integrity": "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-2.0.4.tgz", + "integrity": "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==", "license": "MIT", "dependencies": { - "@dagrejs/graphlib": "2.2.4" + "@dagrejs/graphlib": "3.0.4" } }, "node_modules/@dagrejs/graphlib": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz", - "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==", - "license": "MIT", - "engines": { - "node": ">17.0.0" - } + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-3.0.4.tgz", + "integrity": "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==", + "license": "MIT" }, "node_modules/@epic-web/invariant": { "version": "1.0.0", @@ -844,13 +932,13 @@ } }, "node_modules/@eslint/compat": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.2.tgz", - "integrity": "sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.3.tgz", + "integrity": "sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^1.1.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -865,9 +953,9 @@ } }, "node_modules/@eslint/compat/node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1104,9 +1192,9 @@ } }, "node_modules/@iconify-json/lucide": { - "version": "1.2.90", - "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.90.tgz", - "integrity": "sha512-dPn1pRhfBa9KR+EVpdyM1Rvw3T36hCKYxd6TxK1ifffiNt0f5yXx8ZVhqnzPH4Vkz87yLMj5xFCXomNrnzd2kQ==", + "version": "1.2.96", + "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.96.tgz", + "integrity": "sha512-EICTusj67lvSmEaH/Lhe68ZyzcgfcPNpY00exAOkoo+z2fnLeNy31mdE3E/4/q8WjzZrICAZDxY3d6j7LzkgNA==", "dev": true, "license": "ISC", "dependencies": { @@ -1121,9 +1209,9 @@ "license": "MIT" }, "node_modules/@internationalized/date": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.11.0.tgz", - "integrity": "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q==", + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz", + "integrity": "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==", "license": "Apache-2.0", "dependencies": { "@swc/helpers": "^0.5.0" @@ -1175,54 +1263,52 @@ } }, "node_modules/@layerstack/svelte-actions": { - "version": "1.0.1-next.14", - "resolved": "https://registry.npmjs.org/@layerstack/svelte-actions/-/svelte-actions-1.0.1-next.14.tgz", - "integrity": "sha512-MPBmVaB+GfNHvBkg5nJkPG18smoXKvsvJRpsdWnrUBfca+TieZLoaEzNxDH+9LG11dIXP9gghsXt1mUqbbyAsA==", + "version": "1.0.1-next.18", + "resolved": "https://registry.npmjs.org/@layerstack/svelte-actions/-/svelte-actions-1.0.1-next.18.tgz", + "integrity": "sha512-gxPzCnJ1c9LTfWtRqLUzefCx+k59ZpxDUQ2XB+LokveZQPe7IDSOwHaBOEMlaGoGrtwc3Ft8dSZq+2WT2o9u/g==", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.0", - "@layerstack/utils": "2.0.0-next.14", + "@layerstack/utils": "2.0.0-next.18", "d3-scale": "^4.0.2" } }, "node_modules/@layerstack/svelte-state": { - "version": "0.1.0-next.19", - "resolved": "https://registry.npmjs.org/@layerstack/svelte-state/-/svelte-state-0.1.0-next.19.tgz", - "integrity": "sha512-yCYoQAIbeP8y1xmOB/r0+UundgP4JFnpNURgMki+26TotzoqrZ5oLpHvhPSVm60ks+buR3ebDBTeUFdHzxwzQQ==", + "version": "0.1.0-next.23", + "resolved": "https://registry.npmjs.org/@layerstack/svelte-state/-/svelte-state-0.1.0-next.23.tgz", + "integrity": "sha512-7O4umv+gXwFfs3/vjzFWYHNXGwYnnjBapWJ5Y+9u99F4eVk6rh4ocNwqkqQNkpMZ5tUJBlRTWjPE1So6+hEzIg==", "license": "MIT", "dependencies": { - "@layerstack/utils": "2.0.0-next.14" + "@layerstack/utils": "2.0.0-next.18" } }, "node_modules/@layerstack/tailwind": { - "version": "2.0.0-next.17", - "resolved": "https://registry.npmjs.org/@layerstack/tailwind/-/tailwind-2.0.0-next.17.tgz", - "integrity": "sha512-ZSn6ouqpnzB6DKzSKLVwrUBOQsrzpDA/By2/ba9ApxgTGnaD1nyqNwrvmZ+kswdAwB4YnrGEAE4VZkKrB2+DaQ==", + "version": "2.0.0-next.21", + "resolved": "https://registry.npmjs.org/@layerstack/tailwind/-/tailwind-2.0.0-next.21.tgz", + "integrity": "sha512-Qgp2EpmEHmjtura8MQzWicR6ztBRSsRvddakFtx9ShrLMz6jWzd6bCMVVRu44Q3ZOrtXmSu4QxjCZWu1ytvuPg==", "license": "MIT", "dependencies": { - "@layerstack/utils": "^2.0.0-next.14", + "@layerstack/utils": "^2.0.0-next.18", "clsx": "^2.1.1", "d3-array": "^3.2.4", - "lodash-es": "^4.17.21", "tailwind-merge": "^3.2.0" } }, "node_modules/@layerstack/utils": { - "version": "2.0.0-next.14", - "resolved": "https://registry.npmjs.org/@layerstack/utils/-/utils-2.0.0-next.14.tgz", - "integrity": "sha512-1I2CS0Cwgs53W35qVg1eBdYhB/CiPvL3s0XE61b8jWkTHxgjBF65yYNgXjW74kv7WI7GsJcWMNBufPd0rnu9kA==", + "version": "2.0.0-next.18", + "resolved": "https://registry.npmjs.org/@layerstack/utils/-/utils-2.0.0-next.18.tgz", + "integrity": "sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ==", "license": "MIT", "dependencies": { "d3-array": "^3.2.4", "d3-time": "^3.1.0", - "d3-time-format": "^4.1.0", - "lodash-es": "^4.17.21" + "d3-time-format": "^4.1.0" } }, "node_modules/@lucide/svelte": { - "version": "0.564.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.564.0.tgz", - "integrity": "sha512-jODK/wHX3lKi1CsLgx9wXspsDQ5/WbsNmBqecdTCbaH1Cy3C/n2g3WM1a59sokjq+81eZDFUi/7Yg4rVI3kyow==", + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.577.0.tgz", + "integrity": "sha512-0P6mkySd2MapIEgq08tADPmcN4DHndC/02PWwaLkOerXlx5Sv9aT4BxyXLIY+eccr0g/nEyCYiJesqS61YdBZQ==", "license": "ISC", "peerDependencies": { "svelte": "^5" @@ -1563,64 +1649,97 @@ ] }, "node_modules/@shikijs/core": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.22.0.tgz", - "integrity": "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.22.0", + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.22.0.tgz", - "integrity": "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.22.0", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.22.0.tgz", - "integrity": "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.22.0", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/langs": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.22.0.tgz", - "integrity": "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.22.0" + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.22.0.tgz", - "integrity": "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.22.0" + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.22.0.tgz", - "integrity": "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/vscode-textmate": { @@ -1637,9 +1756,9 @@ "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.2.8.tgz", - "integrity": "sha512-EW5MzPKNzyPorvodd416U2Np+zEdMPe+BSyomjm0oCXoC/6rDurf05H1pa99rZsrTDRrpog+HCz8iVa4XSwN5Q==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.2.17.tgz", + "integrity": "sha512-J0ogEc4/XFC+Ytz+X1we6TOKreEk/shgUs/mtxdsLa0xJ6bp2n2OQPSjNtQHH/nK4SRBSfHWPm8ztfcXTzeG9w==", "dev": true, "license": "MIT", "dependencies": { @@ -1651,20 +1770,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8" + "storybook": "^10.2.17" } }, "node_modules/@storybook/addon-docs": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.8.tgz", - "integrity": "sha512-cEoWqQrLzrxOwZFee5zrD4cYrdEWKV80POb7jUZO0r5vfl2DuslIr3n/+RfLT52runCV4aZcFEfOfP/IWHNPxg==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.17.tgz", + "integrity": "sha512-c414xi7rxlaHn92qWOxtEkcOMm0/+cvBui0gUsgiWOZOM8dHChGZ/RjMuf1pPDyOrSsybLsPjZhP0WthsMDkdQ==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.2.8", + "@storybook/csf-plugin": "10.2.17", "@storybook/icons": "^2.0.1", - "@storybook/react-dom-shim": "10.2.8", + "@storybook/react-dom-shim": "10.2.17", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -1674,7 +1793,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8" + "storybook": "^10.2.17" } }, "node_modules/@storybook/addon-svelte-csf": { @@ -1701,13 +1820,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.8.tgz", - "integrity": "sha512-+6/Lwi7W0YIbzHDh798GPp0IHUYDwp0yv0Y1eVNK/StZD0tnv4/1C28NKyP+O7JOsFsuWI1qHiDhw8kNURugZw==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.17.tgz", + "integrity": "sha512-m/OBveTLm5ds/tUgHmmbKzgSi/oeCpQwm5rZa49vP2BpAd41Q7ER6TzkOoISzPoNNMAcbVmVc5vn7k6hdbPSHw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.2.8", + "@storybook/csf-plugin": "10.2.17", "ts-dedent": "^2.0.0" }, "funding": { @@ -1715,7 +1834,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8", + "storybook": "^10.2.17", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, @@ -1730,9 +1849,9 @@ } }, "node_modules/@storybook/csf-plugin": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.8.tgz", - "integrity": "sha512-kKkLYhRXb33YtIPdavD2DU25sb14sqPYdcQFpyqu4TaD9truPPqW8P5PLTUgERydt/eRvRlnhauPHavU1kjsnA==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.17.tgz", + "integrity": "sha512-crHH8i/4mwzeXpWRPgwvwX2vjytW42zyzTRySUax5dTU8o9sjk4y+Z9hkGx3Nmu1TvqseS8v1Z20saZr/tQcWw==", "dev": true, "license": "MIT", "dependencies": { @@ -1745,7 +1864,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.2.8", + "storybook": "^10.2.17", "vite": "*", "webpack": "*" }, @@ -1783,9 +1902,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.8.tgz", - "integrity": "sha512-Xde9X3VszFV1pTXfc2ZFM89XOCGRxJD8MUIzDwkcT9xaki5a+8srs/fsXj75fMY6gMYfcL5lNRZvCqg37HOmcQ==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.17.tgz", + "integrity": "sha512-x9Kb7eUSZ1zGsEw/TtWrvs1LwWIdNp8qoOQCgPEjdB07reSJcE8R3+ASWHJThmd4eZf66ZALPJyerejake4Osw==", "dev": true, "license": "MIT", "funding": { @@ -1795,13 +1914,13 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.8" + "storybook": "^10.2.17" } }, "node_modules/@storybook/svelte": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/svelte/-/svelte-10.2.8.tgz", - "integrity": "sha512-nJWNzZxAthG7M8tySz2r3P7aLIQUOxKHF9QP08NdcKt32wyw2JGsv+7x2R1Epvt4/XALz2eklsIrCQfDbzQOOg==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/svelte/-/svelte-10.2.17.tgz", + "integrity": "sha512-QG4mb5F4XNwEnGqYTT8XnugKczOaqFUrxHj6+5rhVM/8q+zd6Ygkjine/277A7nIsJW/DogY/QJqTwnWheia7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1813,19 +1932,19 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8", + "storybook": "^10.2.17", "svelte": "^5.0.0" } }, "node_modules/@storybook/svelte-vite": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/svelte-vite/-/svelte-vite-10.2.8.tgz", - "integrity": "sha512-HD1cH+1cDAK47l8qNYjY3Z4EMzTPFNrRXzerYHA+eo1cexwtMbRAAqioH4NnRXSCC0Px4s98hGUYH2BfPkuOQw==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/svelte-vite/-/svelte-vite-10.2.17.tgz", + "integrity": "sha512-vJ7eGGW+qM9L1PMmGFzNHOvIFvXBf0rHXkYYsDAR9mzzMP+/k70o7swFzzDCP6xjjzQpVrrG0xgXw/YAzMxiSg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "10.2.8", - "@storybook/svelte": "10.2.8", + "@storybook/builder-vite": "10.2.17", + "@storybook/svelte": "10.2.17", "magic-string": "^0.30.0", "svelte2tsx": "^0.7.44", "typescript": "^4.9.4 || ^5.0.0" @@ -1836,28 +1955,28 @@ }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", - "storybook": "^10.2.8", + "storybook": "^10.2.17", "svelte": "^5.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/@storybook/sveltekit": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@storybook/sveltekit/-/sveltekit-10.2.8.tgz", - "integrity": "sha512-DT0Vak/fFqvZ98mhn2kvJm5fjYikWtdj4dRKK8j3Nx/Lb+XeM803RDKN51p+l/nTAnWfrpMBmErNF9OZS3AZSw==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/@storybook/sveltekit/-/sveltekit-10.2.17.tgz", + "integrity": "sha512-vuJZOi9STv4vEfsRQDZj4UIUsqeHQrbUCCQqjYr1nF/ZMjG7+kRXbZSuXe7DMZkAeu3JqH/5Jj4hIrkILT20yg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "10.2.8", - "@storybook/svelte": "10.2.8", - "@storybook/svelte-vite": "10.2.8" + "@storybook/builder-vite": "10.2.17", + "@storybook/svelte": "10.2.17", + "@storybook/svelte-vite": "10.2.17" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.8", + "storybook": "^10.2.17", "svelte": "^5.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } @@ -1882,9 +2001,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.52.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.52.0.tgz", - "integrity": "sha512-zG+HmJuSF7eC0e7xt2htlOcEMAdEtlVdb7+gAr+ef08EhtwUsjLxcAwBgUCJY3/5p08OVOxVZti91WfXeuLvsg==", + "version": "2.53.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", + "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1893,12 +2012,11 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "sade": "^1.8.1", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, @@ -1910,10 +2028,10 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -1973,49 +2091,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], @@ -2026,13 +2144,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -2043,13 +2161,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], @@ -2060,13 +2178,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], @@ -2077,13 +2195,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], @@ -2094,81 +2212,93 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2184,21 +2314,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], @@ -2209,13 +2339,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], @@ -2226,28 +2356,28 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "node_modules/@tanstack/devtools-event-client": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.0.tgz", - "integrity": "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.2.tgz", + "integrity": "sha512-nerCPwV6RI4zQY+T5xxXEDOPgSF/gqf6dmCbDpTwkAvQJPHKgroHwKE5kvAcM3JC3ptdr5euwNV0//f8e+wmfQ==", "license": "MIT", "engines": { "node": ">=18" @@ -2281,14 +2411,14 @@ } }, "node_modules/@tanstack/form-core": { - "version": "1.28.3", - "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.3.tgz", - "integrity": "sha512-DBhnu1d5VfACAYOAZJO8tsEUHjWczZMJY8v/YrtAJNWpwvL/3ogDuz8e6yUB2m/iVTNq6K8yrnVN2nrX0/BX/w==", + "version": "1.28.4", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.4.tgz", + "integrity": "sha512-2eox5ePrJ6kvA1DXD5QHk/GeGr3VFZ0uYR63UgQOe7bUg6h1JfXaIMqTjZK9sdGyE4oRNqFpoW54H0pZM7nObQ==", "license": "MIT", "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", - "@tanstack/store": "^0.8.1" + "@tanstack/store": "^0.9.1" }, "funding": { "type": "github", @@ -2329,9 +2459,9 @@ } }, "node_modules/@tanstack/store": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.1.tgz", - "integrity": "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.2.tgz", + "integrity": "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==", "license": "MIT", "funding": { "type": "github", @@ -2339,22 +2469,22 @@ } }, "node_modules/@tanstack/svelte-form": { - "version": "1.28.3", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-form/-/svelte-form-1.28.3.tgz", - "integrity": "sha512-95opkSn2N8fYOWi7K1cxSRPh38bVm3/tVZshEVSbGACOUioyqt2RSJpQ0DobdRLcStdP78slyZTXA3GDUhSNIw==", + "version": "1.28.4", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-form/-/svelte-form-1.28.4.tgz", + "integrity": "sha512-/5J6MrIDQO+12RbvqTtQs+d2T3FuAhKWLQFUDSd3ikDvTG5GJ8gTcahXNijOJG0MajDGlaGPYxHzZA15ksXAKw==", "license": "MIT", "dependencies": { - "@tanstack/form-core": "1.28.3", - "@tanstack/svelte-store": "^0.9.1" + "@tanstack/form-core": "1.28.4", + "@tanstack/svelte-store": "^0.10.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "node_modules/@tanstack/svelte-query": { - "version": "6.0.18", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-6.0.18.tgz", - "integrity": "sha512-iGS8osfrIVUW5pkV4Ig6pspNIMtiNjGnVTNJKDas0m/QaNDFFIKbgg74rCzcjwrTIvO38tMpzb4VUKklvAmjxw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-6.1.0.tgz", + "integrity": "sha512-iKeMaBalk5NeRvp1Y2LgtG3j9HCeLcTEpfsyM+jL1wASnW/EYdCqyyotMKkOY4SNsnBpPDM5zrmstMn6tOzKFQ==", "license": "MIT", "dependencies": { "@tanstack/query-core": "5.90.20" @@ -2386,12 +2516,12 @@ } }, "node_modules/@tanstack/svelte-store": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-store/-/svelte-store-0.9.1.tgz", - "integrity": "sha512-4RYp0CXSB9tjlUZNl29mjraWeRquKzuaW+bGGI4s3kS6BWatgt7BfX4OtoLT8MTBdepW9ARwqHZ3s8YGpfOZkQ==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-store/-/svelte-store-0.10.2.tgz", + "integrity": "sha512-6yrYg6ukZZeqPf3CY5+3H0DI6bUoLNwmImgqLNchavK89LtHL4Sel1fDyLtXymMssK4kYrJOQQ9+BBZWZhhTDw==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.8.1" + "@tanstack/store": "0.9.2" }, "funding": { "type": "github", @@ -2656,13 +2786,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", + "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/react": { @@ -2703,17 +2833,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", - "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -2726,7 +2856,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -2742,16 +2872,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", - "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "engines": { @@ -2767,14 +2897,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", - "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "engines": { @@ -2789,14 +2919,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", - "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2807,9 +2937,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", - "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", "dev": true, "license": "MIT", "engines": { @@ -2824,15 +2954,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", - "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -2849,9 +2979,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", - "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", "dev": true, "license": "MIT", "engines": { @@ -2863,18 +2993,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", - "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -2890,43 +3020,56 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", - "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2941,13 +3084,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", - "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2959,9 +3102,9 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", - "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3247,6 +3390,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -3312,9 +3456,9 @@ } }, "node_modules/bits-ui": { - "version": "2.15.5", - "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.15.5.tgz", - "integrity": "sha512-WhS+P+E//ClLfKU6KqjKC17nGDRLnz+vkwoP6ClFUPd5m1fFVDxTElPX8QVsduLj5V1KFDxlnv6sW2G5Lqk+vw==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.16.3.tgz", + "integrity": "sha512-5hJ5dEhf5yPzkRFcxzgQHScGodeo0gK0MUUXrdLlRHWaBOBGZiacWLG96j/wwFatKwZvouw7q+sn14i0fx3RIg==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.1", @@ -3583,6 +3727,19 @@ "dev": true, "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4268,9 +4425,9 @@ } }, "node_modules/devalue": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", - "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", "license": "MIT" }, "node_modules/devlop": { @@ -4294,9 +4451,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4323,14 +4480,14 @@ "license": "MIT" }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -4525,26 +4682,26 @@ } }, "node_modules/eslint-plugin-perfectionist": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.5.0.tgz", - "integrity": "sha512-lZX2KUpwOQf7J27gAg/6vt8ugdPULOLmelM8oDJPMbaN7P2zNNeyS9yxGSmJcKX0SF9qR/962l9RWM2Z5jpPzg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.6.0.tgz", + "integrity": "sha512-pxrLrfRp5wl1Vol1fAEa/G5yTXxefTPJjz07qC7a8iWFXcOZNuWBItMQ2OtTzfQIvMq6bMyYcrzc3Wz++na55Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.54.0", + "@typescript-eslint/utils": "^8.56.0", "natural-orderby": "^5.0.0" }, "engines": { "node": "^20.0.0 || >=22.0.0" }, "peerDependencies": { - "eslint": ">=8.45.0" + "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-storybook": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.8.tgz", - "integrity": "sha512-BtysXrg1RoYT3DIrCc+svZ0+L3mbWsu7suxTLGrihBY5HfWHkJge+qjlBBR1Nm2ZMslfuFS5K0NUWbWCJRu6kg==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.17.tgz", + "integrity": "sha512-LtzVBHcq+RbrhTnF1rFNpc5bmg/kmdDsw/6bIKOnyDY4r0g5ldZSNN3R/fxLrhFOL2DhmmDywN9lcFNqHCP3vQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4552,13 +4709,13 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^10.2.8" + "storybook": "^10.2.17" } }, "node_modules/eslint-plugin-svelte": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.15.0.tgz", - "integrity": "sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.15.2.tgz", + "integrity": "sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -4794,6 +4951,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5393,16 +5567,16 @@ "license": "MIT" }, "node_modules/layerchart": { - "version": "2.0.0-next.44", - "resolved": "https://registry.npmjs.org/layerchart/-/layerchart-2.0.0-next.44.tgz", - "integrity": "sha512-ivFNjkwjGWbYMw45xr98O1BjUuAtAa1QqvQ1j22ww/UKe+Z72cUiZBoXXbG4bBtok+YJNmEPgSF3Oky2DUkWkg==", + "version": "2.0.0-next.46", + "resolved": "https://registry.npmjs.org/layerchart/-/layerchart-2.0.0-next.46.tgz", + "integrity": "sha512-fpdnvexCG/chonAIunDdLbqrUrD0IZ2v6MG/kbI5v7/yi0bIC47er1Kz31sACQNuoK+sxMIvWeebMHqiQP5XSQ==", "license": "MIT", "dependencies": { - "@dagrejs/dagre": "^1.1.5", - "@layerstack/svelte-actions": "1.0.1-next.14", - "@layerstack/svelte-state": "0.1.0-next.19", - "@layerstack/tailwind": "2.0.0-next.17", - "@layerstack/utils": "2.0.0-next.14", + "@dagrejs/dagre": "^2.0.4", + "@layerstack/svelte-actions": "1.0.1-next.18", + "@layerstack/svelte-state": "0.1.0-next.23", + "@layerstack/tailwind": "2.0.0-next.21", + "@layerstack/utils": "2.0.0-next.18", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-delaunay": "^6.0.4", @@ -5422,30 +5596,13 @@ "d3-shape": "^3.2.0", "d3-tile": "^1.0.0", "d3-time": "^3.1.0", - "lodash-es": "^4.17.21", - "memoize": "^10.1.0", - "runed": "^0.31.1" + "memoize": "^10.2.0", + "runed": "^0.37.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, - "node_modules/layerchart/node_modules/runed": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/runed/-/runed-0.31.1.tgz", - "integrity": "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==", - "funding": [ - "https://github.com/sponsors/huntabyte", - "https://github.com/sponsors/tglide" - ], - "license": "MIT", - "dependencies": { - "esm-env": "^1.0.0" - }, - "peerDependencies": { - "svelte": "^5.7.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5461,9 +5618,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "devOptional": true, "license": "MPL-2.0", "dependencies": { @@ -5477,23 +5634,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -5511,9 +5668,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -5531,9 +5688,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -5551,9 +5708,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -5571,9 +5728,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -5591,12 +5748,15 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5611,12 +5771,15 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5631,12 +5794,15 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5651,12 +5817,15 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5671,9 +5840,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -5691,9 +5860,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -5742,12 +5911,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6034,7 +6197,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6725,9 +6888,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.1.tgz", - "integrity": "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", + "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -7136,7 +7299,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mri": "^1.1.0" @@ -7222,19 +7385,22 @@ } }, "node_modules/shiki": { - "version": "3.22.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.22.0.tgz", - "integrity": "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.22.0", - "@shikijs/engine-javascript": "3.22.0", - "@shikijs/engine-oniguruma": "3.22.0", - "@shikijs/langs": "3.22.0", - "@shikijs/themes": "3.22.0", - "@shikijs/types": "3.22.0", + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/should": { @@ -7399,9 +7565,9 @@ "license": "MIT" }, "node_modules/storybook": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.8.tgz", - "integrity": "sha512-885uSIn8NQw2ZG7vy84K45lHCOSyz1DVsDV8pHiHQj3J0riCuWLNeO50lK9z98zE8kjhgTtxAAkMTy5nkmNRKQ==", + "version": "10.2.17", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.17.tgz", + "integrity": "sha512-yueTpl5YJqLzQqs3CanxNdAAfFU23iP0j+JVJURE4ghfEtRmWfWoZWLGkVcyjmgum7UmjwAlqRuOjQDNvH89kw==", "dev": true, "license": "MIT", "dependencies": { @@ -7554,9 +7720,9 @@ } }, "node_modules/svelte": { - "version": "5.51.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.51.2.tgz", - "integrity": "sha512-AqApqNOxVS97V4Ko9UHTHeSuDJrwauJhZpLDs1gYD8Jk48ntCSWD7NxKje+fnGn5Ja1O3u2FzQZHPdifQjXe3w==", + "version": "5.53.10", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.10.tgz", + "integrity": "sha512-UcNfWzbrjvYXYSk+U2hME25kpb87oq6/WVLeBF4khyQrb3Ob/URVlN23khal+RbdCUTMfg4qWjI9KZjCNFtYMQ==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -7565,10 +7731,10 @@ "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -7626,9 +7792,9 @@ "license": "MIT" }, "node_modules/svelte-check": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.0.tgz", - "integrity": "sha512-gB3FdEPb8tPO3Y7Dzc6d/Pm/KrXAhK+0Fk+LkcysVtupvAh6Y/IrBCEZNupq57oh0hcwlxCUamu/rq7GtvfSEg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", + "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", "dev": true, "license": "MIT", "dependencies": { @@ -7680,9 +7846,9 @@ } }, "node_modules/svelte-sonner": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.0.7.tgz", - "integrity": "sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.0.tgz", + "integrity": "sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==", "license": "MIT", "dependencies": { "runed": "^0.28.0" @@ -7760,6 +7926,15 @@ } } }, + "node_modules/svelte/node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/svelte/node_modules/esrap": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz", @@ -7770,9 +7945,9 @@ } }, "node_modules/svelte2tsx": { - "version": "0.7.48", - "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.48.tgz", - "integrity": "sha512-B15C8dtOY6C9MbnQJDCkzbK3yByInzKtXrr23QCoF8APHMh6JaDhjCMcRl6ay4qaeKYqkX4X3tNaJrsZL45Zlg==", + "version": "0.7.52", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.52.tgz", + "integrity": "sha512-svdT1FTrCLpvlU62evO5YdJt/kQ7nxgQxII/9BpQUvKr+GJRVdAXNVw8UWOt0fhoe5uWKyU0WsUTMRVAtRbMQg==", "dev": true, "license": "MIT", "dependencies": { @@ -7792,14 +7967,15 @@ "license": "ISC" }, "node_modules/swagger-typescript-api": { - "version": "13.2.18", - "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.2.18.tgz", - "integrity": "sha512-YLcEdW3weYRB1BokCJv8RnxPdPRV36segDLu1kVIn+02S9emDU86CwRuZKmQ3izZRAuWtRYvCzB0p1vwb8mwgw==", + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.4.0.tgz", + "integrity": "sha512-p2F4mb8VewzWg1Re8MXrNjANXaYkXY+6lAn4bmYxwan58fAhzNcjywhpKSd0B6bqyv5dAa3sfqSzT8ZvwH9fIQ==", "dev": true, "license": "MIT", "dependencies": { + "@apidevtools/swagger-parser": "12.1.0", "@biomejs/js-api": "4.0.0", - "@biomejs/wasm-nodejs": "2.3.15", + "@biomejs/wasm-nodejs": "2.4.6", "@types/swagger-schema-official": "^2.0.25", "c12": "^3.3.3", "citty": "^0.2.1", @@ -7812,7 +7988,8 @@ "swagger2openapi": "^7.0.8", "type-fest": "^5.4.4", "typescript": "~5.9.3", - "yaml": "^2.8.2" + "yaml": "^2.8.2", + "yummies": "7.10.0" }, "bin": { "sta": "dist/cli.mjs", @@ -7922,9 +8099,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.1.tgz", - "integrity": "sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -7951,9 +8128,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", "license": "MIT" }, "node_modules/tapable": { @@ -8185,16 +8362,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", - "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8219,9 +8396,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "devOptional": true, "license": "MIT" }, @@ -8780,9 +8957,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": { @@ -8902,6 +9079,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yummies": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/yummies/-/yummies-7.10.0.tgz", + "integrity": "sha512-BXMmEYzFr/DtSvr1VMvctPVxQ/BZnx35oY2AGs6clkqgWnflsjqfuVle+8rH2uiMenmEf4I3OH9bRRW86yYyyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "dompurify": "^3.2.6", + "nanoid": "^5.1.5", + "tailwind-merge": "^3.3.1" + }, + "peerDependencies": { + "mobx": "^6.12.4", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "mobx": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/yummies/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", diff --git a/src/Exceptionless.Web/ClientApp/package.json b/src/Exceptionless.Web/ClientApp/package.json index 76692cbd24..373f7b992e 100644 --- a/src/Exceptionless.Web/ClientApp/package.json +++ b/src/Exceptionless.Web/ClientApp/package.json @@ -26,41 +26,41 @@ }, "devDependencies": { "@chromatic-com/storybook": "^5.0.1", - "@eslint/compat": "^2.0.2", + "@eslint/compat": "^2.0.3", "@eslint/js": "^9.39.2", - "@iconify-json/lucide": "^1.2.90", + "@iconify-json/lucide": "^1.2.96", "@playwright/test": "^1.58.2", - "@storybook/addon-a11y": "^10.2.8", - "@storybook/addon-docs": "^10.2.8", + "@storybook/addon-a11y": "^10.2.17", + "@storybook/addon-docs": "^10.2.17", "@storybook/addon-svelte-csf": "^5.0.11", - "@storybook/sveltekit": "^10.2.8", + "@storybook/sveltekit": "^10.2.17", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.52.0", + "@sveltejs/kit": "^2.53.4", "@sveltejs/vite-plugin-svelte": "^6.2.4", - "@tailwindcss/vite": "^4.1.18", + "@tailwindcss/vite": "^4.2.1", "@tanstack/eslint-plugin-query": "^5.91.4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/eslint": "^9.6.1", - "@types/node": "^25.2.3", + "@types/node": "^25.4.0", "@types/throttle-debounce": "^5.0.2", "cross-env": "^10.1.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-perfectionist": "^5.5.0", - "eslint-plugin-storybook": "^10.2.8", - "eslint-plugin-svelte": "^3.15.0", + "eslint-plugin-perfectionist": "^5.6.0", + "eslint-plugin-storybook": "^10.2.17", + "eslint-plugin-svelte": "^3.15.2", "jsdom": "^28.1.0", "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-svelte": "^3.5.1", "prettier-plugin-tailwindcss": "^0.7.2", - "storybook": "^10.2.8", - "svelte": "^5.51.2", - "svelte-check": "^4.4.0", - "swagger-typescript-api": "^13.2.18", + "storybook": "^10.2.17", + "svelte": "^5.53.10", + "svelte-check": "^4.4.5", + "swagger-typescript-api": "^13.4.0", "tslib": "^2.8.1", "typescript": "^5.9.3", - "typescript-eslint": "^8.56.0", + "typescript-eslint": "^8.57.0", "vite": "^7.3.1", "vitest": "4.0.18", "vitest-websocket-mock": "^0.5.0", @@ -69,30 +69,30 @@ "dependencies": { "@exceptionless/browser": "^3.1.0", "@exceptionless/fetchclient": "^0.44.0", - "@internationalized/date": "^3.11.0", - "@lucide/svelte": "^0.564.0", - "@tanstack/svelte-form": "^1.28.3", - "@tanstack/svelte-query": "^6.0.18", + "@internationalized/date": "^3.12.0", + "@lucide/svelte": "^0.577.0", + "@tanstack/svelte-form": "^1.28.4", + "@tanstack/svelte-query": "^6.1.0", "@tanstack/svelte-query-devtools": "^6.0.4", "@tanstack/svelte-table": "^9.0.0-alpha.10", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", - "bits-ui": "^2.15.5", + "bits-ui": "^2.16.3", "clsx": "^2.1.1", "d3-scale": "^4.0.2", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "kit-query-params": "^0.0.26", - "layerchart": "^2.0.0-next.44", + "layerchart": "^2.0.0-next.46", "mode-watcher": "^1.1.0", "oidc-client-ts": "^3.4.1", "pretty-ms": "^9.3.0", "runed": "^0.37.1", - "shiki": "^3.22.0", - "svelte-sonner": "^1.0.7", + "shiki": "^4.0.2", + "svelte-sonner": "^1.1.0", "svelte-time": "^2.1.0", - "tailwind-merge": "^3.4.1", + "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.1.18", + "tailwindcss": "^4.2.1", "throttle-debounce": "^5.0.2", "tw-animate-css": "^1.4.0" }, @@ -100,4 +100,4 @@ "overrides": { "storybook": "$storybook" } -} +} \ No newline at end of file From 6a4778759ee2654eda66e11acb5a97ff28ecdada Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Mar 2026 07:13:10 -0500 Subject: [PATCH 13/29] Fixed skill names --- .agents/skills/accessibility/SKILL.md | 6 ++--- .agents/skills/backend-architecture/SKILL.md | 10 ++++----- .agents/skills/backend-testing/SKILL.md | 2 +- .agents/skills/dotnet-cli/SKILL.md | 6 ++--- .agents/skills/dotnet-conventions/SKILL.md | 20 ++++++++--------- .agents/skills/e2e-testing/SKILL.md | 6 ++--- .agents/skills/foundatio/SKILL.md | 22 +++++++++---------- .agents/skills/frontend-architecture/SKILL.md | 6 ++--- .agents/skills/frontend-testing/SKILL.md | 6 ++--- .agents/skills/security-principles/SKILL.md | 6 ++--- .agents/skills/shadcn-svelte/SKILL.md | 6 ++--- .agents/skills/storybook/SKILL.md | 6 ++--- .agents/skills/svelte-components/SKILL.md | 6 ++--- .agents/skills/tanstack-form/SKILL.md | 6 ++--- .agents/skills/tanstack-query/SKILL.md | 6 ++--- .../skills/typescript-conventions/SKILL.md | 6 ++--- 16 files changed, 63 insertions(+), 63 deletions(-) diff --git a/.agents/skills/accessibility/SKILL.md b/.agents/skills/accessibility/SKILL.md index ac21a4157e..48d5541049 100644 --- a/.agents/skills/accessibility/SKILL.md +++ b/.agents/skills/accessibility/SKILL.md @@ -1,8 +1,8 @@ --- -name: Accessibility +name: accessibility description: | - WCAG 2.2 AA accessibility standards for the Exceptionless frontend. Semantic HTML, keyboard - navigation, ARIA patterns, focus management, and form accessibility. + WCAG 2.2 AA accessibility standards for the Exceptionless frontend. Semantic HTML, keyboard + navigation, ARIA patterns, focus management, and form accessibility. Keywords: WCAG, accessibility, a11y, ARIA, semantic HTML, keyboard navigation, focus management, screen reader, alt text, aria-label, aria-describedby, skip links, focus trap --- diff --git a/.agents/skills/backend-architecture/SKILL.md b/.agents/skills/backend-architecture/SKILL.md index 2f197285d7..29e09c15df 100644 --- a/.agents/skills/backend-architecture/SKILL.md +++ b/.agents/skills/backend-architecture/SKILL.md @@ -1,10 +1,10 @@ --- -name: Backend Architecture +name: backend-architecture description: | - Backend architecture for Exceptionless. Project layering, repositories, validation, - controllers, authorization, WebSockets, configuration, and Aspire orchestration. - Keywords: Core, Insulation, repositories, FluentValidation, MiniValidator, controllers, - AuthorizationRoles, ProblemDetails, Aspire, WebSockets, AppOptions + Backend architecture for Exceptionless. Project layering, repositories, validation, + controllers, authorization, WebSockets, configuration, and Aspire orchestration. + Keywords: Core, Insulation, repositories, FluentValidation, MiniValidator, controllers, + AuthorizationRoles, ProblemDetails, Aspire, WebSockets, AppOptions --- # Backend Architecture diff --git a/.agents/skills/backend-testing/SKILL.md b/.agents/skills/backend-testing/SKILL.md index e86cbe071e..0afdd6582e 100644 --- a/.agents/skills/backend-testing/SKILL.md +++ b/.agents/skills/backend-testing/SKILL.md @@ -1,5 +1,5 @@ --- -name: Backend Testing +name: backend-testing description: | Backend testing with xUnit, Foundatio.Xunit, integration tests with AppWebHostFactory, FluentClient, ProxyTimeProvider for time manipulation, and test data builders. diff --git a/.agents/skills/dotnet-cli/SKILL.md b/.agents/skills/dotnet-cli/SKILL.md index 0e17874d3e..289015780e 100644 --- a/.agents/skills/dotnet-cli/SKILL.md +++ b/.agents/skills/dotnet-cli/SKILL.md @@ -1,8 +1,8 @@ --- -name: .NET CLI +name: dotnet-cli description: | - .NET command-line tools for building, testing, and formatting. Common dotnet commands - and development workflow. + .NET command-line tools for building, testing, and formatting. Common dotnet commands + and development workflow. Keywords: dotnet build, dotnet restore, dotnet test, dotnet format, dotnet run, NuGet, package restore, CLI commands, build system --- diff --git a/.agents/skills/dotnet-conventions/SKILL.md b/.agents/skills/dotnet-conventions/SKILL.md index 88a3d66976..f04363bcff 100644 --- a/.agents/skills/dotnet-conventions/SKILL.md +++ b/.agents/skills/dotnet-conventions/SKILL.md @@ -1,8 +1,8 @@ --- -name: .NET Conventions +name: dotnet-conventions description: | - C# coding standards for the Exceptionless codebase. Naming conventions, async patterns, - structured logging, nullable reference types, and formatting rules. + C# coding standards for the Exceptionless codebase. Naming conventions, async patterns, + structured logging, nullable reference types, and formatting rules. Keywords: C# style, naming conventions, _camelCase, PascalCase, async suffix, CancellationToken, nullable annotations, structured logging, ExceptionlessState --- @@ -18,13 +18,13 @@ description: | ## Naming Conventions -| Element | Convention | Example | -|---------|------------|---------| -| Private fields | `_camelCase` | `_organizationRepository` | -| Public members | PascalCase | `GetByIdAsync` | -| Local variables | camelCase | `organizationId` | -| Constants | PascalCase | `MaxRetryCount` | -| Type parameters | `T` prefix | `TModel` | +| Element | Convention | Example | +| --------------- | ------------ | ------------------------- | +| Private fields | `_camelCase` | `_organizationRepository` | +| Public members | PascalCase | `GetByIdAsync` | +| Local variables | camelCase | `organizationId` | +| Constants | PascalCase | `MaxRetryCount` | +| Type parameters | `T` prefix | `TModel` | ## Formatting Rules diff --git a/.agents/skills/e2e-testing/SKILL.md b/.agents/skills/e2e-testing/SKILL.md index 5122b69835..6c3e278b70 100644 --- a/.agents/skills/e2e-testing/SKILL.md +++ b/.agents/skills/e2e-testing/SKILL.md @@ -1,8 +1,8 @@ --- -name: E2E Testing (Frontend) +name: e2e-testing description: | - End-to-end frontend testing with Playwright. Page Object Model, selectors, fixtures, - accessibility audits. Limited E2E coverage currently - area for improvement. + End-to-end frontend testing with Playwright. Page Object Model, selectors, fixtures, + accessibility audits. Limited E2E coverage currently - area for improvement. Keywords: Playwright, E2E, Page Object Model, POM, data-testid, getByRole, getByLabel, getByText, fixtures, axe-playwright, frontend testing --- diff --git a/.agents/skills/foundatio/SKILL.md b/.agents/skills/foundatio/SKILL.md index 5975511c00..e93930f4d1 100644 --- a/.agents/skills/foundatio/SKILL.md +++ b/.agents/skills/foundatio/SKILL.md @@ -1,8 +1,8 @@ --- -name: Foundatio +name: foundatio description: | - Foundatio infrastructure abstractions for caching, queuing, messaging, file storage, - locking, jobs, and resilience. Use context7 for complete API documentation. + Foundatio infrastructure abstractions for caching, queuing, messaging, file storage, + locking, jobs, and resilience. Use context7 for complete API documentation. Keywords: Foundatio, ICacheClient, IQueue, IMessageBus, IFileStorage, ILockProvider, IJob, QueueJobBase, resilience, retry, Redis, Elasticsearch --- @@ -15,14 +15,14 @@ Foundatio provides pluggable infrastructure abstractions. Use context7 MCP for c ## Core Abstractions -| Interface | Purpose | In-Memory | Production | -| --------- | ------- | --------- | ---------- | -| `ICacheClient` | Distributed caching | `InMemoryCacheClient` | Redis | -| `IQueue` | Message queuing | `InMemoryQueue` | Redis/SQS | -| `IMessageBus` | Pub/sub messaging | `InMemoryMessageBus` | Redis | -| `IFileStorage` | File storage | `InMemoryFileStorage` | S3/Azure | -| `ILockProvider` | Distributed locking | `InMemoryLockProvider` | Redis | -| `IResiliencePolicyProvider` | Retry/circuit breaker | N/A | Polly-based | +| Interface | Purpose | In-Memory | Production | +| --------------------------- | --------------------- | ---------------------- | ----------- | +| `ICacheClient` | Distributed caching | `InMemoryCacheClient` | Redis | +| `IQueue` | Message queuing | `InMemoryQueue` | Redis/SQS | +| `IMessageBus` | Pub/sub messaging | `InMemoryMessageBus` | Redis | +| `IFileStorage` | File storage | `InMemoryFileStorage` | S3/Azure | +| `ILockProvider` | Distributed locking | `InMemoryLockProvider` | Redis | +| `IResiliencePolicyProvider` | Retry/circuit breaker | N/A | Polly-based | ## ICacheClient diff --git a/.agents/skills/frontend-architecture/SKILL.md b/.agents/skills/frontend-architecture/SKILL.md index df1f636c56..b6609daa03 100644 --- a/.agents/skills/frontend-architecture/SKILL.md +++ b/.agents/skills/frontend-architecture/SKILL.md @@ -1,8 +1,8 @@ --- -name: Frontend Architecture +name: frontend-architecture description: | - Svelte SPA architecture for Exceptionless. Route groups, lib structure, API client, - feature slices, and barrel exports. + Svelte SPA architecture for Exceptionless. Route groups, lib structure, API client, + feature slices, and barrel exports. Keywords: route groups, $lib, feature slices, api-client, barrel exports, index.ts, vertical slices, shared components, generated models, ClientApp structure --- diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md index ada25aaff4..96c81daa18 100644 --- a/.agents/skills/frontend-testing/SKILL.md +++ b/.agents/skills/frontend-testing/SKILL.md @@ -1,8 +1,8 @@ --- -name: Frontend Testing +name: frontend-testing description: | - Unit and component testing for the frontend with Vitest and Testing Library. - Keywords: Vitest, @testing-library/svelte, component tests, vi.mock, render, screen, + Unit and component testing for the frontend with Vitest and Testing Library. + Keywords: Vitest, @testing-library/svelte, component tests, vi.mock, render, screen, fireEvent, userEvent, test.ts, spec.ts, describe, it, AAA pattern --- diff --git a/.agents/skills/security-principles/SKILL.md b/.agents/skills/security-principles/SKILL.md index f7652ec0e3..2e56a1e0f4 100644 --- a/.agents/skills/security-principles/SKILL.md +++ b/.agents/skills/security-principles/SKILL.md @@ -1,8 +1,8 @@ --- -name: Security Principles +name: security-principles description: | - Security best practices for the Exceptionless codebase. Secrets management, input validation, - secure defaults, and avoiding common vulnerabilities. + Security best practices for the Exceptionless codebase. Secrets management, input validation, + secure defaults, and avoiding common vulnerabilities. Keywords: security, secrets, encryption, PII, logging, input validation, secure defaults, environment variables, OWASP, cryptography --- diff --git a/.agents/skills/shadcn-svelte/SKILL.md b/.agents/skills/shadcn-svelte/SKILL.md index cd64feda11..95cbe8f603 100644 --- a/.agents/skills/shadcn-svelte/SKILL.md +++ b/.agents/skills/shadcn-svelte/SKILL.md @@ -1,8 +1,8 @@ --- -name: shadcn-svelte Components +name: shadcn-svelte description: | - UI components with shadcn-svelte and bits-ui. Component patterns, trigger snippets, - dialog handling, and accessibility. + UI components with shadcn-svelte and bits-ui. Component patterns, trigger snippets, + dialog handling, and accessibility. Keywords: shadcn-svelte, bits-ui, Button, Dialog, Sheet, Popover, DropdownMenu, Tooltip, Form, Input, Select, child snippet, trigger pattern, cn utility --- diff --git a/.agents/skills/storybook/SKILL.md b/.agents/skills/storybook/SKILL.md index b40b348914..b44ec8689f 100644 --- a/.agents/skills/storybook/SKILL.md +++ b/.agents/skills/storybook/SKILL.md @@ -1,8 +1,8 @@ --- -name: Storybook +name: storybook description: | - Component stories using Storybook with Svelte CSF. Story patterns, defineMeta, argTypes, - snippet-based customization, and visual testing. + Component stories using Storybook with Svelte CSF. Story patterns, defineMeta, argTypes, + snippet-based customization, and visual testing. Keywords: storybook, stories.svelte, defineMeta, Story, args, argTypes, autodocs --- diff --git a/.agents/skills/svelte-components/SKILL.md b/.agents/skills/svelte-components/SKILL.md index adce59f1ce..b57d36ebd7 100644 --- a/.agents/skills/svelte-components/SKILL.md +++ b/.agents/skills/svelte-components/SKILL.md @@ -1,8 +1,8 @@ --- -name: Svelte Components +name: svelte-components description: | - Svelte 5 component patterns for the Exceptionless SPA. Runes, reactivity, props, - events, snippets, component organization, and shadcn-svelte integration. + Svelte 5 component patterns for the Exceptionless SPA. Runes, reactivity, props, + events, snippets, component organization, and shadcn-svelte integration. Keywords: Svelte 5, $state, $derived, $effect, $props, runes, onclick, snippets, {@render}, reactive, component composition, shadcn-svelte --- diff --git a/.agents/skills/tanstack-form/SKILL.md b/.agents/skills/tanstack-form/SKILL.md index c68d3ee9f8..831a55d51a 100644 --- a/.agents/skills/tanstack-form/SKILL.md +++ b/.agents/skills/tanstack-form/SKILL.md @@ -1,8 +1,8 @@ --- -name: TanStack Form +name: tanstack-form description: | - TanStack Form with Zod validation in Svelte 5. Form state management, field validation, - error handling, and ProblemDetails integration. + TanStack Form with Zod validation in Svelte 5. Form state management, field validation, + error handling, and ProblemDetails integration. Keywords: TanStack Form, createForm, Field, form validation, zod schema, form errors, onSubmit, onSubmitAsync, problemDetailsToFormErrors --- diff --git a/.agents/skills/tanstack-query/SKILL.md b/.agents/skills/tanstack-query/SKILL.md index b19a51ab04..de7cc6a9f8 100644 --- a/.agents/skills/tanstack-query/SKILL.md +++ b/.agents/skills/tanstack-query/SKILL.md @@ -1,8 +1,8 @@ --- -name: TanStack Query +name: tanstack-query description: | - Data fetching and caching with TanStack Query in Svelte. Query patterns, mutations, - cache invalidation, WebSocket-driven updates, and optimistic updates. + Data fetching and caching with TanStack Query in Svelte. Query patterns, mutations, + cache invalidation, WebSocket-driven updates, and optimistic updates. Keywords: createQuery, createMutation, TanStack Query, query keys, cache invalidation, optimistic updates, refetch, stale time, @exceptionless/fetchclient, WebSocket --- diff --git a/.agents/skills/typescript-conventions/SKILL.md b/.agents/skills/typescript-conventions/SKILL.md index 50520954ce..c21c4e7dbe 100644 --- a/.agents/skills/typescript-conventions/SKILL.md +++ b/.agents/skills/typescript-conventions/SKILL.md @@ -1,8 +1,8 @@ --- -name: TypeScript Conventions +name: typescript-conventions description: | - TypeScript coding standards for the Exceptionless frontend. Naming, imports, error handling, - ESLint/Prettier configuration, and type safety. + TypeScript coding standards for the Exceptionless frontend. Naming, imports, error handling, + ESLint/Prettier configuration, and type safety. Keywords: TypeScript, ESLint, Prettier, naming conventions, kebab-case, named imports, type guards, interfaces, avoid any, Promise handling, try catch, braces --- From 17dbe6e21bcbfb30d0fc57dd5873d80f388d14a9 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Mar 2026 12:22:15 -0500 Subject: [PATCH 14/29] fixed lint --- src/Exceptionless.Web/ClientApp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exceptionless.Web/ClientApp/package.json b/src/Exceptionless.Web/ClientApp/package.json index 373f7b992e..20aad8ac4a 100644 --- a/src/Exceptionless.Web/ClientApp/package.json +++ b/src/Exceptionless.Web/ClientApp/package.json @@ -100,4 +100,4 @@ "overrides": { "storybook": "$storybook" } -} \ No newline at end of file +} From 282e2183fbcbd09533a42d057228f70a8f7de95b Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Mar 2026 13:49:54 -0500 Subject: [PATCH 15/29] Add project notification settings maintenance cleanup (#2145) * Add project notification settings maintenance cleanup * Fix organization cleanup review feedback * Revert cleanup notes from agents * Refine notification cleanup logic and update agent skills Adjusts the processing loop in the notification settings cleanup job to improve batch handling and timing. Also updates AI agent skill documentation to provide clearer usage instructions and normalize code formatting across the skill library. --- .agents/skills/accessibility/SKILL.md | 19 +- .agents/skills/backend-architecture/SKILL.md | 10 +- .agents/skills/backend-testing/SKILL.md | 10 +- .agents/skills/dotnet-cli/SKILL.md | 10 +- .agents/skills/dotnet-conventions/SKILL.md | 10 +- .agents/skills/e2e-testing/SKILL.md | 78 ++--- .agents/skills/foundatio/SKILL.md | 10 +- .agents/skills/frontend-architecture/SKILL.md | 52 +-- .agents/skills/frontend-testing/SKILL.md | 137 ++++---- .agents/skills/security-principles/SKILL.md | 10 +- .agents/skills/shadcn-svelte/SKILL.md | 24 +- .agents/skills/storybook/SKILL.md | 9 +- .agents/skills/svelte-components/SKILL.md | 12 +- .agents/skills/tanstack-form/SKILL.md | 24 +- .agents/skills/tanstack-query/SKILL.md | 143 +++++--- .../skills/typescript-conventions/SKILL.md | 40 +-- src/Exceptionless.Core/Bootstrapper.cs | 1 + ...jectNotificationSettingsWorkItemHandler.cs | 101 ++++++ ...dateProjectNotificationSettingsWorkItem.cs | 6 + .../Services/OrganizationService.cs | 143 +++++++- .../Controllers/AdminController.cs | 54 ++-- .../Controllers/OrganizationController.cs | 9 +- .../Controllers/AdminControllerTests.cs | 155 +++++++++ .../OrganizationControllerTests.cs | 124 +++++++ ...otificationSettingsWorkItemHandlerTests.cs | 305 ++++++++++++++++++ 25 files changed, 1181 insertions(+), 315 deletions(-) create mode 100644 src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs create mode 100644 src/Exceptionless.Core/Models/WorkItems/UpdateProjectNotificationSettingsWorkItem.cs create mode 100644 tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs diff --git a/.agents/skills/accessibility/SKILL.md b/.agents/skills/accessibility/SKILL.md index 48d5541049..0db396127f 100644 --- a/.agents/skills/accessibility/SKILL.md +++ b/.agents/skills/accessibility/SKILL.md @@ -1,10 +1,11 @@ --- name: accessibility -description: | - WCAG 2.2 AA accessibility standards for the Exceptionless frontend. Semantic HTML, keyboard - navigation, ARIA patterns, focus management, and form accessibility. - Keywords: WCAG, accessibility, a11y, ARIA, semantic HTML, keyboard navigation, focus management, - screen reader, alt text, aria-label, aria-describedby, skip links, focus trap +description: > + Use this skill when building or reviewing frontend components for accessibility compliance. + Covers WCAG 2.2 AA standards including semantic HTML, keyboard navigation, ARIA patterns, + focus management, screen reader support, and form accessibility. Apply when creating new + UI components, fixing accessibility bugs, adding skip links or focus traps, or ensuring + inclusive markup — even if the user doesn't explicitly mention "a11y" or "WCAG." --- # Accessibility (WCAG 2.2 AA) @@ -110,7 +111,7 @@ description: | // When dialog opens, focus first interactive element $effect(() => { if (open) { - dialogRef?.querySelector('input, button')?.focus(); + dialogRef?.querySelector("input, button")?.focus(); } }); @@ -240,10 +241,10 @@ npm run test:e2e ```typescript // In Playwright tests -import AxeBuilder from '@axe-core/playwright'; +import AxeBuilder from "@axe-core/playwright"; -test('page is accessible', async ({ page }) => { - await page.goto('/dashboard'); +test("page is accessible", async ({ page }) => { + await page.goto("/dashboard"); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); }); diff --git a/.agents/skills/backend-architecture/SKILL.md b/.agents/skills/backend-architecture/SKILL.md index 29e09c15df..c771180466 100644 --- a/.agents/skills/backend-architecture/SKILL.md +++ b/.agents/skills/backend-architecture/SKILL.md @@ -1,10 +1,10 @@ --- name: backend-architecture -description: | - Backend architecture for Exceptionless. Project layering, repositories, validation, - controllers, authorization, WebSockets, configuration, and Aspire orchestration. - Keywords: Core, Insulation, repositories, FluentValidation, MiniValidator, controllers, - AuthorizationRoles, ProblemDetails, Aspire, WebSockets, AppOptions +description: > + Use this skill when working on the ASP.NET Core backend — adding controllers, repositories, + validators, authorization, WebSocket endpoints, or Aspire orchestration. Apply when modifying + project layering (Core, Insulation, Web, Job), configuring services, returning ProblemDetails + errors, or understanding how the backend is structured. --- # Backend Architecture diff --git a/.agents/skills/backend-testing/SKILL.md b/.agents/skills/backend-testing/SKILL.md index 0afdd6582e..08acef96e8 100644 --- a/.agents/skills/backend-testing/SKILL.md +++ b/.agents/skills/backend-testing/SKILL.md @@ -1,10 +1,10 @@ --- name: backend-testing -description: | - Backend testing with xUnit, Foundatio.Xunit, integration tests with AppWebHostFactory, - FluentClient, ProxyTimeProvider for time manipulation, and test data builders. - Keywords: xUnit, Fact, Theory, integration tests, AppWebHostFactory, FluentClient, - ProxyTimeProvider, TimeProvider, Foundatio.Xunit, TestWithLoggingBase, test data builders +description: > + Use this skill when writing or modifying C# tests — unit tests, integration tests, or + test fixtures. Covers xUnit patterns, AppWebHostFactory for integration testing, FluentClient + for API assertions, ProxyTimeProvider for time manipulation, and test data builders. Apply + when adding new test cases, debugging test failures, or setting up test infrastructure. --- # Backend Testing diff --git a/.agents/skills/dotnet-cli/SKILL.md b/.agents/skills/dotnet-cli/SKILL.md index 289015780e..b20841d86c 100644 --- a/.agents/skills/dotnet-cli/SKILL.md +++ b/.agents/skills/dotnet-cli/SKILL.md @@ -1,10 +1,10 @@ --- name: dotnet-cli -description: | - .NET command-line tools for building, testing, and formatting. Common dotnet commands - and development workflow. - Keywords: dotnet build, dotnet restore, dotnet test, dotnet format, dotnet run, - NuGet, package restore, CLI commands, build system +description: > + Use this skill when running .NET CLI commands — building, testing, restoring packages, + formatting code, or running projects. Covers dotnet build, test, restore, format, run, + and NuGet package management. Apply when troubleshooting build errors, running the backend, + or executing any dotnet command-line operation. --- # .NET CLI diff --git a/.agents/skills/dotnet-conventions/SKILL.md b/.agents/skills/dotnet-conventions/SKILL.md index f04363bcff..540bbf10e6 100644 --- a/.agents/skills/dotnet-conventions/SKILL.md +++ b/.agents/skills/dotnet-conventions/SKILL.md @@ -1,10 +1,10 @@ --- name: dotnet-conventions -description: | - C# coding standards for the Exceptionless codebase. Naming conventions, async patterns, - structured logging, nullable reference types, and formatting rules. - Keywords: C# style, naming conventions, _camelCase, PascalCase, async suffix, - CancellationToken, nullable annotations, structured logging, ExceptionlessState +description: > + Use this skill when writing or reviewing C# code to follow project conventions. Covers + naming standards, async patterns, CancellationToken usage, structured logging, nullable + reference types, and formatting rules. Apply when authoring new C# classes, reviewing + code style, or ensuring consistency with existing patterns. --- # .NET Conventions diff --git a/.agents/skills/e2e-testing/SKILL.md b/.agents/skills/e2e-testing/SKILL.md index 6c3e278b70..9b05e26fa6 100644 --- a/.agents/skills/e2e-testing/SKILL.md +++ b/.agents/skills/e2e-testing/SKILL.md @@ -1,10 +1,10 @@ --- name: e2e-testing -description: | - End-to-end frontend testing with Playwright. Page Object Model, selectors, fixtures, - accessibility audits. Limited E2E coverage currently - area for improvement. - Keywords: Playwright, E2E, Page Object Model, POM, data-testid, getByRole, getByLabel, - getByText, fixtures, axe-playwright, frontend testing +description: > + Use this skill when writing or running end-to-end browser tests with Playwright. Covers + Page Object Model patterns, selector strategies (data-testid, getByRole, getByLabel), + fixtures, and accessibility audits with axe-playwright. Apply when adding E2E test coverage, + debugging flaky tests, or testing user flows through the browser. --- # E2E Testing (Frontend) @@ -24,7 +24,7 @@ Create page objects for reusable page interactions: ```typescript // e2e/pages/login-page.ts -import { type Page, type Locator, expect } from '@playwright/test'; +import { type Page, type Locator, expect } from "@playwright/test"; export class LoginPage { readonly page: Page; @@ -35,14 +35,14 @@ export class LoginPage { constructor(page: Page) { this.page = page; - this.emailInput = page.getByLabel('Email'); - this.passwordInput = page.getByLabel('Password'); - this.submitButton = page.getByRole('button', { name: /log in/i }); - this.errorMessage = page.getByRole('alert'); + this.emailInput = page.getByLabel("Email"); + this.passwordInput = page.getByLabel("Password"); + this.submitButton = page.getByRole("button", { name: /log in/i }); + this.errorMessage = page.getByRole("alert"); } async goto() { - await this.page.goto('/login'); + await this.page.goto("/login"); } async login(email: string, password: string) { @@ -61,26 +61,26 @@ export class LoginPage { ```typescript // e2e/auth/login.spec.ts -import { test, expect } from '@playwright/test'; -import { LoginPage } from '../pages/login-page'; +import { test, expect } from "@playwright/test"; +import { LoginPage } from "../pages/login-page"; -test.describe('Login', () => { - test('successful login redirects to dashboard', async ({ page }) => { +test.describe("Login", () => { + test("successful login redirects to dashboard", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); - await loginPage.login('user@example.com', 'password123'); + await loginPage.login("user@example.com", "password123"); - await expect(page).toHaveURL('/'); + await expect(page).toHaveURL("/"); }); - test('invalid credentials shows error', async ({ page }) => { + test("invalid credentials shows error", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); - await loginPage.login('wrong@example.com', 'wrongpassword'); + await loginPage.login("wrong@example.com", "wrongpassword"); - await loginPage.expectError('Invalid email or password'); + await loginPage.expectError("Invalid email or password"); }); }); ``` @@ -89,24 +89,24 @@ test.describe('Login', () => { 1. **Semantic selectors first**: - ```typescript - page.getByRole('button', { name: /submit/i }); - page.getByLabel('Email address'); - page.getByText('Welcome back'); - ``` + ```typescript + page.getByRole("button", { name: /submit/i }); + page.getByLabel("Email address"); + page.getByText("Welcome back"); + ``` 2. **Fallback to test IDs**: - ```typescript - page.getByTestId('stack-trace'); - ``` + ```typescript + page.getByTestId("stack-trace"); + ``` 3. **Avoid implementation details**: - ```typescript - // ❌ Avoid CSS classes and IDs - page.locator('.btn-primary'); - ``` + ```typescript + // ❌ Avoid CSS classes and IDs + page.locator(".btn-primary"); + ``` ## Backend Data Setup @@ -122,13 +122,13 @@ For tests requiring specific data, consider: ```typescript test.beforeEach(async ({ request }) => { // Set up test data via API - await request.post('/api/test/seed', { - data: { scenario: 'events-with-errors' } + await request.post("/api/test/seed", { + data: { scenario: "events-with-errors" }, }); }); test.afterEach(async ({ request }) => { - await request.delete('/api/test/cleanup'); + await request.delete("/api/test/cleanup"); }); ``` @@ -137,11 +137,11 @@ test.afterEach(async ({ request }) => { ## Accessibility Audits ```typescript -import { test, expect } from '@playwright/test'; -import AxeBuilder from '@axe-core/playwright'; +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; -test('login page has no accessibility violations', async ({ page }) => { - await page.goto('/login'); +test("login page has no accessibility violations", async ({ page }) => { + await page.goto("/login"); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); diff --git a/.agents/skills/foundatio/SKILL.md b/.agents/skills/foundatio/SKILL.md index e93930f4d1..72ed37ec8e 100644 --- a/.agents/skills/foundatio/SKILL.md +++ b/.agents/skills/foundatio/SKILL.md @@ -1,10 +1,10 @@ --- name: foundatio -description: | - Foundatio infrastructure abstractions for caching, queuing, messaging, file storage, - locking, jobs, and resilience. Use context7 for complete API documentation. - Keywords: Foundatio, ICacheClient, IQueue, IMessageBus, IFileStorage, ILockProvider, - IJob, QueueJobBase, resilience, retry, Redis, Elasticsearch +description: > + Use this skill when working with Foundatio infrastructure abstractions — caching, queuing, + messaging, file storage, locking, or background jobs. Apply when using ICacheClient, IQueue, + IMessageBus, IFileStorage, ILockProvider, or IJob, or when implementing retry/resilience + patterns. Covers both in-memory and production (Redis, Elasticsearch) implementations. --- # Foundatio diff --git a/.agents/skills/frontend-architecture/SKILL.md b/.agents/skills/frontend-architecture/SKILL.md index b6609daa03..3fb9a9ae60 100644 --- a/.agents/skills/frontend-architecture/SKILL.md +++ b/.agents/skills/frontend-architecture/SKILL.md @@ -1,10 +1,10 @@ --- name: frontend-architecture -description: | - Svelte SPA architecture for Exceptionless. Route groups, lib structure, API client, - feature slices, and barrel exports. - Keywords: route groups, $lib, feature slices, api-client, barrel exports, index.ts, - vertical slices, shared components, generated models, ClientApp structure +description: > + Use this skill when working on the Svelte SPA's project structure — adding routes, creating + feature slices, organizing shared components, or understanding the ClientApp directory layout. + Covers route groups, $lib conventions, barrel exports, API client organization, and vertical + slice architecture. Apply when deciding where to place new files or components. --- # Frontend Architecture @@ -80,20 +80,25 @@ Centralize API calls per feature: ```typescript // features/organizations/api.svelte.ts -import { createQuery, createMutation, useQueryClient } from '@tanstack/svelte-query'; -import { useFetchClient } from '@exceptionless/fetchclient'; -import type { Organization, CreateOrganizationRequest } from './models'; +import { + createQuery, + createMutation, + useQueryClient, +} from "@tanstack/svelte-query"; +import { useFetchClient } from "@exceptionless/fetchclient"; +import type { Organization, CreateOrganizationRequest } from "./models"; export function getOrganizationsQuery() { const client = useFetchClient(); return createQuery(() => ({ - queryKey: ['organizations'], + queryKey: ["organizations"], queryFn: async () => { - const response = await client.getJSON('/organizations'); + const response = + await client.getJSON("/organizations"); if (!response.ok) throw response.problem; return response.data!; - } + }, })); } @@ -103,13 +108,16 @@ export function postOrganizationMutation() { return createMutation(() => ({ mutationFn: async (data: CreateOrganizationRequest) => { - const response = await client.postJSON('/organizations', data); + const response = await client.postJSON( + "/organizations", + data, + ); if (!response.ok) throw response.problem; return response.data!; }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['organizations'] }); - } + queryClient.invalidateQueries({ queryKey: ["organizations"] }); + }, })); } ``` @@ -123,8 +131,8 @@ Re-export generated models through feature model folders: export type { Organization, CreateOrganizationRequest, - UpdateOrganizationRequest -} from '$lib/generated'; + UpdateOrganizationRequest, +} from "$lib/generated"; // Add feature-specific types export interface OrganizationWithStats extends Organization { @@ -139,9 +147,9 @@ Use `index.ts` for clean imports: ```typescript // features/organizations/index.ts -export { getOrganizationsQuery, postOrganizationMutation } from './api.svelte'; -export type { Organization, CreateOrganizationRequest } from './models'; -export { organizationSchema } from './schemas'; +export { getOrganizationsQuery, postOrganizationMutation } from "./api.svelte"; +export type { Organization, CreateOrganizationRequest } from "./models"; +export { organizationSchema } from "./schemas"; ``` ## Shared Components @@ -176,9 +184,9 @@ Prefer regeneration over hand-writing DTOs. Generated types live in `$lib/genera ```typescript // Configured in svelte.config.js -import { Button } from '$comp/ui/button'; // $lib/components -import { User } from '$features/users/models'; // $lib/features -import { formatDate } from '$shared/formatters'; // $lib/features/shared +import { Button } from "$comp/ui/button"; // $lib/components +import { User } from "$features/users/models"; // $lib/features +import { formatDate } from "$shared/formatters"; // $lib/features/shared ``` ## Composite Component Pattern diff --git a/.agents/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md index 96c81daa18..760982642a 100644 --- a/.agents/skills/frontend-testing/SKILL.md +++ b/.agents/skills/frontend-testing/SKILL.md @@ -1,9 +1,10 @@ --- name: frontend-testing -description: | - Unit and component testing for the frontend with Vitest and Testing Library. - Keywords: Vitest, @testing-library/svelte, component tests, vi.mock, render, screen, - fireEvent, userEvent, test.ts, spec.ts, describe, it, AAA pattern +description: > + Use this skill when writing or running frontend unit and component tests with Vitest and + Testing Library. Covers render/screen/fireEvent patterns, vi.mock for mocking, and the + AAA (Arrange-Act-Assert) test structure. Apply when adding test coverage for Svelte + components, debugging test failures, or setting up test utilities. --- # Frontend Testing @@ -27,10 +28,10 @@ npm run test:unit Use explicit Arrange, Act, Assert regions: ```typescript -import { describe, expect, it } from 'vitest'; +import { describe, expect, it } from "vitest"; -describe('Calculator', () => { - it('should add two numbers correctly', () => { +describe("Calculator", () => { + it("should add two numbers correctly", () => { // Arrange const a = 5; const b = 3; @@ -42,7 +43,7 @@ describe('Calculator', () => { expect(result).toBe(8); }); - it('should handle negative numbers', () => { + it("should handle negative numbers", () => { // Arrange const a = -5; const b = 3; @@ -63,11 +64,11 @@ describe('Calculator', () => { From [dates.test.ts](src/Exceptionless.Web/ClientApp/src/lib/features/shared/dates.test.ts): ```typescript -import { describe, expect, it } from 'vitest'; -import { getDifferenceInSeconds, getRelativeTimeFormatUnit } from './dates'; +import { describe, expect, it } from "vitest"; +import { getDifferenceInSeconds, getRelativeTimeFormatUnit } from "./dates"; -describe('getDifferenceInSeconds', () => { - it('should calculate difference in seconds correctly', () => { +describe("getDifferenceInSeconds", () => { + it("should calculate difference in seconds correctly", () => { // Arrange const now = new Date(); const past = new Date(now.getTime() - 5000); @@ -80,18 +81,18 @@ describe('getDifferenceInSeconds', () => { }); }); -describe('getRelativeTimeFormatUnit', () => { - it('should return correct unit for given seconds', () => { +describe("getRelativeTimeFormatUnit", () => { + it("should return correct unit for given seconds", () => { // Arrange & Act & Assert (simple value tests) - expect(getRelativeTimeFormatUnit(30)).toBe('seconds'); - expect(getRelativeTimeFormatUnit(1800)).toBe('minutes'); - expect(getRelativeTimeFormatUnit(7200)).toBe('hours'); + expect(getRelativeTimeFormatUnit(30)).toBe("seconds"); + expect(getRelativeTimeFormatUnit(1800)).toBe("minutes"); + expect(getRelativeTimeFormatUnit(7200)).toBe("hours"); }); - it('should handle boundary cases correctly', () => { + it("should handle boundary cases correctly", () => { // Arrange & Act & Assert - expect(getRelativeTimeFormatUnit(59)).toBe('seconds'); - expect(getRelativeTimeFormatUnit(60)).toBe('minutes'); + expect(getRelativeTimeFormatUnit(59)).toBe("seconds"); + expect(getRelativeTimeFormatUnit(60)).toBe("minutes"); }); }); ``` @@ -101,27 +102,27 @@ describe('getRelativeTimeFormatUnit', () => { From [cached-persisted-state.svelte.test.ts](src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/cached-persisted-state.svelte.test.ts): ```typescript -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { CachedPersistedState } from './cached-persisted-state.svelte'; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CachedPersistedState } from "./cached-persisted-state.svelte"; -describe('CachedPersistedState', () => { +describe("CachedPersistedState", () => { beforeEach(() => { vi.clearAllMocks(); }); - it('should initialize with default value when storage is empty', () => { + it("should initialize with default value when storage is empty", () => { // Arrange & Act - const state = new CachedPersistedState('test-key', 'default'); + const state = new CachedPersistedState("test-key", "default"); // Assert - expect(state.current).toBe('default'); + expect(state.current).toBe("default"); }); - it('should return cached value without reading storage repeatedly', () => { + it("should return cached value without reading storage repeatedly", () => { // Arrange - const getItemSpy = vi.spyOn(Storage.prototype, 'getItem'); - localStorage.setItem('test-key', 'value1'); - const state = new CachedPersistedState('test-key', 'default'); + const getItemSpy = vi.spyOn(Storage.prototype, "getItem"); + localStorage.setItem("test-key", "value1"); + const state = new CachedPersistedState("test-key", "default"); getItemSpy.mockClear(); // Act @@ -129,8 +130,8 @@ describe('CachedPersistedState', () => { const val2 = state.current; // Assert - expect(val1).toBe('value1'); - expect(val2).toBe('value1'); + expect(val1).toBe("value1"); + expect(val2).toBe("value1"); expect(getItemSpy).not.toHaveBeenCalled(); }); }); @@ -141,26 +142,44 @@ describe('CachedPersistedState', () => { From [helpers.svelte.test.ts](src/Exceptionless.Web/ClientApp/src/lib/features/events/components/filters/helpers.svelte.test.ts): ```typescript -import { describe, expect, it } from 'vitest'; -import { quoteIfSpecialCharacters } from './helpers.svelte'; +import { describe, expect, it } from "vitest"; +import { quoteIfSpecialCharacters } from "./helpers.svelte"; -describe('helpers.svelte', () => { - it('quoteIfSpecialCharacters handles tabs and newlines', () => { +describe("helpers.svelte", () => { + it("quoteIfSpecialCharacters handles tabs and newlines", () => { // Arrange & Act & Assert - expect(quoteIfSpecialCharacters('foo\tbar')).toBe('"foo\tbar"'); - expect(quoteIfSpecialCharacters('foo\nbar')).toBe('"foo\nbar"'); + expect(quoteIfSpecialCharacters("foo\tbar")).toBe('"foo\tbar"'); + expect(quoteIfSpecialCharacters("foo\nbar")).toBe('"foo\nbar"'); }); - it('quoteIfSpecialCharacters handles empty string and undefined/null', () => { + it("quoteIfSpecialCharacters handles empty string and undefined/null", () => { // Arrange & Act & Assert - expect(quoteIfSpecialCharacters('')).toBe(''); + expect(quoteIfSpecialCharacters("")).toBe(""); expect(quoteIfSpecialCharacters(undefined)).toBeUndefined(); expect(quoteIfSpecialCharacters(null)).toBeNull(); }); - it('quoteIfSpecialCharacters quotes all Lucene special characters', () => { + it("quoteIfSpecialCharacters quotes all Lucene special characters", () => { // Arrange - const luceneSpecials = ['+', '-', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '\\', '/']; + const luceneSpecials = [ + "+", + "-", + "!", + "(", + ")", + "{", + "}", + "[", + "]", + "^", + '"', + "~", + "*", + "?", + ":", + "\\", + "/", + ]; // Act & Assert for (const char of luceneSpecials) { @@ -176,50 +195,50 @@ Use accessible queries (not implementation details): ```typescript // ✅ Role-based -screen.getByRole('button', { name: /submit/i }); -screen.getByRole('textbox', { name: /email/i }); +screen.getByRole("button", { name: /submit/i }); +screen.getByRole("textbox", { name: /email/i }); // ✅ Label-based -screen.getByLabelText('Email address'); +screen.getByLabelText("Email address"); // ✅ Text-based -screen.getByText('Welcome back'); +screen.getByText("Welcome back"); // ⚠️ Fallback: Test ID -screen.getByTestId('complex-chart'); +screen.getByTestId("complex-chart"); // ❌ Avoid: Implementation details -screen.getByClassName('btn-primary'); +screen.getByClassName("btn-primary"); ``` ## Mocking Modules ```typescript -import { vi, describe, it, beforeEach, expect } from 'vitest'; -import { render, screen } from '@testing-library/svelte'; +import { vi, describe, it, beforeEach, expect } from "vitest"; +import { render, screen } from "@testing-library/svelte"; -vi.mock('$lib/api/organizations', () => ({ - getOrganizations: vi.fn() +vi.mock("$lib/api/organizations", () => ({ + getOrganizations: vi.fn(), })); -import { getOrganizations } from '$lib/api/organizations'; -import OrganizationList from './organization-list.svelte'; +import { getOrganizations } from "$lib/api/organizations"; +import OrganizationList from "./organization-list.svelte"; -describe('OrganizationList', () => { +describe("OrganizationList", () => { beforeEach(() => { vi.clearAllMocks(); }); - it('displays organizations from API', async () => { + it("displays organizations from API", async () => { // Arrange - const mockOrganizations = [{ id: '1', name: 'Org One' }]; + const mockOrganizations = [{ id: "1", name: "Org One" }]; vi.mocked(getOrganizations).mockResolvedValue(mockOrganizations); // Act render(OrganizationList); // Assert - expect(await screen.findByText('Org One')).toBeInTheDocument(); + expect(await screen.findByText("Org One")).toBeInTheDocument(); }); }); ``` @@ -227,7 +246,7 @@ describe('OrganizationList', () => { ## Snapshot Testing (Use Sparingly) ```typescript -it('matches snapshot', () => { +it("matches snapshot", () => { // Arrange & Act const { container } = render(StaticComponent); diff --git a/.agents/skills/security-principles/SKILL.md b/.agents/skills/security-principles/SKILL.md index 2e56a1e0f4..2fbb49b1cb 100644 --- a/.agents/skills/security-principles/SKILL.md +++ b/.agents/skills/security-principles/SKILL.md @@ -1,10 +1,10 @@ --- name: security-principles -description: | - Security best practices for the Exceptionless codebase. Secrets management, input validation, - secure defaults, and avoiding common vulnerabilities. - Keywords: security, secrets, encryption, PII, logging, input validation, secure defaults, - environment variables, OWASP, cryptography +description: > + Use this skill when handling secrets, credentials, PII, input validation, or any + security-sensitive code. Covers secrets management, secure defaults, encryption, logging + safety, and common vulnerability prevention. Apply when adding authentication, configuring + environment variables, reviewing code for security issues, or working with sensitive data. --- # Security Principles diff --git a/.agents/skills/shadcn-svelte/SKILL.md b/.agents/skills/shadcn-svelte/SKILL.md index 95cbe8f603..8e7cc0fe6a 100644 --- a/.agents/skills/shadcn-svelte/SKILL.md +++ b/.agents/skills/shadcn-svelte/SKILL.md @@ -1,10 +1,10 @@ --- name: shadcn-svelte -description: | - UI components with shadcn-svelte and bits-ui. Component patterns, trigger snippets, - dialog handling, and accessibility. - Keywords: shadcn-svelte, bits-ui, Button, Dialog, Sheet, Popover, DropdownMenu, - Tooltip, Form, Input, Select, child snippet, trigger pattern, cn utility +description: > + Use this skill when building UI with shadcn-svelte or bits-ui components — buttons, dialogs, + sheets, popovers, dropdowns, tooltips, forms, inputs, or selects. Covers import patterns, + trigger snippets, child snippet composition, and the cn utility. Apply when adding or + customizing any shadcn-svelte component in the frontend. --- # shadcn-svelte Components @@ -173,18 +173,18 @@ When using trigger components with custom elements like Button, **always use the ```typescript // options.ts -import type { DropdownItem } from '$shared/types'; +import type { DropdownItem } from "$shared/types"; export enum Status { - Active = 'active', - Inactive = 'inactive', - Pending = 'pending' + Active = "active", + Inactive = "inactive", + Pending = "pending", } export const statusOptions: DropdownItem[] = [ - { value: Status.Active, label: 'Active' }, - { value: Status.Inactive, label: 'Inactive' }, - { value: Status.Pending, label: 'Pending' } + { value: Status.Active, label: "Active" }, + { value: Status.Inactive, label: "Inactive" }, + { value: Status.Pending, label: "Pending" }, ]; ``` diff --git a/.agents/skills/storybook/SKILL.md b/.agents/skills/storybook/SKILL.md index b44ec8689f..8b7d786eee 100644 --- a/.agents/skills/storybook/SKILL.md +++ b/.agents/skills/storybook/SKILL.md @@ -1,9 +1,10 @@ --- name: storybook -description: | - Component stories using Storybook with Svelte CSF. Story patterns, defineMeta, argTypes, - snippet-based customization, and visual testing. - Keywords: storybook, stories.svelte, defineMeta, Story, args, argTypes, autodocs +description: > + Use this skill when creating or updating Storybook stories for Svelte components. Covers + Svelte CSF story format, defineMeta, argTypes, snippet-based customization, and autodocs. + Apply when adding visual documentation for components, setting up story files, or running + Storybook for development. --- # Storybook diff --git a/.agents/skills/svelte-components/SKILL.md b/.agents/skills/svelte-components/SKILL.md index b57d36ebd7..f838e69017 100644 --- a/.agents/skills/svelte-components/SKILL.md +++ b/.agents/skills/svelte-components/SKILL.md @@ -1,10 +1,10 @@ --- name: svelte-components -description: | - Svelte 5 component patterns for the Exceptionless SPA. Runes, reactivity, props, - events, snippets, component organization, and shadcn-svelte integration. - Keywords: Svelte 5, $state, $derived, $effect, $props, runes, onclick, snippets, - {@render}, reactive, component composition, shadcn-svelte +description: > + Use this skill when writing Svelte 5 components — using runes ($state, $derived, $effect, + $props), handling events, composing with snippets ({@render}), or integrating with + shadcn-svelte. Apply when creating new components, refactoring to Svelte 5 patterns, + or debugging reactivity issues in the frontend. --- # Svelte Components @@ -146,7 +146,7 @@ Use `onclick` instead of `on:click`: ## Snippets (Content Projection) -Replace `` with snippets. From [login/+page.svelte](src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte): +Replace `` with snippets. From [login/+page.svelte](): ```svelte state.errors}> diff --git a/.agents/skills/tanstack-form/SKILL.md b/.agents/skills/tanstack-form/SKILL.md index 831a55d51a..d7011192c3 100644 --- a/.agents/skills/tanstack-form/SKILL.md +++ b/.agents/skills/tanstack-form/SKILL.md @@ -1,10 +1,10 @@ --- name: tanstack-form -description: | - TanStack Form with Zod validation in Svelte 5. Form state management, field validation, - error handling, and ProblemDetails integration. - Keywords: TanStack Form, createForm, Field, form validation, zod schema, form errors, - onSubmit, onSubmitAsync, problemDetailsToFormErrors +description: > + Use this skill when building or modifying forms with TanStack Form and Zod validation. + Covers createForm, field-level validation, error handling, and mapping ProblemDetails + API errors to form fields. Apply when adding new forms, implementing validation logic, + or handling form submission in the Svelte frontend. --- # TanStack Form @@ -24,23 +24,23 @@ export type LoginFormData = Infer; // Extended in feature schemas.ts // From src/lib/features/auth/schemas.ts -import { ChangePasswordModelSchema } from '$generated/schemas'; +import { ChangePasswordModelSchema } from "$generated/schemas"; export const ChangePasswordSchema = ChangePasswordModelSchema.extend({ - confirm_password: string().min(6).max(100) + confirm_password: string().min(6).max(100), }).refine((data) => data.password === data.confirm_password, { - message: 'Passwords do not match', - path: ['confirm_password'] + message: "Passwords do not match", + path: ["confirm_password"], }); export type ChangePasswordFormData = Infer; // Re-export generated schemas -export { LoginSchema, type LoginFormData } from '$generated/schemas'; +export { LoginSchema, type LoginFormData } from "$generated/schemas"; ``` ## Basic Form Pattern -From [login/+page.svelte](src/Exceptionless.Web/ClientApp/src/routes/(auth)/login/+page.svelte): +From [login/+page.svelte](): ```svelte + + + +

{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + {action.label} + {action.description} + + +
+ {#if action.dangerous} +
+ + This is a destructive operation. Please ensure you understand the impact before proceeding. +
+ {/if} + + state.errors}> + {#snippet children(errors)} + + {/snippet} + + + {#if action.hasDateRange} +
+ + Start Date (UTC) + + + {#snippet child({ props: triggerProps })} + + {/snippet} + + + + + + Leave blank to use the default start date. + + + + End Date (UTC) + + + {#snippet child({ props: triggerProps })} + + {/snippet} + + + + + + Leave blank to process through the current date. + +
+ {/if} + + {#if action.hasOrganizationId && !organizationId} + + {#snippet children(field)} + + Organization ID + field.handleChange(e.currentTarget.value)} + aria-invalid={ariaInvalid(field)} + /> + + Restrict this job to a specific organization. + + {/snippet} + + {/if} + + (value === action.name ? undefined : `Type "${action.name}" to confirm`) }} + > + {#snippet children(field)} + + + Type {action.name} to confirm + + field.handleChange(e.currentTarget.value)} + aria-invalid={ariaInvalid(field)} + /> + + + {/snippet} + +
+ + + Cancel + state.isSubmitting || state.values.confirmText !== action.name}> + {#snippet children(isDisabled)} + + {isDisabled && form.state.isSubmitting ? 'Running...' : 'Run Job'} + + {/snippet} + + +
+ + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-data-table.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-data-table.svelte new file mode 100644 index 0000000000..b7c4659789 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-data-table.svelte @@ -0,0 +1,43 @@ + + + + {#if toolbarChildren} + + {@render toolbarChildren()} + + {:else} + + {/if} + + {#if isLoading} + + + + {:else} + + {/if} + + + +
+ + +
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts new file mode 100644 index 0000000000..988626f97b --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts @@ -0,0 +1,96 @@ +import type { ElasticsearchSnapshot } from '$features/admin/models'; + +import DateTime from '$comp/formatters/date-time.svelte'; +import Number from '$comp/formatters/number.svelte'; +import { getSharedTableOptions, type TableMemoryPagingParameters } from '$features/shared/table.svelte'; +import { type ColumnDef, getSortedRowModel, renderComponent } from '@tanstack/svelte-table'; + +import ShardsCell from './shards-cell.svelte'; +import SnapshotStatusCell from './snapshot-status-cell.svelte'; + +export function getColumns(): ColumnDef[] { + return [ + { + accessorKey: 'name', + cell: (info) => info.getValue(), + enableSorting: true, + header: 'Name', + meta: { + class: 'max-w-64 font-mono' + } + }, + { + accessorKey: 'repository', + cell: (info) => info.getValue(), + enableSorting: true, + header: 'Repository' + }, + { + accessorKey: 'status', + cell: (info) => renderComponent(SnapshotStatusCell, { value: info.getValue() as string | undefined }), + enableSorting: true, + header: 'Status' + }, + { + accessorKey: 'start_time', + cell: (info) => renderComponent(DateTime, { value: (info.getValue() as null | string) ?? undefined }), + enableSorting: true, + header: 'Started' + }, + { + accessorKey: 'duration', + cell: (info) => info.getValue() || '—', + enableSorting: false, + header: 'Duration', + meta: { + class: 'font-mono' + } + }, + { + accessorKey: 'indices_count', + cell: (info) => renderComponent(Number, { value: info.getValue() as null | number }), + enableSorting: true, + header: 'Indices', + meta: { + class: 'text-right' + } + }, + { + cell: ({ row }) => + renderComponent(ShardsCell, { + failedShards: row.original.failed_shards, + successfulShards: row.original.successful_shards, + totalShards: row.original.total_shards + }), + enableSorting: false, + header: 'Shards', + id: 'shards', + meta: { + class: 'text-right' + } + } + ]; +} + +export function getTableOptions(queryParameters: TableMemoryPagingParameters, getData: () => ElasticsearchSnapshot[]) { + return getSharedTableOptions({ + columnPersistenceKey: 'admin-backups', + get columns() { + return getColumns(); + }, + configureOptions: (options) => { + options.getRowId = (row) => row.repository + '/' + row.name; + options.getSortedRowModel = getSortedRowModel(); + options.initialState = { sorting: [{ desc: true, id: 'start_time' }] }; + options.manualSorting = false; + return options; + }, + paginationStrategy: 'memory', + get queryData() { + return getData(); + }, + get queryParameters() { + return queryParameters; + } + }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte new file mode 100644 index 0000000000..b11baae2a6 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte @@ -0,0 +1,14 @@ + + + + {healthLabel(value ?? '')} + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/indices-data-table.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/indices-data-table.svelte new file mode 100644 index 0000000000..7ac36abebc --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/indices-data-table.svelte @@ -0,0 +1,43 @@ + + + + {#if toolbarChildren} + + {@render toolbarChildren()} + + {:else} + + {/if} + + {#if isLoading} + + + + {:else} + + {/if} + + + +
+ + +
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/indices-options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/indices-options.svelte.ts new file mode 100644 index 0000000000..cce671df3f --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/indices-options.svelte.ts @@ -0,0 +1,106 @@ +import type { ElasticsearchIndexDetail } from '$features/admin/models'; + +import Bytes from '$comp/formatters/bytes.svelte'; +import Number from '$comp/formatters/number.svelte'; +import { getSharedTableOptions, type TableMemoryPagingParameters } from '$features/shared/table.svelte'; +import { type ColumnDef, getSortedRowModel, renderComponent } from '@tanstack/svelte-table'; + +import HealthBadgeCell from './health-badge-cell.svelte'; +import UnassignedShardsCell from './unassigned-shards-cell.svelte'; + +export function getColumns(): ColumnDef[] { + return [ + { + accessorKey: 'index', + cell: (info) => info.getValue(), + enableSorting: true, + header: 'Index', + meta: { + class: 'max-w-xs' + } + }, + { + accessorKey: 'health', + cell: (info) => renderComponent(HealthBadgeCell, { value: info.getValue() as null | string | undefined }), + enableSorting: true, + header: 'Health' + }, + { + accessorKey: 'status', + cell: (info) => info.getValue(), + enableSorting: true, + header: 'Status' + }, + { + accessorKey: 'primary', + cell: (info) => renderComponent(Number, { value: info.getValue() as null | number }), + enableSorting: true, + header: 'Primary', + meta: { + class: 'text-right' + } + }, + { + accessorKey: 'replica', + cell: (info) => renderComponent(Number, { value: info.getValue() as null | number }), + enableSorting: true, + header: 'Replica', + meta: { + class: 'text-right' + } + }, + { + accessorKey: 'unassigned_shards', + cell: (info) => renderComponent(UnassignedShardsCell, { value: info.getValue() as number }), + enableSorting: true, + header: 'Unassigned', + meta: { + class: 'text-right' + } + }, + { + accessorKey: 'docs_count', + cell: (info) => renderComponent(Number, { value: info.getValue() as null | number }), + enableSorting: true, + header: 'Documents', + meta: { + class: 'text-right' + } + }, + { + accessorKey: 'store_size_in_bytes', + cell: (info) => renderComponent(Bytes, { value: info.getValue() as null | number }), + enableSorting: true, + header: 'Size', + meta: { + class: 'text-right' + } + } + ]; +} + +export function getTableOptions(queryParameters: TableMemoryPagingParameters, getData: () => ElasticsearchIndexDetail[]) { + return getSharedTableOptions({ + columnPersistenceKey: 'admin-indices', + get columns() { + return getColumns(); + }, + configureOptions: (options) => { + options.getSortedRowModel = getSortedRowModel(); + options.initialState = { sorting: [{ desc: true, id: 'store_size_in_bytes' }] }; + options.manualSorting = false; + return options; + }, + defaultColumnVisibility: { + primary: false, + replica: false + }, + paginationStrategy: 'memory', + get queryData() { + return getData(); + }, + get queryParameters() { + return queryParameters; + } + }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-duration-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-duration-cell.svelte new file mode 100644 index 0000000000..dd33a3e403 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-duration-cell.svelte @@ -0,0 +1,19 @@ + + +{#if durationMs !== null} + +{:else} + +{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-error-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-error-cell.svelte new file mode 100644 index 0000000000..80915bf166 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-error-cell.svelte @@ -0,0 +1,15 @@ + + +{#if value} + {value} +{:else} + +{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-status-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-status-cell.svelte new file mode 100644 index 0000000000..352303773a --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-status-cell.svelte @@ -0,0 +1,28 @@ + + +
+ + {status} +
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-type-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-type-cell.svelte new file mode 100644 index 0000000000..1fc35cd8bb --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migration-type-cell.svelte @@ -0,0 +1,22 @@ + + +{migrationTypeLabel(value)} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migrations-data-table.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migrations-data-table.svelte new file mode 100644 index 0000000000..784564e469 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migrations-data-table.svelte @@ -0,0 +1,43 @@ + + + + {#if toolbarChildren} + + {@render toolbarChildren()} + + {:else} + + {/if} + + {#if isLoading} + + + + {:else} + + {/if} + + + +
+ + +
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migrations-options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migrations-options.svelte.ts new file mode 100644 index 0000000000..93082459a8 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/migrations-options.svelte.ts @@ -0,0 +1,108 @@ +import type { MigrationState, MigrationStatus } from '$features/admin/models'; + +import DateTime from '$comp/formatters/date-time.svelte'; +import { getSharedTableOptions, type TableMemoryPagingParameters } from '$features/shared/table.svelte'; +import { type ColumnDef, getSortedRowModel, renderComponent } from '@tanstack/svelte-table'; + +import MigrationDurationCell from './migration-duration-cell.svelte'; +import MigrationErrorCell from './migration-error-cell.svelte'; +import MigrationStatusCell from './migration-status-cell.svelte'; +import MigrationTypeCell from './migration-type-cell.svelte'; + +export type MigrationStateRow = MigrationState & { status: MigrationStatus }; + +export function getColumns(): ColumnDef[] { + return [ + { + accessorKey: 'id', + cell: (info) => info.getValue(), + enableSorting: true, + header: 'Name', + meta: { + class: 'max-w-xs font-medium' + } + }, + { + accessorKey: 'version', + cell: (info) => (info.getValue() as null | number) ?? '—', + enableSorting: true, + header: 'Version', + meta: { + class: 'text-right' + } + }, + { + accessorKey: 'migration_type', + cell: (info) => renderComponent(MigrationTypeCell, { value: info.getValue() as number }), + enableSorting: true, + header: 'Type' + }, + { + accessorKey: 'status', + cell: (info) => renderComponent(MigrationStatusCell, { status: info.getValue() as MigrationStatus }), + enableSorting: true, + header: 'Status' + }, + { + accessorKey: 'started_utc', + cell: (info) => renderComponent(DateTime, { value: (info.getValue() as null | string) ?? undefined }), + enableSorting: true, + header: 'Started' + }, + { + accessorKey: 'completed_utc', + cell: (info) => renderComponent(DateTime, { value: (info.getValue() as null | string) ?? undefined }), + enableSorting: true, + header: 'Completed' + }, + { + cell: ({ row }) => + renderComponent(MigrationDurationCell, { + completedUtc: row.original.completed_utc, + startedUtc: row.original.started_utc + }), + enableSorting: false, + header: 'Duration', + id: 'duration' + }, + { + accessorKey: 'error_message', + cell: (info) => renderComponent(MigrationErrorCell, { value: info.getValue() as null | string | undefined }), + enableSorting: false, + header: 'Error', + meta: { + class: 'max-w-xs' + } + } + ]; +} + +export function getTableOptions(queryParameters: TableMemoryPagingParameters, getData: () => MigrationStateRow[]) { + return getSharedTableOptions({ + columnPersistenceKey: 'admin-migrations', + get columns() { + return getColumns(); + }, + configureOptions: (options) => { + options.getRowId = (row) => row.id; + options.getSortedRowModel = getSortedRowModel(); + options.initialState = { + sorting: [{ desc: true, id: 'version' }] + }; + options.manualSorting = false; + return options; + }, + defaultColumnVisibility: { + completed_utc: false, + duration: false, + error_message: false + }, + paginationStrategy: 'memory', + get queryData() { + return getData(); + }, + get queryParameters() { + return queryParameters; + } + }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shard-metric-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shard-metric-cell.svelte new file mode 100644 index 0000000000..be6bb923e2 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shard-metric-cell.svelte @@ -0,0 +1,31 @@ + + +
+ {#if metricId === 'active_primary'} + + {:else if metricId === 'active_total'} + + {:else if metricId === 'relocating'} + + {:else if metricId === 'unassigned'} + {#if value > 0} + + {:else} + + {/if} + {/if} + {label} +
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shard-value-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shard-value-cell.svelte new file mode 100644 index 0000000000..5b89f6b744 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shard-value-cell.svelte @@ -0,0 +1,14 @@ + + + 0 ? 'font-semibold text-amber-500' : ''}> + + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-cell.svelte new file mode 100644 index 0000000000..5d49267f66 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-cell.svelte @@ -0,0 +1,18 @@ + + + 0 ? 'text-destructive font-semibold' : 'text-muted-foreground'}> + / + {#if failedShards > 0} + ( failed) + {/if} + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-overview-data-table.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-overview-data-table.svelte new file mode 100644 index 0000000000..4e064e8257 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-overview-data-table.svelte @@ -0,0 +1,27 @@ + + + + + {#if isLoading} + + + + {:else} + + {/if} + + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-overview-options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-overview-options.svelte.ts new file mode 100644 index 0000000000..71536e7aeb --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/shards-overview-options.svelte.ts @@ -0,0 +1,52 @@ +import type { ShardMetric } from '$features/admin/models'; + +import { getSharedTableOptions, type TableMemoryPagingParameters } from '$features/shared/table.svelte'; +import { type ColumnDef, renderComponent } from '@tanstack/svelte-table'; + +import ShardMetricCell from './shard-metric-cell.svelte'; +import ShardValueCell from './shard-value-cell.svelte'; + +export function getColumns(): ColumnDef[] { + return [ + { + accessorKey: 'label', + cell: (info) => + renderComponent(ShardMetricCell, { + label: info.row.original.label, + metricId: info.row.original.id, + value: info.row.original.value + }), + enableSorting: false, + header: 'Metric' + }, + { + accessorKey: 'value', + cell: (info) => + renderComponent(ShardValueCell, { + metricId: info.row.original.id, + value: info.getValue() as number + }), + enableSorting: false, + header: 'Count', + meta: { + class: 'text-right' + } + } + ]; +} + +export function getTableOptions(queryParameters: TableMemoryPagingParameters, getData: () => ShardMetric[]) { + return getSharedTableOptions({ + columnPersistenceKey: 'admin-shards-overview', + get columns() { + return getColumns(); + }, + paginationStrategy: 'memory', + get queryData() { + return getData(); + }, + get queryParameters() { + return queryParameters; + } + }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/snapshot-status-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/snapshot-status-cell.svelte new file mode 100644 index 0000000000..6744a0b18c --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/snapshot-status-cell.svelte @@ -0,0 +1,14 @@ + + + + {value?.toLowerCase().replaceAll('_', ' ')} + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/unassigned-shards-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/unassigned-shards-cell.svelte new file mode 100644 index 0000000000..3b5fccd88f --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/unassigned-shards-cell.svelte @@ -0,0 +1,13 @@ + + + + {#if value > 0}{:else}—{/if} + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts new file mode 100644 index 0000000000..f530306221 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts @@ -0,0 +1,85 @@ +export function healthBadgeClass(status: null | number | string | undefined): string { + const s = typeof status === 'string' ? status.toLowerCase() : status; + + if (s === 0 || s === 'green') { + return 'text-muted-foreground border-muted-foreground/30'; + } + + if (s === 1 || s === 'yellow') { + return 'border-amber-500 text-amber-600 dark:text-amber-400'; + } + + if (s === 2 || s === 'red') { + return 'border-destructive/50 text-destructive'; + } + + return 'text-muted-foreground border-muted-foreground/30'; +} + +export function healthColor(status: null | number | string | undefined): string { + const s = typeof status === 'string' ? status.toLowerCase() : status; + + if (s === 0 || s === 'green') { + return 'text-green-600'; + } + + if (s === 1 || s === 'yellow') { + return 'text-amber-500'; + } + + if (s === 2 || s === 'red') { + return 'text-destructive'; + } + + return 'text-muted-foreground'; +} + +export function healthLabel(status: null | number | string | undefined): string { + if (typeof status === 'string') { + return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + } + switch (status) { + case 0: + return 'Green'; + case 1: + return 'Yellow'; + case 2: + return 'Red'; + default: + return 'Unknown'; + } +} + +export function healthVariant(status: null | number | string | undefined): 'default' | 'destructive' | 'outline' | 'secondary' { + const s = typeof status === 'string' ? status.toLowerCase() : status; + + if (s === 2 || s === 'red') { + return 'destructive'; + } + + return 'outline'; +} + +export function snapshotBadgeClass(status: string | undefined): string { + switch (status?.toUpperCase()) { + case 'IN_PROGRESS': + return 'border-blue-500 text-blue-600 dark:text-blue-400'; + case 'PARTIAL': + return 'border-amber-500 text-amber-600 dark:text-amber-400'; + case 'SUCCESS': + return 'text-muted-foreground border-muted-foreground/30'; + default: + return ''; + } +} + +export function snapshotVariant(status: string | undefined): 'default' | 'destructive' | 'outline' | 'secondary' { + switch (status?.toUpperCase()) { + case 'FAILED': + return 'destructive'; + case 'IN_PROGRESS': + return 'secondary'; + default: + return 'outline'; + } +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/models.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/models.ts new file mode 100644 index 0000000000..0e2fe5ebe3 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/models.ts @@ -0,0 +1,186 @@ +import type { CountResult } from '$generated/api'; + +export enum MigrationType { + Versioned = 0, + VersionedAndResumable = 1, + Repeatable = 2 +} + +export type AdminStats = { + events: CountResult; + organizations: CountResult; + projects: CountResult; + stacks: CountResult; + users: CountResult; +}; + +export type ElasticsearchHealth = { + active_primary_shards: number; + active_shards: number; + cluster_name: string; + number_of_data_nodes: number; + number_of_nodes: number; + relocating_shards: number; + status: number; + unassigned_shards: number; +}; + +export type ElasticsearchIndexDetail = { + docs_count: number; + health?: null | string; + index?: null | string; + primary: number; + replica: number; + status?: null | string; + store_size_in_bytes: number; + unassigned_shards: number; +}; + +export type ElasticsearchIndices = { + count: number; + docs_count: number; + store_size_in_bytes: number; +}; + +export type ElasticsearchInfo = { + health: ElasticsearchHealth; + index_details: ElasticsearchIndexDetail[]; + indices: ElasticsearchIndices; +}; + +export type ElasticsearchSnapshot = { + duration: string; + end_time?: null | string; + failed_shards: number; + indices_count: number; + name: string; + repository: string; + start_time?: null | string; + status: string; + successful_shards: number; + total_shards: number; +}; + +export type ElasticsearchSnapshotsResponse = { + repositories: string[]; + snapshots: ElasticsearchSnapshot[]; +}; + +export type MaintenanceAction = { + category: MaintenanceActionCategory; + dangerous: boolean; + description: string; + hasDateRange?: boolean; + hasOrganizationId?: boolean; + label: string; + name: string; +}; + +export type MaintenanceActionCategory = 'Billing' | 'Configuration' | 'Elasticsearch' | 'Maintenance' | 'Security' | 'Users'; +export type MigrationsResponse = { + current_version: number; + states: MigrationState[]; +}; + +export type MigrationState = { + completed_utc?: null | string; + error_message?: null | string; + id: string; + migration_type: number; + started_utc?: null | string; + version: number; +}; + +export type MigrationStatus = 'Completed' | 'Failed' | 'Pending' | 'Running'; + +export type ShardMetric = { + id: string; + label: string; + value: number; +}; + +export const maintenanceActions: MaintenanceAction[] = [ + { + category: 'Elasticsearch', + dangerous: false, + description: + 'Runs Elasticsearch index setup for all indices: creates any missing indices and applies current field mappings. Does not reindex existing documents. Safe to run at any time to bring the schema in sync with new mapping definitions.', + label: 'Configure Indexes', + name: 'indexes' + }, + { + category: 'Billing', + dangerous: false, + description: + 'Re-applies the current billing plan limits and features (event limits, data retention, team size, etc.) to every organization without changing subscription status. Run after updating a plan definition to propagate changes to all existing subscribers.', + label: 'Update Organization Plans', + name: 'update-organization-plans' + }, + { + category: 'Maintenance', + dangerous: true, + description: + 'Permanently deletes hourly usage records older than 3 days and monthly usage records older than 366 days from every organization. Reduces document size and removes stale data no longer needed for billing or dashboards.', + label: 'Remove Old Organization Usage', + name: 'remove-old-organization-usage' + }, + { + category: 'Configuration', + dangerous: false, + description: + 'Re-stamps the latest system-default user-agent bot-filter patterns onto every project and bumps the configuration version, forcing all Exceptionless clients to refresh their local settings on the next request.', + label: 'Update Project Default Bot Lists', + name: 'update-project-default-bot-lists' + }, + { + category: 'Configuration', + dangerous: false, + description: + 'Bumps the configuration version counter on every project, forcing all connected Exceptionless clients to re-download their project settings (rate limits, user-agent filters, custom data exclusions, etc.) on the next heartbeat.', + label: 'Increment Project Configuration Version', + name: 'increment-project-configuration-version' + }, + { + category: 'Maintenance', + dangerous: true, + description: + 'Permanently deletes hourly usage records older than 3 days and monthly usage records older than 366 days from every project. Similar to organization usage cleanup but operates at the per-project level.', + label: 'Remove Old Project Usage', + name: 'remove-old-project-usage' + }, + { + category: 'Users', + dangerous: false, + description: + "Trims whitespace and lowercases every user's email address and full name. Fixes historical records created before strict normalization was enforced, ensuring consistent login lookups and deduplication.", + label: 'Normalize User Email Addresses', + name: 'normalize-user-email-address' + }, + { + category: 'Security', + dangerous: true, + description: + 'Generates a fresh random verification token and resets the expiration date for every unverified user account. Run before a bulk re-verification email campaign or after changing the token TTL policy. Does not send any emails.', + label: 'Reset Verify Email Address Tokens', + name: 'reset-verify-email-address-token-and-expiration' + }, + { + category: 'Elasticsearch', + dangerous: false, + description: + 'Re-derives first occurrence, last occurrence, and total event count for every stack by running aggregations against raw event documents. Only updates fields that are out-of-date. Accepts an optional date range and organization ID to limit scope. Corrects stale or corrupted stats caused by missed event counter flushes.', + hasDateRange: true, + hasOrganizationId: true, + label: 'Fix Stack Stats', + name: 'fix-stack-stats' + }, + { + category: 'Maintenance', + dangerous: false, + description: + 'Scans every project and removes notification settings for users who no longer belong to the organization. Accepts an optional organization ID to limit scope. Prevents stale user entries from accumulating in project notification settings.', + hasOrganizationId: true, + label: 'Update Project Notification Settings', + name: 'update-project-notification-settings' + } +]; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/schemas.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/schemas.ts new file mode 100644 index 0000000000..661e0abad5 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/schemas.ts @@ -0,0 +1,9 @@ +import { date, type infer as Infer, object, string } from 'zod'; + +export const RunMaintenanceJobSchema = object({ + confirmText: string().min(1), + organizationId: string().optional(), + utcEnd: date().optional(), + utcStart: date().optional() +}); +export type RunMaintenanceJobFormData = Infer; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte index 244d7443a3..2b407c2e42 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte @@ -128,9 +128,9 @@ > () {:else} - + - {/if} + {/if} {#if projectQuery.isSuccess} @@ -140,9 +140,9 @@ > {projectQuery.data.name} {:else} - + - + {/if} @@ -177,14 +177,14 @@ {/each} {:else} - + {#each { length: 5 } as name, index (`${name}-${index}`)} - + - + {/each} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte index 9cf98565c3..626daca8a4 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte @@ -252,7 +252,7 @@ -
+
{#if event.data?.['@error']} {:else if event.data?.['@simple_error']} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/impersonate-organization-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/impersonate-organization-dialog.svelte index 43047ca466..548ee7edc1 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/impersonate-organization-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/impersonate-organization-dialog.svelte @@ -47,7 +47,7 @@ let suspendedFilter = $state(undefined); let selectedOrganization = $state(null); let currentPage = $state(1); - const pageSize = 5; + const pageSize = 10; const searchResults = getAdminOrganizationsQuery({ params: { @@ -205,7 +205,7 @@ } }} > - + {#if paidFilter === undefined} All Plans {:else if paidFilter} @@ -232,7 +232,7 @@ } }} > - + {#if suspendedFilter === undefined} All Status {:else if suspendedFilter} @@ -253,7 +253,7 @@ {/if}
-
+
{#if searchResults.isFetching}
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-admin-actions-dropdown-menu.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-admin-actions-dropdown-menu.svelte index 5095c3bfe3..55b0b063df 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-admin-actions-dropdown-menu.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-admin-actions-dropdown-menu.svelte @@ -4,6 +4,9 @@ import { Button } from '$comp/ui/button'; import * as DropdownMenu from '$comp/ui/dropdown-menu'; + import { runMaintenanceJobMutation } from '$features/admin/api.svelte'; + import RunMaintenanceJobDialog from '$features/admin/components/dialogs/run-maintenance-job-dialog.svelte'; + import { maintenanceActions } from '$features/admin/models'; import { deleteSuspendOrganization, postSetBonusOrganization, postSuspendOrganization } from '$features/organizations/api.svelte'; import SetEventBonusDialog from '$features/organizations/components/dialogs/set-event-bonus-dialog.svelte'; import SuspendOrganizationDialog from '$features/organizations/components/dialogs/suspend-organization-dialog.svelte'; @@ -11,6 +14,7 @@ import Award from '@lucide/svelte/icons/award'; import Pause from '@lucide/svelte/icons/pause'; import Play from '@lucide/svelte/icons/play'; + import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import Shield from '@lucide/svelte/icons/shield'; import { toast } from 'svelte-sonner'; @@ -23,6 +27,7 @@ let toastId = $state(); let openSuspendOrganizationDialog = $state(false); let openSetEventBonusDialog = $state(false); + let openFixStackStatsDialog = $state(false); const markSuspended = postSuspendOrganization({ route: { @@ -41,6 +46,7 @@ }); const setOrganizationBonus = postSetBonusOrganization(); + const runJob = runMaintenanceJobMutation(); async function suspend(params: PostSuspendOrganizationParams) { toast.dismiss(toastId); @@ -83,6 +89,19 @@ function handleSetBonus() { openSetEventBonusDialog = true; } + + async function handleFixStackStats(params: Parameters[0]) { + toast.dismiss(toastId); + + try { + await runJob.mutateAsync(params); + toastId = toast.success('Successfully enqueued the Fix Stack Stats job.'); + } catch (error: unknown) { + const message = error instanceof ProblemDetails ? error.title : 'Please try again.'; + toastId = toast.error(`An error occurred while starting the job: ${message}`); + throw error; + } + } @@ -113,6 +132,11 @@ Set Bonus + + (openFixStackStatsDialog = true)} disabled={runJob.isPending}> + + Fix Stack Stats + @@ -124,3 +148,12 @@ {#if organization && openSetEventBonusDialog} {/if} + +{#if organization && openFixStackStatsDialog} + a.name === 'fix-stack-stats')!} + organizationId={organization.id} + onConfirm={handleFixStackStats} + /> +{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/table/organization-suspension-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/table/organization-suspension-cell.svelte index a004279ea7..179cd05a08 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/table/organization-suspension-cell.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/table/organization-suspension-cell.svelte @@ -11,5 +11,6 @@ {#if code} -{:else}x - +{:else} + - {/if} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-body.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-body.svelte index b3f28ff719..adda35925e 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-body.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-body.svelte @@ -21,8 +21,14 @@ let { children, rowClick, rowHref, table }: Props = $props(); function getHeaderColumnClass(header: Header) { - const classes = [(header.column.columnDef.meta as { class?: string })?.class || '']; - return classes.filter(Boolean).join(' '); + const metaClass = (header.column.columnDef.meta as { class?: string })?.class || ''; + if (!metaClass) { + return ''; + } + if (metaClass.includes('text-right')) { + return [metaClass, 'justify-end'].join(' '); + } + return metaClass; } function getCellClass(cell: Cell) { @@ -30,7 +36,9 @@ return; } - return 'cursor-pointer hover truncate max-w-sm'; + const metaClass = (cell.column.columnDef.meta as { class?: string })?.class ?? ''; + const classes = rowClick ? ['cursor-pointer', 'truncate', 'max-w-sm', metaClass] : ['truncate', 'max-w-sm', metaClass]; + return classes.filter(Boolean).join(' '); } function onCellClick(event: MouseEvent, cell: Cell): void { @@ -63,8 +71,9 @@ {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} - - + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-footer.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-footer.svelte index da229d18a9..9888a2b377 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-footer.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-footer.svelte @@ -20,7 +20,7 @@ let { children, class: className, table }: Props = $props(); -
+
{#if children} {@render children()} {:else} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-toolbar.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-toolbar.svelte index 4ab2282918..df63108800 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-toolbar.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/data-table/data-table-toolbar.svelte @@ -18,7 +18,7 @@ let { children, size = 'icon', table }: Props = $props(); -
+
{#if children} {@render children()} {:else} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-range-selector.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-range-selector.svelte index 0836c0d5c5..638781cb89 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-range-selector.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/date-range-picker/quick-range-selector.svelte @@ -29,7 +29,7 @@ value={option.label} onclick={() => selectQuick(option)} aria-selected={option.value === value} - class={['cursor-pointer', option.value === value ? 'bg-primary text-primary-foreground' : 'opacity-50 [&_svg]:invisible']} + class={['cursor-pointer', option.value === value ? 'bg-secondary text-secondary-foreground' : 'opacity-50 [&_svg]:invisible']} > {option.label} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/table.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/table.svelte.ts index aaf6c41f03..4bd38cf8b8 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/table.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/table.svelte.ts @@ -22,6 +22,7 @@ export interface TableConfiguration[]; configureOptions?: (options: TableOptions) => TableOptions; + defaultColumnVisibility?: VisibilityState; paginationStrategy: TPaginationStrategy; queryData?: TData[]; queryMeta?: QueryMeta; @@ -66,7 +67,7 @@ export function getSharedTableOptions{}); + const [columnVisibility, setColumnVisibility] = createPersistedTableState(visibilityKey, configuration.defaultColumnVisibility ?? {}); // Initialize pagination state from parameters const initialPageIndex = isOffsetPaging diff --git a/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts b/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts index f53b1bf1c1..856890de12 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts @@ -164,7 +164,7 @@ export interface OAuthAccount { provider: string; provider_user_id: string; username: string; - extra_data?: null | object; + extra_data: Record; } export interface PersistentEvent { @@ -223,7 +223,7 @@ export interface PersistentEvent { */ count?: null | number; /** Optional data entries that contain additional information about this event. */ - data?: null | object; + data?: null | Record; /** An optional identifier to be used for referencing this event instance at a later time. */ reference_id?: null | string; } @@ -419,7 +419,7 @@ export interface UserDescription { email_address?: null | string; description?: null | string; /** Extended data entries for this user description. */ - data?: null | object; + data?: null | Record; } export interface ViewCurrentUser { @@ -487,7 +487,7 @@ export interface ViewOrganization { invites: Invite[]; usage_hours: UsageHourInfo[]; usage: UsageInfo[]; - data?: null | object; + data?: null | Record; is_throttled: boolean; is_over_monthly_limit: boolean; is_over_request_limit: boolean; @@ -503,7 +503,7 @@ export interface ViewProject { organization_name: string; name: string; delete_bot_data_enabled: boolean; - data?: null | object; + data?: null | Record; promoted_tabs: string[]; is_configured?: null | boolean; /** @format int64 */ diff --git a/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts b/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts index 75f1d790ff..66e65ceef1 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/generated/schemas.ts @@ -224,7 +224,7 @@ export const OAuthAccountSchema = object({ provider: string().min(1, "Provider is required"), provider_user_id: string().min(1, "Provider user id is required"), username: string().min(1, "Username is required"), - extra_data: record(string(), unknown()).nullable().optional(), + extra_data: record(string(), string()), }); export type OAuthAccountFormData = Infer; @@ -407,7 +407,7 @@ export const UserSchema = object({ id: string() .length(24, "Id must be exactly 24 characters") .regex(/^[a-fA-F0-9]{24}$/, "Id has invalid format"), - organization_ids: array(string()).optional(), + organization_ids: array(string()), password: string().min(1, "Password is required").nullable().optional(), salt: string().min(1, "Salt is required").nullable().optional(), password_reset_token: string() @@ -415,7 +415,7 @@ export const UserSchema = object({ .nullable() .optional(), password_reset_token_expiration: iso.datetime(), - o_auth_accounts: array(lazy(() => OAuthAccountSchema)).optional(), + o_auth_accounts: array(lazy(() => OAuthAccountSchema)), full_name: string().min(1, "Full name is required"), email_address: email(), email_notifications_enabled: boolean(), @@ -603,7 +603,7 @@ export const WebHookSchema = object({ export type WebHookFormData = Infer; export const WorkInProgressResultSchema = object({ - workers: array(string()).optional(), + workers: array(string()), }); export type WorkInProgressResultFormData = Infer< typeof WorkInProgressResultSchema diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar-user.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar-user.svelte index 165b399639..df8b0b9dba 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar-user.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar-user.svelte @@ -21,12 +21,17 @@ import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down'; import Help from '@lucide/svelte/icons/circle-help'; import CreditCard from '@lucide/svelte/icons/credit-card'; + import Database from '@lucide/svelte/icons/database'; + import DatabaseZap from '@lucide/svelte/icons/database-zap'; import Eye from '@lucide/svelte/icons/eye'; import EyeOff from '@lucide/svelte/icons/eye-off'; import GitHub from '@lucide/svelte/icons/github'; + import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard'; import LogOut from '@lucide/svelte/icons/log-out'; + import Play from '@lucide/svelte/icons/play'; import Plus from '@lucide/svelte/icons/plus'; import Settings from '@lucide/svelte/icons/settings'; + import Wrench from '@lucide/svelte/icons/wrench'; interface Props { gravatar: Gravatar; @@ -196,6 +201,32 @@ + + + + System + + + + + Overview + + + + Elasticsearch + + + + Actions + + + + Migrations + + + {#if isImpersonating} import type { ComponentProps, Snippet } from 'svelte'; + import { resolve } from '$app/paths'; import { page } from '$app/state'; import * as Collapsible from '$comp/ui/collapsible'; import * as Sidebar from '$comp/ui/sidebar'; import { useSidebar } from '$comp/ui/sidebar'; import ChevronRight from '@lucide/svelte/icons/chevron-right'; import Settings from '@lucide/svelte/icons/settings-2'; + import Wrench from '@lucide/svelte/icons/wrench'; - import type { NavigationItem, NavigationItemContext } from '../../../routes.svelte'; + import type { NavigationItem } from '../../../routes.svelte'; type Props = ComponentProps & { footer?: Snippet; header?: Snippet; - impersonating?: boolean; routes: NavigationItem[]; }; - let { footer, header, impersonating = false, routes, ...props }: Props = $props(); + let { footer, header, routes, ...props }: Props = $props(); const dashboardRoutes = $derived(routes.filter((route) => route.group === 'Dashboards')); - // Settings routes need additional filtering based on navigation context - const navigationContext: NavigationItemContext = $derived({ authenticated: true, impersonating }); - const settingsRoutes = $derived( - routes.filter((route) => route.group === 'Settings').filter((route) => (route.show ? route.show(navigationContext) : true)) - ); + const settingsRoutes = $derived(routes.filter((route) => route.group === 'Settings')); const settingsIsActive = $derived(settingsRoutes.some((route) => route.href === page.url.pathname)); + const systemRoutes = $derived(routes.filter((route) => route.group === 'System')); + const systemBasePath = resolve('/(app)/system'); + const systemIsActive = $derived(page.url.pathname === systemBasePath || page.url.pathname.startsWith(systemBasePath + '/')); + const sidebar = useSidebar(); function onMenuClick() { @@ -82,6 +83,10 @@ {#snippet child({ props })} + {#if subItem.icon} + {@const Icon = subItem.icon} + + {/if} {subItem.title} {/snippet} @@ -95,6 +100,47 @@ + + {#if systemRoutes.length > 0} + + + + {#snippet child({ props })} + + + {#snippet child({ props })} + + + System + + + {/snippet} + + + + {#each systemRoutes as subItem (subItem.href)} + + + {#snippet child({ props })} + + {#if subItem.icon} + {@const Icon = subItem.icon} + + {/if} + {subItem.title} + + {/snippet} + + + {/each} + + + + {/snippet} + + + + {/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte index f69ca9ab0a..eb7e0da037 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte @@ -222,7 +222,7 @@ {#if isAuthenticated} - + {#snippet header()} {/snippet} -
-
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte index 403c2a72d6..b4351ba2d2 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte @@ -282,7 +282,7 @@ {#snippet footerChildren()} -
+
{#if table.getSelectedRowModel().flatRows.length} {/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte index c80f9439dd..2f66c284ee 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/issues/+page.svelte @@ -311,7 +311,7 @@ {#snippet footerChildren()} -
+
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts b/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts index bf9c758859..30bb166562 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts @@ -13,6 +13,7 @@ import { routes as accountRoutes } from './account/routes.svelte'; import { routes as eventRoutes } from './event/routes.svelte'; import { routes as organizationRoutes } from './organization/routes.svelte'; import { routes as projectRoutes } from './project/routes.svelte'; +import { routes as systemRoutes } from './system/routes.svelte'; export function routes(): NavigationItem[] { const items = [ @@ -65,7 +66,8 @@ export function routes(): NavigationItem[] { ...accountRoutes(), ...eventRoutes(), ...organizationRoutes(), - ...projectRoutes() + ...projectRoutes(), + ...systemRoutes() ]; return items; diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/+layout.svelte new file mode 100644 index 0000000000..6e7616d3ff --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/+layout.svelte @@ -0,0 +1,42 @@ + + + +

System Administration

+ Manage Exceptionless system maintenance and operations. + + + + + + + {@render children()} + + + {#snippet disabled()} +
+ Access Denied + You must be a global administrator to access this page. +
+ {/snippet} +
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/+page.svelte new file mode 100644 index 0000000000..24225dac27 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/+page.svelte @@ -0,0 +1,9 @@ + diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/actions/[[category]]/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/actions/[[category]]/+page.svelte new file mode 100644 index 0000000000..15d2e9ad5f --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/actions/[[category]]/+page.svelte @@ -0,0 +1,163 @@ + + +
+
+

Actions

+ Run maintenance jobs and system operations. +
+ + + +
+
+ {#each categories as category (category)} + + {/each} +
+ +
+ + +
+
+ Action + + {filteredActions.length} + {filteredActions.length === 1 ? 'action' : 'actions'} + {#if destructiveCount > 0} + · {destructiveCount} destructive + {/if} + +
+ {#each filteredActions as action (action.name)} +
+
+
+ {action.label} + {#if activeCategory === 'All'} + + {action.category.toUpperCase()} + + {/if} + {#if action.dangerous} + + + DESTRUCTIVE + + {/if} +
+

{action.description}

+
+ +
+ {/each} + {#if filteredActions.length === 0} +
No actions found.
+ {/if} +
+
+ +{#if selectedAction && openDialog} + +{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/+layout.svelte new file mode 100644 index 0000000000..73c5fac8a7 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/+layout.svelte @@ -0,0 +1,45 @@ + + +
+
+ +
+

Elasticsearch

+ Cluster health, storage metrics, indices, and backup snapshots. +
+
+ + + + + {@render children()} +
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/+page.svelte new file mode 100644 index 0000000000..37c8e8e4ff --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/+page.svelte @@ -0,0 +1,9 @@ + diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/backups/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/backups/+page.svelte new file mode 100644 index 0000000000..348e46f1b2 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/backups/+page.svelte @@ -0,0 +1,58 @@ + + +{#if snapshotsQuery.isPending} + + + Snapshot Backups + Loading snapshot repositories... + + + {#each [1, 2, 3, 4, 5] as i (i)} + + {/each} + + +{:else if snapshotsQuery.isError} + + +

Failed to load snapshot information. Please try again.

+
+
+{:else if !snapshotsData || snapshotsData.repositories.length === 0} + + + +

No snapshot repositories configured.

+

+ Snapshot repositories are typically named <scope>-hourly (e.g. + prod-hourly). +

+
+
+{:else} +

+ {snapshotsData.snapshots.length} snapshots across {snapshotsData.repositories.length} + {snapshotsData.repositories.length === 1 ? 'repository' : 'repositories'}: {snapshotsData.repositories.join(', ')} +

+ +{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/indices/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/indices/+page.svelte new file mode 100644 index 0000000000..db1c4fafc3 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/indices/+page.svelte @@ -0,0 +1,66 @@ + + +{#if esQuery.isPending} + + + + + +{:else if esQuery.isError} + + +

Failed to load Elasticsearch info. Please try again.

+
+
+{:else if data} + + {#snippet toolbarChildren()} + +
+ + +
+ + {/snippet} +
+{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/overview/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/overview/+page.svelte new file mode 100644 index 0000000000..7983b5ef8c --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/elasticsearch/overview/+page.svelte @@ -0,0 +1,130 @@ + + +{#if esQuery.isPending} +
+ {#each [1, 2, 3, 4] as i (i)} + + + + + + + {/each} +
+{:else if esQuery.isError} + + +

Failed to load Elasticsearch info. Please try again.

+
+
+{:else if data} +
+
+ + + Cluster Status + {#if data.health.status === 0} + + {:else} + + {/if} + + +
+ + {healthLabel(data.health.status)} + +
+

{data.health.cluster_name}

+
+
+ + + + Nodes + + + +
{data.health.number_of_nodes}
+

+ {data.health.number_of_data_nodes} data node{data.health.number_of_data_nodes !== 1 ? 's' : ''} +

+
+
+ + + + Indices + + + +
+

documents

+
+
+ + + + Storage + + + +
+

Total index size

+
+
+
+ +
+
+

Shard Details

+

Allocation and status of Elasticsearch shards across the cluster

+
+ +
+
+{/if} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/migrations/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/migrations/+page.svelte new file mode 100644 index 0000000000..04dfbac56f --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/migrations/+page.svelte @@ -0,0 +1,146 @@ + + +
+
+

Migrations

+ Database migration history — versioned schema changes applied to Elasticsearch. +
+ + + {#if migrationsQuery.isError} + + +

Failed to load migration history. Please try again.

+
+
+ {:else} + {#if migrationsQuery.isPending} +
+ {#each [1, 2, 3, 4] as i (i)} + + + + + + + {/each} +
+ {:else if data} +
+ + + Current Version + + + +
{data.current_version >= 0 ? data.current_version : '—'}
+ Highest completed version +
+
+ + + Failed + + + +
0}>{failedCount}
+ {failedCount > 0 ? 'Requires attention' : 'No failures'} +
+
+ + + Running + + + +
0}>{runningCount}
+ In progress +
+
+ + + Total + + + +
{allStates.length}
+ State records in ES +
+
+
+ {/if} + + + {#snippet toolbarChildren()} + + + + {/snippet} + + {/if} +
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/overview/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/overview/+page.svelte new file mode 100644 index 0000000000..9b4847a24b --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/overview/+page.svelte @@ -0,0 +1,281 @@ + + +
+
+

Overview

+ System-wide statistics and usage trends. +
+ + + {#if statsQuery.isError} + + +

Failed to load system statistics. Please try again.

+
+
+ {:else} +
+ {#each statCards as card (card.label)} + {@const Icon = card.icon} + + + {card.label} + + + + {#if statsQuery.isPending} +
+ {:else} +
+ {#if card.label === 'Total Stacks' && stackStatusBreakdown.length > 0} +

+ {#each stackStatusBreakdown as bucket, index (bucket.status)} + {bucket.status}{index < stackStatusBreakdown.length - 1 ? ', ' : ''} + {/each} +

+ {:else if card.sub} +

{card.sub}

+ {/if} + {/if} +
+
+ {/each} +
+ +
+ + + Events All-Time + Total event volume by month across the full history + + + {#if statsQuery.isPending} + + {:else if eventsAllTimeChartData.length === 0} +

No event history available.

+ {:else} + + d.count))]} + series={eventsAllTimeChartSeries} + props={{ area: { curve: curveLinear } }} + > + {#snippet tooltip()} + formatMonthLabel(v)} /> + {/snippet} + + + {/if} +
+
+ + + + Organization Growth + New organizations created over time + + + {#if statsQuery.isPending} + + {:else if organizationGrowthChartData.length === 0} +

No growth data available.

+ {:else} + + d.count))]} + series={organizationGrowthChartSeries} + props={{ area: { curve: curveLinear } }} + > + {#snippet tooltip()} + formatMonthLabel(v)} /> + {/snippet} + + + {/if} +
+
+
+ + {#if stackTypeStatusBuckets.length > 0 || statsQuery.isPending} + + + Status by Event Type + Breakdown of stack statuses across each event type + + + {#if statsQuery.isPending} + + {:else} + + + {#snippet tooltip()} + + {/snippet} + + + {/if} + + + {/if} + {/if} +
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/routes.svelte.ts b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/routes.svelte.ts new file mode 100644 index 0000000000..e41cff7fd6 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/system/routes.svelte.ts @@ -0,0 +1,40 @@ +import { resolve } from '$app/paths'; +import Database from '@lucide/svelte/icons/database'; +import DatabaseZap from '@lucide/svelte/icons/database-zap'; +import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard'; +import Play from '@lucide/svelte/icons/play'; + +import type { NavigationItem } from '../../routes.svelte'; + +export function routes(): NavigationItem[] { + return [ + { + group: 'System', + href: resolve('/(app)/system/overview'), + icon: LayoutDashboard, + show: (context) => context.user?.roles?.includes('global') ?? false, + title: 'Overview' + }, + { + group: 'System', + href: resolve('/(app)/system/elasticsearch/overview'), + icon: Database, + show: (context) => context.user?.roles?.includes('global') ?? false, + title: 'Elasticsearch' + }, + { + group: 'System', + href: resolve('/(app)/system/actions'), + icon: Play, + show: (context) => context.user?.roles?.includes('global') ?? false, + title: 'Actions' + }, + { + group: 'System', + href: resolve('/(app)/system/migrations'), + icon: DatabaseZap, + show: (context) => context.user?.roles?.includes('global') ?? false, + title: 'Migrations' + } + ]; +} diff --git a/src/Exceptionless.Web/Controllers/AdminController.cs b/src/Exceptionless.Web/Controllers/AdminController.cs index 5e54941854..0549a57def 100644 --- a/src/Exceptionless.Web/Controllers/AdminController.cs +++ b/src/Exceptionless.Web/Controllers/AdminController.cs @@ -11,10 +11,12 @@ using Exceptionless.Core.Utility; using Exceptionless.DateTimeExtensions; using Exceptionless.Web.Extensions; +using Exceptionless.Web.Models.Admin; using Foundatio.Jobs; using Foundatio.Messaging; using Foundatio.Queues; using Foundatio.Repositories; +using Foundatio.Repositories.Migrations; using Foundatio.Storage; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -26,37 +28,55 @@ namespace Exceptionless.Web.Controllers; [ApiExplorerSettings(IgnoreApi = true)] public class AdminController : ExceptionlessApiController { + private readonly ILogger _logger; private readonly ExceptionlessElasticConfiguration _configuration; private readonly IFileStorage _fileStorage; private readonly IMessagePublisher _messagePublisher; private readonly IOrganizationRepository _organizationRepository; + private readonly IProjectRepository _projectRepository; + private readonly IStackRepository _stackRepository; + private readonly IEventRepository _eventRepository; + private readonly IUserRepository _userRepository; private readonly IQueue _eventPostQueue; private readonly IQueue _workItemQueue; private readonly AppOptions _appOptions; private readonly BillingManager _billingManager; private readonly BillingPlans _plans; + private readonly IMigrationStateRepository _migrationStateRepository; public AdminController( ExceptionlessElasticConfiguration configuration, IFileStorage fileStorage, IMessagePublisher messagePublisher, IOrganizationRepository organizationRepository, + IProjectRepository projectRepository, + IStackRepository stackRepository, + IEventRepository eventRepository, + IUserRepository userRepository, IQueue eventPostQueue, IQueue workItemQueue, AppOptions appOptions, BillingManager billingManager, BillingPlans plans, - TimeProvider timeProvider) : base(timeProvider) + IMigrationStateRepository migrationStateRepository, + TimeProvider timeProvider, + ILoggerFactory loggerFactory) : base(timeProvider) { + _logger = loggerFactory.CreateLogger(); _configuration = configuration; _fileStorage = fileStorage; _messagePublisher = messagePublisher; _organizationRepository = organizationRepository; + _projectRepository = projectRepository; + _stackRepository = stackRepository; + _eventRepository = eventRepository; + _userRepository = userRepository; _eventPostQueue = eventPostQueue; _workItemQueue = workItemQueue; _appOptions = appOptions; _billingManager = billingManager; _plans = plans; + _migrationStateRepository = migrationStateRepository; } [HttpGet("settings")] @@ -65,6 +85,60 @@ public ActionResult SettingsRequest() return Ok(_appOptions); } + [HttpGet("stats")] + public async Task> GetStatsAsync() + { + var organizationCountTask = _organizationRepository.CountAsync(q => q + .AggregationsExpression("terms:billing_status date:created_utc~1M")); + + var userCountTask = _userRepository.CountAsync(); + var projectCountTask = _projectRepository.CountAsync(); + + var stackCountTask = _stackRepository.CountAsync(q => q + .AggregationsExpression("terms:status terms:(type terms:status)")); + + var eventCountTask = _eventRepository.CountAsync(q => q + .AggregationsExpression("date:date~1M")); + + await Task.WhenAll(organizationCountTask, userCountTask, projectCountTask, stackCountTask, eventCountTask); + + return Ok(new AdminStatsResponse( + Organizations: await organizationCountTask, + Users: await userCountTask, + Projects: await projectCountTask, + Stacks: await stackCountTask, + Events: await eventCountTask + )); + } + + [HttpGet("migrations")] + public async Task> GetMigrationsAsync() + { + var result = await _migrationStateRepository.GetAllAsync(o => o.SearchAfterPaging().PageLimit(1000)); + var migrationStates = new List(result.Documents.Count); + + while (result.Documents.Count > 0) + { + migrationStates.AddRange(result.Documents); + + if (!await result.NextPageAsync()) + break; + } + + var states = migrationStates + .OrderByDescending(s => s.Version) + .ThenByDescending(s => s.StartedUtc) + .ToArray(); + + int currentVersion = states + .Where(s => s.MigrationType != MigrationType.Repeatable && s.CompletedUtc.HasValue) + .Select(s => s.Version) + .DefaultIfEmpty(-1) + .Max(); + + return Ok(new MigrationsResponse(currentVersion, states)); + } + [HttpGet("echo")] public ActionResult EchoRequest() { @@ -164,8 +238,7 @@ public async Task RunJobAsync(string name, DateTime? utcStart = n switch (name.ToLowerInvariant()) { case "fix-stack-stats": - var defaultUtcStart = new DateTime(2026, 2, 10, 0, 0, 0, DateTimeKind.Utc); - var effectiveUtcStart = utcStart ?? defaultUtcStart; + var effectiveUtcStart = utcStart ?? _timeProvider.GetUtcNow().UtcDateTime.AddDays(-90); if (utcEnd.HasValue && utcEnd.Value.IsBefore(effectiveUtcStart)) { @@ -217,4 +290,149 @@ await _workItemQueue.EnqueueAsync(new UpdateProjectNotificationSettingsWorkItem return Ok(); } + + [HttpGet("elasticsearch")] + public async Task> GetElasticsearchInfoAsync() + { + var client = _configuration.Client; + var healthTask = client.Cluster.HealthAsync(); + var statsTask = client.Cluster.StatsAsync(); + var catIndicesTask = client.Cat.IndicesAsync(r => r.Bytes(Elasticsearch.Net.Bytes.B)); + var catShardsTask = client.Cat.ShardsAsync(); + await Task.WhenAll(healthTask, statsTask, catIndicesTask, catShardsTask); + + var healthResponse = await healthTask; + var statsResponse = await statsTask; + var catIndicesResponse = await catIndicesTask; + var catShardsResponse = await catShardsTask; + + if (!healthResponse.IsValid || !statsResponse.IsValid || !catIndicesResponse.IsValid || !catShardsResponse.IsValid) + return Problem(title: "Elasticsearch cluster information is unavailable."); + + // Count unassigned shards per index + var unassignedByIndex = (catShardsResponse.Records ?? []) + .Where(s => string.Equals(s.State, "UNASSIGNED", StringComparison.OrdinalIgnoreCase)) + .GroupBy(s => s.Index ?? String.Empty, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); + + var indexDetails = (catIndicesResponse.Records ?? []) + .OrderByDescending(i => long.TryParse(i.StoreSize, out var s) ? s : 0) + .Select(i => new ElasticsearchIndexDetailResponse( + Index: i.Index, + Health: i.Health, + Status: i.Status, + Primary: int.TryParse(i.Primary, out var p) ? p : 0, + Replica: int.TryParse(i.Replica, out var r) ? r : 0, + DocsCount: long.TryParse(i.DocsCount, out var dc) ? dc : 0, + StoreSizeInBytes: long.TryParse(i.StoreSize, out var ss) ? ss : 0, + UnassignedShards: unassignedByIndex.GetValueOrDefault(i.Index ?? String.Empty, 0) + )) + .ToArray(); + + return Ok(new ElasticsearchInfoResponse( + Health: new ElasticsearchHealthResponse( + Status: (int)healthResponse.Status, + ClusterName: healthResponse.ClusterName, + NumberOfNodes: healthResponse.NumberOfNodes, + NumberOfDataNodes: healthResponse.NumberOfDataNodes, + ActiveShards: healthResponse.ActiveShards, + RelocatingShards: healthResponse.RelocatingShards, + UnassignedShards: healthResponse.UnassignedShards, + ActivePrimaryShards: healthResponse.ActivePrimaryShards + ), + Indices: new ElasticsearchIndicesResponse( + Count: statsResponse.Indices.Count, + DocsCount: statsResponse.Indices.Documents.Count, + StoreSizeInBytes: statsResponse.Indices.Store.SizeInBytes + ), + IndexDetails: indexDetails + )); + } + + [HttpGet("elasticsearch/snapshots")] + public async Task> GetElasticsearchSnapshotsAsync() + { + var client = _configuration.Client; + try + { + var repositoryResponse = await client.Cat.RepositoriesAsync(); + if (!repositoryResponse.IsValid) + return Problem(title: "Snapshot repository information is unavailable."); + + if (!(repositoryResponse.Records?.Any() ?? false)) + return Ok(new ElasticsearchSnapshotsResponse([], [])); + + var repositoryNames = repositoryResponse.Records + .Where(r => !String.IsNullOrEmpty(r.Id)) + .Select(r => r.Id!) + .ToArray(); + + var snapshotTasks = repositoryNames + .Select(async repositoryName => + { + var snapshotResponse = await client.Cat.SnapshotsAsync(r => r.RepositoryName(repositoryName)); + if (!snapshotResponse.IsValid) + return ( + RepositoryName: repositoryName, + Snapshots: Array.Empty(), + Error: $"Unable to retrieve snapshots for repository: {repositoryName}." + ); + + var snapshotRecords = snapshotResponse.Records?.ToArray() ?? []; + return ( + RepositoryName: repositoryName, + Snapshots: snapshotRecords.Select(s => new ElasticsearchSnapshotResponse( + Repository: repositoryName, + Name: s.Id ?? String.Empty, + Status: s.Status ?? String.Empty, + StartTime: s.StartEpoch > 0 ? DateTimeOffset.FromUnixTimeSeconds(s.StartEpoch).UtcDateTime : null, + EndTime: s.EndEpoch > 0 ? DateTimeOffset.FromUnixTimeSeconds(s.EndEpoch).UtcDateTime : null, + Duration: s.Duration?.ToString() ?? String.Empty, + IndicesCount: s.Indices, + SuccessfulShards: s.SuccessfulShards, + FailedShards: s.FailedShards, + TotalShards: s.TotalShards + )).ToArray(), + Error: (string?)null + ); + }) + .ToArray(); + + var snapshotResults = await Task.WhenAll(snapshotTasks); + + var failedSnapshotResults = snapshotResults + .Where(r => r.Error is not null) + .ToArray(); + + if (failedSnapshotResults.Length is > 0) + { + _logger.LogWarning("Unable to retrieve snapshots for one or more repositories: {Repositories}", + String.Join(", ", failedSnapshotResults.Select(r => r.RepositoryName))); + } + + var successfulSnapshotResults = snapshotResults + .Where(r => r.Error is null) + .ToArray(); + + if (successfulSnapshotResults.Length is 0) + return Problem(title: "Unable to retrieve snapshot information."); + + var snapshots = successfulSnapshotResults + .SelectMany(r => r.Snapshots) + .OrderByDescending(s => s.StartTime) + .ToArray(); + + var successfulRepositoryNames = successfulSnapshotResults + .Select(r => r.RepositoryName) + .ToArray(); + + return Ok(new ElasticsearchSnapshotsResponse(successfulRepositoryNames, snapshots)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unable to retrieve snapshot information"); + return Problem(title: "Unable to retrieve snapshot information."); + } + } } + diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs index 79be92327c..905451ad6c 100644 --- a/src/Exceptionless.Web/Controllers/OrganizationController.cs +++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs @@ -1,4 +1,4 @@ -using Exceptionless.Core; +using Exceptionless.Core; using Exceptionless.Core.Authorization; using Exceptionless.Core.Billing; using Exceptionless.Core.Extensions; @@ -590,7 +590,8 @@ public async Task RemoveUserAsync(string id, string email) if (!user.OrganizationIds.Contains(organization.Id)) return BadRequest(); - if ((await _userRepository.GetByOrganizationIdAsync(organization.Id)).Total == 1) + var organizationUsers = await _userRepository.GetByOrganizationIdAsync(organization.Id); + if (organizationUsers.Total is 1) return BadRequest("An organization must contain at least one user."); await _organizationService.CleanupProjectNotificationSettingsAsync(organization, [user.Id]); @@ -768,7 +769,8 @@ protected override async Task CanDeleteAsync(Organization valu if (!String.IsNullOrEmpty(value.StripeCustomerId) && !User.IsInRole(AuthorizationRoles.GlobalAdmin)) return PermissionResult.DenyWithMessage("An organization cannot be deleted if it has a subscription.", value.Id); - var projects = (await _projectRepository.GetByOrganizationIdAsync(value.Id)).Documents.ToList(); + var organizationProjects = await _projectRepository.GetByOrganizationIdAsync(value.Id); + var projects = organizationProjects.Documents.ToList(); if (!User.IsInRole(AuthorizationRoles.GlobalAdmin) && projects.Count > 0) return PermissionResult.DenyWithMessage("An organization cannot be deleted if it contains any projects.", value.Id); diff --git a/src/Exceptionless.Web/Controllers/ProjectController.cs b/src/Exceptionless.Web/Controllers/ProjectController.cs index 42d3d207c2..04700c5e43 100644 --- a/src/Exceptionless.Web/Controllers/ProjectController.cs +++ b/src/Exceptionless.Web/Controllers/ProjectController.cs @@ -404,7 +404,7 @@ public async Task> GetIntegrationNotification [HttpPost("~/" + API_PREFIX + "/users/{userId:objectid}/projects/{id:objectid}/notifications")] [Consumes("application/json")] [Authorize(Policy = AuthorizationRoles.UserPolicy)] - public async Task SetNotificationSettingsAsync(string id, string userId, NotificationSettings settings) + public async Task SetNotificationSettingsAsync(string id, string userId, NotificationSettings? settings) { var project = await GetModelAsync(id, false); if (project is null) diff --git a/src/Exceptionless.Web/Controllers/TokenController.cs b/src/Exceptionless.Web/Controllers/TokenController.cs index 0aaf0b4058..d5bc931d0d 100644 --- a/src/Exceptionless.Web/Controllers/TokenController.cs +++ b/src/Exceptionless.Web/Controllers/TokenController.cs @@ -100,7 +100,8 @@ public async Task> GetDefaultTokenAsync(string projectId if (project is null) return NotFound(); - var token = (await _repository.GetByTypeAndProjectIdAsync(TokenType.Access, projectId, o => o.PageLimit(1))).Documents.FirstOrDefault(); + var defaultTokenResults = await _repository.GetByTypeAndProjectIdAsync(TokenType.Access, projectId, o => o.PageLimit(1)); + var token = defaultTokenResults.Documents.FirstOrDefault(); if (token is not null) return await OkModelAsync(token); diff --git a/src/Exceptionless.Web/Models/Admin/AdminStatsResponse.cs b/src/Exceptionless.Web/Models/Admin/AdminStatsResponse.cs new file mode 100644 index 0000000000..4cf42cc1f4 --- /dev/null +++ b/src/Exceptionless.Web/Models/Admin/AdminStatsResponse.cs @@ -0,0 +1,11 @@ +using Foundatio.Repositories.Models; + +namespace Exceptionless.Web.Models.Admin; + +public record AdminStatsResponse( + CountResult Organizations, + CountResult Users, + CountResult Projects, + CountResult Stacks, + CountResult Events +); diff --git a/src/Exceptionless.Web/Models/Admin/ElasticsearchResponse.cs b/src/Exceptionless.Web/Models/Admin/ElasticsearchResponse.cs new file mode 100644 index 0000000000..b8b7854642 --- /dev/null +++ b/src/Exceptionless.Web/Models/Admin/ElasticsearchResponse.cs @@ -0,0 +1,53 @@ +namespace Exceptionless.Web.Models.Admin; + +public record ElasticsearchHealthResponse( + int Status, + string ClusterName, + int NumberOfNodes, + int NumberOfDataNodes, + int ActiveShards, + int RelocatingShards, + int UnassignedShards, + int ActivePrimaryShards +); + +public record ElasticsearchIndicesResponse( + long Count, + long DocsCount, + double StoreSizeInBytes +); + +public record ElasticsearchIndexDetailResponse( + string? Index, + string? Health, + string? Status, + int Primary, + int Replica, + long DocsCount, + long StoreSizeInBytes, + int UnassignedShards +); + +public record ElasticsearchInfoResponse( + ElasticsearchHealthResponse Health, + ElasticsearchIndicesResponse Indices, + ElasticsearchIndexDetailResponse[] IndexDetails +); + +public record ElasticsearchSnapshotResponse( + string Repository, + string Name, + string Status, + DateTime? StartTime, + DateTime? EndTime, + string Duration, + long IndicesCount, + long SuccessfulShards, + long FailedShards, + long TotalShards +); + +public record ElasticsearchSnapshotsResponse( + string[] Repositories, + ElasticsearchSnapshotResponse[] Snapshots +); diff --git a/src/Exceptionless.Web/Models/Admin/MigrationsResponse.cs b/src/Exceptionless.Web/Models/Admin/MigrationsResponse.cs new file mode 100644 index 0000000000..61be050d91 --- /dev/null +++ b/src/Exceptionless.Web/Models/Admin/MigrationsResponse.cs @@ -0,0 +1,6 @@ +namespace Exceptionless.Web.Models.Admin; + +public record MigrationsResponse( + int CurrentVersion, + Foundatio.Repositories.Migrations.MigrationState[] States +); diff --git a/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs index 3c75abef39..3b5a6cd65e 100644 --- a/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AdminControllerTests.cs @@ -3,9 +3,12 @@ using Exceptionless.Core.Utility; using Exceptionless.Tests.Extensions; using Exceptionless.Tests.Utility; +using Exceptionless.Web.Models.Admin; using Foundatio.Jobs; using Foundatio.Queues; using Foundatio.Repositories; +using Foundatio.Repositories.Migrations; +using Foundatio.Repositories.Models; using Foundatio.Repositories.Utility; using Xunit; @@ -72,7 +75,7 @@ public async Task RunJobAsync_WhenFixStackStatsWindowIsOmitted_ShouldUseDefaultS { // Arrange TimeProvider.SetUtcNow(new DateTime(2026, 2, 5, 12, 0, 0, DateTimeKind.Utc)); - var beforeWindow = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2026, 2, 5, 12, 0, 0, TimeSpan.Zero)); + var beforeWindow = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2025, 11, 1, 12, 0, 0, TimeSpan.Zero)); TimeProvider.SetUtcNow(new DateTime(2026, 2, 15, 12, 0, 0, DateTimeKind.Utc)); var inWindow = await CreateCorruptedStackWithEventAsync(new DateTimeOffset(2026, 2, 15, 12, 0, 0, TimeSpan.Zero)); @@ -327,4 +330,216 @@ await SendRequestAsync(r => r Assert.NotNull(project); Assert.DoesNotContain(orphanedUserId, project.NotificationSettings.Keys); } + + [Fact] + public async Task GetStats_AsGlobalAdmin_ReturnsAllFieldsPopulated() + { + // Act + var stats = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "stats") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(stats); + Assert.True(stats.Organizations.Total >= 0); + Assert.True(stats.Users.Total >= 0); + Assert.True(stats.Projects.Total >= 0); + Assert.True(stats.Stacks.Total >= 0); + Assert.True(stats.Events.Total >= 0); + + Assert.NotNull(stats.Organizations.Aggregations); + Assert.NotNull(stats.Stacks.Aggregations); + Assert.NotNull(stats.Events.Aggregations); + } + + [Fact] + public async Task GetStats_AsGlobalAdmin_BillingStatusBreakdownSumsToOrgCount() + { + // Act + var stats = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "stats") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(stats); + var billingTerms = stats.Organizations.Aggregations.Terms("terms_billing_status"); + Assert.NotNull(billingTerms); + var billingBuckets = billingTerms.Buckets; + Assert.NotNull(billingBuckets); + long billingTotal = billingBuckets.Sum(b => b.Total ?? 0); + Assert.Equal(stats.Organizations.Total, billingTotal); + } + + [Fact] + public async Task GetStats_AsGlobalAdmin_StacksByStatusSumsToStackCount() + { + // Act + var stats = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "stats") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(stats); + var statusTerms = stats.Stacks.Aggregations.Terms("terms_status"); + Assert.NotNull(statusTerms); + var statusBuckets = statusTerms.Buckets; + Assert.NotNull(statusBuckets); + long statusTotal = statusBuckets.Sum(b => b.Total ?? 0); + Assert.Equal(stats.Stacks.Total, statusTotal); + } + + [Fact] + public async Task GetStats_AsGlobalAdmin_StacksByTypeStatusHasValidStructure() + { + // Act + var stats = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "stats") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(stats); + var typeTerms = stats.Stacks.Aggregations.Terms("terms_type"); + Assert.NotNull(typeTerms); + var typeBuckets = typeTerms.Buckets; + Assert.NotNull(typeBuckets); + foreach (var typeBucket in typeBuckets) + { + Assert.NotNull(typeBucket.Key); + Assert.True(typeBucket.Total >= 0); + var nestedStatusTerms = typeBucket.Aggregations.Terms("terms_status"); + Assert.NotNull(nestedStatusTerms); + var nestedStatusBuckets = nestedStatusTerms.Buckets; + Assert.NotNull(nestedStatusBuckets); + long subTotal = nestedStatusBuckets.Sum(b => b.Total ?? 0); + Assert.Equal(typeBucket.Total, subTotal); + } + } + + [Fact] + public Task GetStats_WithoutAuth_ReturnsUnauthorized() + { + return SendRequestAsync(r => r + .AppendPaths("admin", "stats") + .StatusCodeShouldBeUnauthorized()); + } + + [Fact] + public Task RunJobAsync_AsAuthenticatedNonGlobalAdmin_ReturnsForbidden() + { + return SendRequestAsync(r => r + .AsTestOrganizationUser() + .AppendPaths("admin", "maintenance", "fix-stack-stats") + .StatusCodeShouldBeForbidden()); + } + + [Theory] + [InlineData("admin/stats")] + [InlineData("admin/migrations")] + [InlineData("admin/elasticsearch")] + [InlineData("admin/elasticsearch/snapshots")] + public Task AdminReadEndpoints_AsAuthenticatedNonGlobalAdmin_ReturnForbidden(string path) + { + return SendRequestAsync(r => r + .AsTestOrganizationUser() + .AppendPath(path) + .StatusCodeShouldBeForbidden()); + } + + [Fact] + public async Task GetMigrations_AsGlobalAdmin_ReturnsAllRegisteredMigrations() + { + // Act + var response = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "migrations") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.States); + + foreach (var state in response.States) + { + Assert.NotNull(state.Id); + Assert.True(Enum.IsDefined(state.MigrationType)); + } + } + + [Fact] + public Task GetMigrations_WithoutAuth_ReturnsUnauthorized() + { + return SendRequestAsync(r => r + .AppendPaths("admin", "migrations") + .StatusCodeShouldBeUnauthorized()); + } + + [Fact] + public async Task GetElasticsearch_AsGlobalAdmin_ReturnsClusterHealthAndIndices() + { + // Act + var elasticsearch = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "elasticsearch") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(elasticsearch); + Assert.NotNull(elasticsearch.Health); + Assert.NotNull(elasticsearch.Indices); + Assert.NotNull(elasticsearch.IndexDetails); + } + + [Fact] + public async Task GetElasticsearch_AsGlobalAdmin_IndexDetailsContainExpectedFields() + { + // Act + var elasticsearch = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "elasticsearch") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(elasticsearch); + Assert.All(elasticsearch.IndexDetails, indexDetail => + { + Assert.True(indexDetail.DocsCount >= 0); + Assert.True(indexDetail.StoreSizeInBytes >= 0); + Assert.True(indexDetail.UnassignedShards >= 0); + }); + } + + [Fact] + public Task GetElasticsearch_WithoutAuth_ReturnsUnauthorized() + { + return SendRequestAsync(r => r + .AppendPaths("admin", "elasticsearch") + .StatusCodeShouldBeUnauthorized()); + } + + [Fact] + public async Task GetElasticsearchSnapshots_AsGlobalAdmin_ReturnsTypedResponse() + { + // Act + var snapshots = await SendRequestAsAsync(r => r + .AsGlobalAdminUser() + .AppendPaths("admin", "elasticsearch", "snapshots") + .StatusCodeShouldBeOk()); + + // Assert + Assert.NotNull(snapshots); + Assert.NotNull(snapshots.Repositories); + Assert.NotNull(snapshots.Snapshots); + } + + [Fact] + public Task GetElasticsearchSnapshots_WithoutAuth_ReturnsUnauthorized() + { + return SendRequestAsync(r => r + .AppendPaths("admin", "elasticsearch", "snapshots") + .StatusCodeShouldBeUnauthorized()); + } } diff --git a/tests/Exceptionless.Tests/Controllers/Data/openapi.json b/tests/Exceptionless.Tests/Controllers/Data/openapi.json index 7bd902af81..5756ec39bf 100644 --- a/tests/Exceptionless.Tests/Controllers/Data/openapi.json +++ b/tests/Exceptionless.Tests/Controllers/Data/openapi.json @@ -5272,16 +5272,29 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] } }, "application/*+json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] } } - }, - "required": true + } }, "responses": { "200": { @@ -5327,16 +5340,29 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] } }, "application/*+json": { "schema": { - "$ref": "#/components/schemas/NotificationSettings" + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/NotificationSettings" + } + ] } } - }, - "required": true + } }, "responses": { "200": { diff --git a/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs b/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs index 2e8b23ce73..9514a29281 100644 --- a/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/OrganizationControllerTests.cs @@ -2,7 +2,6 @@ using Exceptionless.Core.Repositories; using Exceptionless.Core.Utility; using Exceptionless.Tests.Extensions; -using Exceptionless.Tests.Utility; using Exceptionless.Web.Models; using Foundatio.Repositories; using Foundatio.Repositories.Utility; diff --git a/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs b/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs index ee05bc4995..edd3d3ee27 100644 --- a/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs +++ b/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs @@ -3,7 +3,6 @@ using Exceptionless.Core.Models.WorkItems; using Exceptionless.Core.Repositories; using Exceptionless.Core.Utility; -using Exceptionless.Tests.Extensions; using Exceptionless.Tests.Utility; using Foundatio.Jobs; using Foundatio.Queues; diff --git a/tests/http/admin.http b/tests/http/admin.http index f02571be7c..bfc3249eca 100644 --- a/tests/http/admin.http +++ b/tests/http/admin.http @@ -85,7 +85,7 @@ Authorization: Bearer {{token}} GET {{apiUrl}}/admin/maintenance/update-project-default-bot-lists Authorization: Bearer {{token}} -### Fix Stack Stats (defaults: utcStart=2026-02-10T00:00:00Z, utcEnd=null=>now) +### Fix Stack Stats (defaults: utcStart=now-90d, utcEnd=null=>now) GET {{apiUrl}}/admin/maintenance/fix-stack-stats Authorization: Bearer {{token}} @@ -93,3 +93,19 @@ Authorization: Bearer {{token}} GET {{apiUrl}}/admin/maintenance/fix-stack-stats?utcStart=2026-02-10T00:00:00Z&utcEnd=2026-02-23T00:00:00Z Authorization: Bearer {{token}} +### Admin Stats +GET {{apiUrl}}/admin/stats +Authorization: Bearer {{token}} + +### Migrations +GET {{apiUrl}}/admin/migrations +Authorization: Bearer {{token}} + +### Elasticsearch Cluster Info +GET {{apiUrl}}/admin/elasticsearch +Authorization: Bearer {{token}} + +### Elasticsearch Snapshots +GET {{apiUrl}}/admin/elasticsearch/snapshots +Authorization: Bearer {{token}} + From 03467d1c78891369f431dcc6aab4080c2b64e4b9 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Sun, 15 Mar 2026 23:10:21 -0500 Subject: [PATCH 26/29] Update agent descriptions with usage guidelines and trigger phrases Enhance agent metadata in both the individual agent definitions and AGENTS.md to include specific "Use when" scenarios and keyword triggers. This helps improve agent selection and intent matching for tasks like feature implementation, PR reviews, and issue triage. --- .claude/agents/engineer.md | 2 +- .claude/agents/pr-reviewer.md | 2 +- .claude/agents/reviewer.md | 2 +- .claude/agents/triage.md | 2 +- AGENTS.md | 8 ++++---- .../admin/components/table/backups-options.svelte.ts | 2 +- .../admin/components/table/health-badge-cell.svelte | 4 ++-- .../src/lib/features/admin/elasticsearch-utils.ts | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.claude/agents/engineer.md b/.claude/agents/engineer.md index df3e6bb15b..4fac76fca2 100644 --- a/.claude/agents/engineer.md +++ b/.claude/agents/engineer.md @@ -1,7 +1,7 @@ --- name: engineer model: sonnet -description: "Fullstack development agent for ASP.NET Core 10 + SvelteKit. Plans before coding, writes idiomatic code, builds, tests, and hands off to @reviewer." +description: "Use when implementing features, fixing bugs, or making any code changes. Plans before coding, writes idiomatic ASP.NET Core 10 + SvelteKit code, builds, tests, and hands off to @reviewer. Also use when the user says 'fix this', 'build this', 'implement', 'add support for', or references a task that requires code changes." --- You are a distinguished fullstack engineer working on Exceptionless — a real-time error monitoring platform handling billions of requests. You write production-quality code that is readable, performant, and backwards-compatible. diff --git a/.claude/agents/pr-reviewer.md b/.claude/agents/pr-reviewer.md index b71a871046..7ae4d4668e 100644 --- a/.claude/agents/pr-reviewer.md +++ b/.claude/agents/pr-reviewer.md @@ -1,7 +1,7 @@ --- name: pr-reviewer model: sonnet -description: "PR lifecycle gate: zero-trust security pre-screen → dependency audit → build → delegate to @reviewer for code analysis → PR-level checks → verdict." +description: "Use when reviewing pull requests end-to-end before merge. Performs zero-trust security pre-screen, dependency audit, build verification, delegates to @reviewer for 4-pass code analysis, and delivers a final verdict. Also use when the user says 'review PR #N', 'check this PR', or wants to assess whether a pull request is ready to merge." --- You are the last gate before code reaches production for Exceptionless — a real-time error monitoring platform handling billions of requests. You own the full PR lifecycle: security pre-screening, build verification, code review delegation, and final verdict. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 1215a2de85..a5539d1b22 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -1,7 +1,7 @@ --- name: reviewer model: opus -description: "Adversarial 4-pass code analysis: security (before any execution), machine checks, correctness/performance, style/maintainability. Read-only — reports findings but never edits code." +description: "Use when reviewing code changes for quality, security, and correctness. Performs adversarial 4-pass analysis: security screening (before any code execution), machine checks, correctness/performance, and style/maintainability. Read-only — reports findings but never edits code. Also use when the user says 'review this', 'check my changes', or wants a second opinion on code quality." disallowedTools: - Edit - Write diff --git a/.claude/agents/triage.md b/.claude/agents/triage.md index 7cedb14888..8cf678fa9f 100644 --- a/.claude/agents/triage.md +++ b/.claude/agents/triage.md @@ -1,7 +1,7 @@ --- name: triage model: opus -description: "Analyze GitHub issues with depth: impact assessment, root cause analysis, reproduction, strategic context, and actionable implementation plans." +description: "Use when analyzing GitHub issues, investigating bug reports, answering codebase questions, or creating implementation plans. Performs impact assessment, root cause analysis, reproduction, and strategic context analysis. Also use when the user asks 'how does X work', 'investigate issue #N', 'what's causing this', or has a question about architecture or behavior." --- You are a senior issue analyst for Exceptionless — a real-time error monitoring platform handling billions of requests. You assess business impact, trace root causes, and produce plans that an engineer can ship immediately. diff --git a/AGENTS.md b/AGENTS.md index 3f61b52159..30d6e030e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,10 +57,10 @@ Load from `.agents/skills//SKILL.md` when working in that domain: Available in `.claude/agents/`. Use `@agent-name` to invoke: -- `engineer`: Fullstack development — RCA, TDD, implements, verify loop, ships end-to-end -- `reviewer`: Adversarial 4-pass code analysis — security (before any code execution), then build, correctness/performance, style. Read-only. -- `triage`: Issue analysis or question answering — impact assessment, RCA, reproduction, implementation plans -- `pr-reviewer`: PR lifecycle gate — zero-trust security pre-screen, dependency audit, delegates to @reviewer +- `engineer`: Use for implementing features, fixing bugs, or making code changes — plans, TDD, implements, verify loop, ships end-to-end +- `reviewer`: Use for reviewing code quality — adversarial 4-pass analysis (security → build → correctness → style). Read-only. +- `triage`: Use for analyzing issues, investigating bugs, or answering codebase questions — impact assessment, RCA, reproduction, implementation plans +- `pr-reviewer`: Use for end-to-end PR review — zero-trust security pre-screen, dependency audit, delegates to @reviewer, delivers verdict ### Orchestration Flow diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts index 988626f97b..35818298d5 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/backups-options.svelte.ts @@ -39,7 +39,7 @@ export function getColumns(): ColumnDef[] { }, { accessorKey: 'duration', - cell: (info) => info.getValue() || '—', + cell: (info) => info.getValue() ?? '—', enableSorting: false, header: 'Duration', meta: { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte index b11baae2a6..d5ca55b98e 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/components/table/health-badge-cell.svelte @@ -9,6 +9,6 @@ let { value }: Props = $props(); - - {healthLabel(value ?? '')} + + {healthLabel(value)} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts index f530306221..bad9071378 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/admin/elasticsearch-utils.ts @@ -69,7 +69,7 @@ export function snapshotBadgeClass(status: string | undefined): string { case 'SUCCESS': return 'text-muted-foreground border-muted-foreground/30'; default: - return ''; + return 'text-muted-foreground border-muted-foreground/30'; } } From d35310ceca444b8199050ef33ac33a165e59c839 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 07:56:54 -0500 Subject: [PATCH 27/29] Bump Scalar.AspNetCore from 2.13.8 to 2.13.9 (#2158) --- src/Exceptionless.Web/Exceptionless.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index d2fc17f4c2..68da397f09 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -20,7 +20,7 @@ - + From 6db0131292127e02dfaa9218e28ac6dadd825f66 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 16 Mar 2026 08:20:24 -0500 Subject: [PATCH 28/29] Update deps --- .../ClientApp/package-lock.json | 669 +++++++++--------- src/Exceptionless.Web/ClientApp/package.json | 34 +- 2 files changed, 344 insertions(+), 359 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/package-lock.json b/src/Exceptionless.Web/ClientApp/package-lock.json index cfd4a5edd0..80b2b975f7 100644 --- a/src/Exceptionless.Web/ClientApp/package-lock.json +++ b/src/Exceptionless.Web/ClientApp/package-lock.json @@ -12,7 +12,7 @@ "@exceptionless/fetchclient": "^0.44.0", "@internationalized/date": "^3.12.0", "@lucide/svelte": "^0.577.0", - "@tanstack/svelte-form": "^1.28.4", + "@tanstack/svelte-form": "^1.28.5", "@tanstack/svelte-query": "^6.1.0", "@tanstack/svelte-query-devtools": "^6.0.4", "@tanstack/svelte-table": "^9.0.0-alpha.10", @@ -25,7 +25,7 @@ "kit-query-params": "^0.0.26", "layerchart": "^2.0.0-next.46", "mode-watcher": "^1.1.0", - "oidc-client-ts": "^3.4.1", + "oidc-client-ts": "^3.5.0", "pretty-ms": "^9.3.0", "runed": "^0.37.1", "shiki": "^4.0.2", @@ -40,53 +40,46 @@ "devDependencies": { "@chromatic-com/storybook": "^5.0.1", "@eslint/compat": "^2.0.3", - "@eslint/js": "^9.39.2", - "@iconify-json/lucide": "^1.2.96", + "@eslint/js": "^9.39.4", + "@iconify-json/lucide": "^1.2.98", "@playwright/test": "^1.58.2", - "@storybook/addon-a11y": "^10.2.17", - "@storybook/addon-docs": "^10.2.17", + "@storybook/addon-a11y": "^10.2.19", + "@storybook/addon-docs": "^10.2.19", "@storybook/addon-svelte-csf": "^5.0.11", - "@storybook/sveltekit": "^10.2.17", + "@storybook/sveltekit": "^10.2.19", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.4", + "@sveltejs/kit": "^2.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.2.1", "@tanstack/eslint-plugin-query": "^5.91.4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/eslint": "^9.6.1", - "@types/node": "^25.4.0", + "@types/node": "^25.5.0", "@types/throttle-debounce": "^5.0.2", "cross-env": "^10.1.0", - "eslint": "^9.39.2", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-perfectionist": "^5.6.0", - "eslint-plugin-storybook": "^10.2.17", + "eslint-plugin-storybook": "^10.2.19", "eslint-plugin-svelte": "^3.15.2", - "jsdom": "^28.1.0", + "jsdom": "^29.0.0", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.5.1", "prettier-plugin-tailwindcss": "^0.7.2", - "storybook": "^10.2.17", - "svelte": "^5.53.10", + "storybook": "^10.2.19", + "svelte": "^5.53.12", "svelte-check": "^4.4.5", - "swagger-typescript-api": "^13.4.0", + "swagger-typescript-api": "^13.6.3", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "vite": "^7.3.1", - "vitest": "4.0.18", + "vitest": "4.1.0", "vitest-websocket-mock": "^0.5.0", "zod": "^4.3.6" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -186,31 +179,37 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", - "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.0.0", - "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.5" + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", + "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", + "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -320,9 +319,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", - "integrity": "sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -364,9 +363,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.1.tgz", - "integrity": "sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -380,8 +379,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.1", - "@csstools/css-calc": "^3.0.0" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { "node": ">=20.19.0" @@ -415,9 +414,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.27", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", - "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", "dev": true, "funding": [ { @@ -429,7 +428,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -966,15 +973,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1007,20 +1014,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1031,9 +1038,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -1090,9 +1097,9 @@ "license": "Apache-2.0" }, "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -1192,9 +1199,9 @@ } }, "node_modules/@iconify-json/lucide": { - "version": "1.2.96", - "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.96.tgz", - "integrity": "sha512-EICTusj67lvSmEaH/Lhe68ZyzcgfcPNpY00exAOkoo+z2fnLeNy31mdE3E/4/q8WjzZrICAZDxY3d6j7LzkgNA==", + "version": "1.2.98", + "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.98.tgz", + "integrity": "sha512-Lx2464W8Tty/QEnZ2UPb73nPdML/HpGCj0J0w37jP3/jx3l4fniZBjDxe1TgHiIL5XW9QO3vlx53ZQZ5JsNpzQ==", "dev": true, "license": "ISC", "dependencies": { @@ -1749,16 +1756,16 @@ "license": "MIT" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "devOptional": true, "license": "MIT" }, "node_modules/@storybook/addon-a11y": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.2.17.tgz", - "integrity": "sha512-J0ogEc4/XFC+Ytz+X1we6TOKreEk/shgUs/mtxdsLa0xJ6bp2n2OQPSjNtQHH/nK4SRBSfHWPm8ztfcXTzeG9w==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.2.19.tgz", + "integrity": "sha512-SJGf1ghCoRVlwyiRwz5GiHuNvu7C5iCDNIRJW8WGOJlnoQa3rYaY7WJ/8a/eT9N8buIscL9AYWudhh5zsI1W3g==", "dev": true, "license": "MIT", "dependencies": { @@ -1770,20 +1777,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.17" + "storybook": "^10.2.19" } }, "node_modules/@storybook/addon-docs": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.17.tgz", - "integrity": "sha512-c414xi7rxlaHn92qWOxtEkcOMm0/+cvBui0gUsgiWOZOM8dHChGZ/RjMuf1pPDyOrSsybLsPjZhP0WthsMDkdQ==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.2.19.tgz", + "integrity": "sha512-tXugthdzjX5AkGWDSP4pnRgA/CWlOaEKp/+y9JOGXHLQmm1GHjW+4brNvNkKbjBl06LALXwlcTOyU4lyVRDLAw==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.2.17", + "@storybook/csf-plugin": "10.2.19", "@storybook/icons": "^2.0.1", - "@storybook/react-dom-shim": "10.2.17", + "@storybook/react-dom-shim": "10.2.19", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -1793,7 +1800,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.17" + "storybook": "^10.2.19" } }, "node_modules/@storybook/addon-svelte-csf": { @@ -1820,13 +1827,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.17.tgz", - "integrity": "sha512-m/OBveTLm5ds/tUgHmmbKzgSi/oeCpQwm5rZa49vP2BpAd41Q7ER6TzkOoISzPoNNMAcbVmVc5vn7k6hdbPSHw==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.2.19.tgz", + "integrity": "sha512-a59xALzM9GeYh6p+wzAeBbDyIe+qyrC4nxS3QNzb5i2ZOhrq1iIpvnDaOWe80NC8mV3IlqUEGY8Uawkf//1Rmg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.2.17", + "@storybook/csf-plugin": "10.2.19", "ts-dedent": "^2.0.0" }, "funding": { @@ -1834,8 +1841,8 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.17", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "storybook": "^10.2.19", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@storybook/csf": { @@ -1849,9 +1856,9 @@ } }, "node_modules/@storybook/csf-plugin": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.17.tgz", - "integrity": "sha512-crHH8i/4mwzeXpWRPgwvwX2vjytW42zyzTRySUax5dTU8o9sjk4y+Z9hkGx3Nmu1TvqseS8v1Z20saZr/tQcWw==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.2.19.tgz", + "integrity": "sha512-BpjYIOdyQn/Rm6MjUAc5Gl8HlARZrskD/OhUNShiOh2fznb523dHjiE5mbU1kKM/+L1uvRlEqqih40rTx+xCrg==", "dev": true, "license": "MIT", "dependencies": { @@ -1864,7 +1871,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.2.17", + "storybook": "^10.2.19", "vite": "*", "webpack": "*" }, @@ -1902,9 +1909,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.17.tgz", - "integrity": "sha512-x9Kb7eUSZ1zGsEw/TtWrvs1LwWIdNp8qoOQCgPEjdB07reSJcE8R3+ASWHJThmd4eZf66ZALPJyerejake4Osw==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.2.19.tgz", + "integrity": "sha512-BXCEfBGVBRYBTYeBeH/PJsy0Bq5MERe/HiaylR+ah/XrvIr2Z9bkne1J8yYiXCjiyq5HQa7Bj11roz0+vyUaEw==", "dev": true, "license": "MIT", "funding": { @@ -1914,13 +1921,13 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.2.17" + "storybook": "^10.2.19" } }, "node_modules/@storybook/svelte": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/svelte/-/svelte-10.2.17.tgz", - "integrity": "sha512-QG4mb5F4XNwEnGqYTT8XnugKczOaqFUrxHj6+5rhVM/8q+zd6Ygkjine/277A7nIsJW/DogY/QJqTwnWheia7Q==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/svelte/-/svelte-10.2.19.tgz", + "integrity": "sha512-M/u/6mRskNjRqGRpzx8IPJvjRQNa0KNoca9GxOa8hTr4c6/mhPZuGyPDHgGvgt5OUX9xdP8vE31I9kv7maw7aA==", "dev": true, "license": "MIT", "dependencies": { @@ -1932,53 +1939,53 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.17", + "storybook": "^10.2.19", "svelte": "^5.0.0" } }, - "node_modules/@storybook/svelte-vite": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/svelte-vite/-/svelte-vite-10.2.17.tgz", - "integrity": "sha512-vJ7eGGW+qM9L1PMmGFzNHOvIFvXBf0rHXkYYsDAR9mzzMP+/k70o7swFzzDCP6xjjzQpVrrG0xgXw/YAzMxiSg==", + "node_modules/@storybook/sveltekit": { + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/sveltekit/-/sveltekit-10.2.19.tgz", + "integrity": "sha512-8D24gyV7jq+FO+wmRDIgb4vYw2FC7QIrRpSKR6E6b6XqX0QrgVVevLI1nmAZaa3Mex4rpPBI32NLwzjSKOdBiw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "10.2.17", - "@storybook/svelte": "10.2.17", - "magic-string": "^0.30.0", - "svelte2tsx": "^0.7.44", - "typescript": "^4.9.4 || ^5.0.0" + "@storybook/builder-vite": "10.2.19", + "@storybook/svelte": "10.2.19", + "@storybook/svelte-vite": "10.2.19" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", - "storybook": "^10.2.17", + "storybook": "^10.2.19", "svelte": "^5.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@storybook/sveltekit": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/@storybook/sveltekit/-/sveltekit-10.2.17.tgz", - "integrity": "sha512-vuJZOi9STv4vEfsRQDZj4UIUsqeHQrbUCCQqjYr1nF/ZMjG7+kRXbZSuXe7DMZkAeu3JqH/5Jj4hIrkILT20yg==", + "node_modules/@storybook/sveltekit/node_modules/@storybook/svelte-vite": { + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/@storybook/svelte-vite/-/svelte-vite-10.2.19.tgz", + "integrity": "sha512-nMSctY8YgRAmIjfU/7/Kp7Ujyx7UvJc+QD+pP/UKBG8Rjd4It88t6BWVzTjzPF+8byCxQxaWtyYugyrN1itLzw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "10.2.17", - "@storybook/svelte": "10.2.17", - "@storybook/svelte-vite": "10.2.17" + "@storybook/builder-vite": "10.2.19", + "@storybook/svelte": "10.2.19", + "magic-string": "^0.30.0", + "svelte2tsx": "^0.7.44", + "typescript": "^4.9.4 || ^5.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.2.17", + "@sveltejs/vite-plugin-svelte": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "storybook": "^10.2.19", "svelte": "^5.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@sveltejs/acorn-typescript": { @@ -2001,9 +2008,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.53.4", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", - "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", + "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2012,7 +2019,7 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", @@ -2375,10 +2382,13 @@ } }, "node_modules/@tanstack/devtools-event-client": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.2.tgz", - "integrity": "sha512-nerCPwV6RI4zQY+T5xxXEDOPgSF/gqf6dmCbDpTwkAvQJPHKgroHwKE5kvAcM3JC3ptdr5euwNV0//f8e+wmfQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.3.tgz", + "integrity": "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw==", "license": "MIT", + "bin": { + "intent": "bin/intent.js" + }, "engines": { "node": ">=18" }, @@ -2411,12 +2421,12 @@ } }, "node_modules/@tanstack/form-core": { - "version": "1.28.4", - "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.4.tgz", - "integrity": "sha512-2eox5ePrJ6kvA1DXD5QHk/GeGr3VFZ0uYR63UgQOe7bUg6h1JfXaIMqTjZK9sdGyE4oRNqFpoW54H0pZM7nObQ==", + "version": "1.28.5", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.28.5.tgz", + "integrity": "sha512-8lYnduHHfP6uaXF9+2OLnh3Fo27tH4TdtekWLG2b/Bp26ynbrWG6L4qhBgEb7VcvTpJw/RjvJF/JyFhZkG3pfQ==", "license": "MIT", "dependencies": { - "@tanstack/devtools-event-client": "^0.4.0", + "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" }, @@ -2469,12 +2479,12 @@ } }, "node_modules/@tanstack/svelte-form": { - "version": "1.28.4", - "resolved": "https://registry.npmjs.org/@tanstack/svelte-form/-/svelte-form-1.28.4.tgz", - "integrity": "sha512-/5J6MrIDQO+12RbvqTtQs+d2T3FuAhKWLQFUDSd3ikDvTG5GJ8gTcahXNijOJG0MajDGlaGPYxHzZA15ksXAKw==", + "version": "1.28.5", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-form/-/svelte-form-1.28.5.tgz", + "integrity": "sha512-m1nxd9U2/6rcBEShNt22/iyxrptScYL8kxsG0PLnNVZRWmcX+SbN0U3buZTgRzhLDgBanc8BdPHoObCDf280Zg==", "license": "MIT", "dependencies": { - "@tanstack/form-core": "1.28.4", + "@tanstack/form-core": "1.28.5", "@tanstack/svelte-store": "^0.10.1" }, "peerDependencies": { @@ -2786,9 +2796,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3138,13 +3148,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3153,7 +3163,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -3165,9 +3175,9 @@ } }, "node_modules/@vitest/mocker/node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -3188,13 +3198,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -3202,9 +3212,9 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3215,13 +3225,14 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -3229,9 +3240,9 @@ } }, "node_modules/@vitest/runner/node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3239,13 +3250,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3254,9 +3266,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3266,10 +3278,25 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/snapshot/node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@vitest/snapshot/node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3326,20 +3353,10 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3840,6 +3857,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -3884,14 +3908,14 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -3917,22 +3941,6 @@ "node": ">=4" } }, - "node_modules/cssstyle": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.0.1.tgz", - "integrity": "sha512-IoJs7La+oFp/AB033wBStxNOJt4+9hHMxsXUPANcoXL2b3W4DZKghlJ2cI/eyeRZIQ9ysvYEorVhjrcYctWbog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^4.1.2", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.5" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -4425,9 +4433,9 @@ } }, "node_modules/devalue": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", - "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", "license": "MIT" }, "node_modules/devlop": { @@ -4516,9 +4524,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -4606,25 +4614,25 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -4643,7 +4651,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4699,9 +4707,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.17.tgz", - "integrity": "sha512-LtzVBHcq+RbrhTnF1rFNpc5bmg/kmdDsw/6bIKOnyDY4r0g5ldZSNN3R/fxLrhFOL2DhmmDywN9lcFNqHCP3vQ==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.2.19.tgz", + "integrity": "sha512-JwwNgG24mkwwiJp/VIwUuJ9QIXoeCZteSZ7PEpb8DUhKpzCrNxJOeg7i5ep6yLexWAVjfFLG4OnFeV8cVS2PAg==", "dev": true, "license": "MIT", "dependencies": { @@ -4709,7 +4717,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^10.2.17" + "storybook": "^10.2.19" } }, "node_modules/eslint-plugin-svelte": { @@ -4907,9 +4915,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5201,20 +5209,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http2-client": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", @@ -5222,20 +5216,6 @@ "dev": true, "license": "MIT" }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5448,36 +5428,36 @@ } }, "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", + "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", "dev": true, "license": "MIT", "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.2", "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", + "tough-cookie": "^6.0.1", + "undici": "^7.24.3", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -5926,9 +5906,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5975,9 +5955,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -6108,9 +6088,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6474,9 +6454,9 @@ "license": "MIT" }, "node_modules/oidc-client-ts": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz", - "integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.5.0.tgz", + "integrity": "sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==", "license": "Apache-2.0", "dependencies": { "jwt-decode": "^4.0.0" @@ -7558,16 +7538,16 @@ } }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, "node_modules/storybook": { - "version": "10.2.17", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.17.tgz", - "integrity": "sha512-yueTpl5YJqLzQqs3CanxNdAAfFU23iP0j+JVJURE4ghfEtRmWfWoZWLGkVcyjmgum7UmjwAlqRuOjQDNvH89kw==", + "version": "10.2.19", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.2.19.tgz", + "integrity": "sha512-UUm5eGSm6BLhkcFP0WbxkmAHJZfVN2ViLpIZOqiIPS++q32VYn+CLFC0lrTYTDqYvaG7i4BK4uowXYujzE4NdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7720,9 +7700,9 @@ } }, "node_modules/svelte": { - "version": "5.53.10", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.10.tgz", - "integrity": "sha512-UcNfWzbrjvYXYSk+U2hME25kpb87oq6/WVLeBF4khyQrb3Ob/URVlN23khal+RbdCUTMfg4qWjI9KZjCNFtYMQ==", + "version": "5.53.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.12.tgz", + "integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -7734,7 +7714,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -7967,9 +7947,9 @@ "license": "ISC" }, "node_modules/swagger-typescript-api": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.4.0.tgz", - "integrity": "sha512-p2F4mb8VewzWg1Re8MXrNjANXaYkXY+6lAn4bmYxwan58fAhzNcjywhpKSd0B6bqyv5dAa3sfqSzT8ZvwH9fIQ==", + "version": "13.6.3", + "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.6.3.tgz", + "integrity": "sha512-LS0LNTr4kKyKZZFK079lzCWsBqP7hi3WiU9HagtiUvUGQNPrUvQuskZfBTdSynhFDapyjgthr8CJSI1w4fQC0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8218,22 +8198,22 @@ } }, "node_modules/tldts": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", - "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.19" + "tldts-core": "^7.0.26" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.19", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", - "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", "dev": true, "license": "MIT" }, @@ -8248,9 +8228,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8386,9 +8366,9 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "dev": true, "license": "MIT", "engines": { @@ -8661,31 +8641,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8701,12 +8681,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -8735,6 +8716,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -8753,17 +8737,17 @@ } }, "node_modules/vitest/node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, "funding": { @@ -8771,9 +8755,9 @@ } }, "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -8784,9 +8768,9 @@ } }, "node_modules/vitest/node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -8794,13 +8778,14 @@ } }, "node_modules/vitest/node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -8818,9 +8803,9 @@ } }, "node_modules/vitest/node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -8868,9 +8853,9 @@ } }, "node_modules/whatwg-url": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", - "integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Exceptionless.Web/ClientApp/package.json b/src/Exceptionless.Web/ClientApp/package.json index 20aad8ac4a..1d2d918657 100644 --- a/src/Exceptionless.Web/ClientApp/package.json +++ b/src/Exceptionless.Web/ClientApp/package.json @@ -27,42 +27,42 @@ "devDependencies": { "@chromatic-com/storybook": "^5.0.1", "@eslint/compat": "^2.0.3", - "@eslint/js": "^9.39.2", - "@iconify-json/lucide": "^1.2.96", + "@eslint/js": "^9.39.4", + "@iconify-json/lucide": "^1.2.98", "@playwright/test": "^1.58.2", - "@storybook/addon-a11y": "^10.2.17", - "@storybook/addon-docs": "^10.2.17", + "@storybook/addon-a11y": "^10.2.19", + "@storybook/addon-docs": "^10.2.19", "@storybook/addon-svelte-csf": "^5.0.11", - "@storybook/sveltekit": "^10.2.17", + "@storybook/sveltekit": "^10.2.19", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.53.4", + "@sveltejs/kit": "^2.55.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.2.1", "@tanstack/eslint-plugin-query": "^5.91.4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/svelte": "^5.3.1", "@types/eslint": "^9.6.1", - "@types/node": "^25.4.0", + "@types/node": "^25.5.0", "@types/throttle-debounce": "^5.0.2", "cross-env": "^10.1.0", - "eslint": "^9.39.2", + "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "eslint-plugin-perfectionist": "^5.6.0", - "eslint-plugin-storybook": "^10.2.17", + "eslint-plugin-storybook": "^10.2.19", "eslint-plugin-svelte": "^3.15.2", - "jsdom": "^28.1.0", + "jsdom": "^29.0.0", "prettier": "^3.8.1", "prettier-plugin-svelte": "^3.5.1", "prettier-plugin-tailwindcss": "^0.7.2", - "storybook": "^10.2.17", - "svelte": "^5.53.10", + "storybook": "^10.2.19", + "svelte": "^5.53.12", "svelte-check": "^4.4.5", - "swagger-typescript-api": "^13.4.0", + "swagger-typescript-api": "^13.6.3", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.57.0", "vite": "^7.3.1", - "vitest": "4.0.18", + "vitest": "4.1.0", "vitest-websocket-mock": "^0.5.0", "zod": "^4.3.6" }, @@ -71,7 +71,7 @@ "@exceptionless/fetchclient": "^0.44.0", "@internationalized/date": "^3.12.0", "@lucide/svelte": "^0.577.0", - "@tanstack/svelte-form": "^1.28.4", + "@tanstack/svelte-form": "^1.28.5", "@tanstack/svelte-query": "^6.1.0", "@tanstack/svelte-query-devtools": "^6.0.4", "@tanstack/svelte-table": "^9.0.0-alpha.10", @@ -84,7 +84,7 @@ "kit-query-params": "^0.0.26", "layerchart": "^2.0.0-next.46", "mode-watcher": "^1.1.0", - "oidc-client-ts": "^3.4.1", + "oidc-client-ts": "^3.5.0", "pretty-ms": "^9.3.0", "runed": "^0.37.1", "shiki": "^4.0.2", @@ -100,4 +100,4 @@ "overrides": { "storybook": "$storybook" } -} +} \ No newline at end of file From ee58abf2ea3c3417653b0a7dbd12c1bae8ce0037 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:15:38 +0000 Subject: [PATCH 29/29] Initial plan