diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/ConfigAuditDataSeeder.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/ConfigAuditDataSeeder.cs
new file mode 100644
index 00000000..c024f405
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests/Audit/ConfigAuditDataSeeder.cs
@@ -0,0 +1,170 @@
+using Microsoft.Data.SqlClient;
+using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
+
+namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;
+
+///
+/// Direct-SQL seeding helper for the Configuration Audit Log Playwright E2E tests
+/// (Wave 3). The /audit/configuration page reads the AuditLogEntries
+/// table (entity )
+/// via CentralUiRepository — a different table from the canonical
+/// AuditLog table that writes — so this seeder
+/// targets AuditLogEntries specifically.
+///
+///
+/// Mirrors 's connection handling and best-effort
+/// cleanup idiom: it opens a to
+/// (the running Docker
+/// cluster's ScadaBridgeConfig DB), inserts its own rows at setup time, and
+/// best-effort deletes them at teardown.
+///
+///
+///
+/// Every seeded row carries EntityType = marker (a unique per-test prefix
+/// derived from the test name + a GUID), so a UI filter on Entity Type isolates
+/// exactly this run's rows for deterministic pagination at 50/page, and the
+/// teardown DELETE never touches rows the cluster itself produced. Column
+/// names/types are verified against ScadaBridgeDbContextModelSnapshot:
+/// AuditLogEntries with Id (int identity), User/Action/
+/// EntityType/EntityId/EntityName (nvarchar, required),
+/// AfterStateJson (nvarchar(max), nullable), Timestamp
+/// (datetimeoffset), BundleImportId (uniqueidentifier, nullable).
+///
+///
+internal static class ConfigAuditDataSeeder
+{
+ ///
+ /// Connection string for the running cluster's configuration DB.
+ /// Delegates to .
+ ///
+ public static string ConnectionString => PlaywrightDbConnection.ConnectionString;
+
+ private const string InsertSql = @"
+INSERT INTO [AuditLogEntries]
+([User],[Action],[EntityType],[EntityId],[EntityName],[AfterStateJson],[Timestamp],[BundleImportId])
+VALUES (@user, @action, @entityType, @entityId, @entityName, @afterState, @ts, @bundleId);";
+
+ ///
+ /// Probe whether the configuration DB is reachable. Tests gate their per-test
+ /// setup on this; when the cluster is down the test fails with a clear
+ /// "MSSQL unavailable" message instead of an opaque SqlException. Mirrors
+ /// .
+ ///
+ public static async Task IsAvailableAsync()
+ {
+ try
+ {
+ await using var connection = new SqlConnection(ConnectionString);
+ await connection.OpenAsync();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Seeds rows into AuditLogEntries. Inserts
+ /// bulk rows (all EntityType = marker, BundleImportId = NULL) plus
+ /// bundle rows (also EntityType = marker,
+ /// but BundleImportId = bundleId). Row index 0 of the bulk set carries a
+ /// > 1024-char AfterStateJson to drive the large-state modal on the
+ /// page; the rest carry a tiny JSON blob. Timestamps step back one second per
+ /// row from "now" so rows are recent and ordered.
+ ///
+ /// Unique per-test prefix; isolates this run's rows.
+ /// Bundle import id stamped on the extra bundle rows.
+ /// Number of bulk rows (default 55 — spans two pages at 50/page).
+ /// Number of extra rows carrying the bundle import id (default 2).
+ public static async Task SeedAsync(string marker, Guid bundleId, int bulkCount = 55, int bundleRows = 2)
+ {
+ await using var connection = new SqlConnection(ConnectionString);
+ await connection.OpenAsync();
+
+ // Bulk rows: all EntityType = marker, BundleImportId NULL. Row 0 gets a
+ // large AfterStateJson (> 1024 chars) to drive the large-state modal.
+ for (var i = 0; i < bulkCount; i++)
+ {
+ var afterState = i == 0
+ ? "{\"blob\":\"" + new string('x', 1100) + "\"}"
+ : "{\"k\":\"v\"}";
+
+ await InsertRowAsync(
+ connection,
+ user: marker + "-user",
+ action: "Update",
+ entityType: marker,
+ entityId: marker + "-eid-" + i,
+ entityName: marker + "-" + i,
+ afterStateJson: afterState,
+ timestamp: DateTimeOffset.UtcNow.AddSeconds(-i),
+ bundleImportId: null);
+ }
+
+ // Bundle rows: EntityType = marker (so DeleteByMarkerAsync cleans them too)
+ // AND BundleImportId = bundleId, to drive the ?bundleImportId= chip drill-in.
+ for (var i = 0; i < bundleRows; i++)
+ {
+ await InsertRowAsync(
+ connection,
+ user: marker + "-user",
+ action: "Update",
+ entityType: marker,
+ entityId: marker + "-eid-bundle-" + i,
+ entityName: marker + "-bundle-" + i,
+ afterStateJson: "{\"k\":\"v\"}",
+ timestamp: DateTimeOffset.UtcNow.AddSeconds(-(bulkCount + i)),
+ bundleImportId: bundleId);
+ }
+ }
+
+ ///
+ /// Best-effort cleanup. Deletes every AuditLogEntries row whose
+ /// EntityType equals . Swallows all errors — the
+ /// marker carries a GUID so the rows are unique to this test run and tests
+ /// should not fail teardown. Mirrors
+ /// .
+ ///
+ public static async Task DeleteByMarkerAsync(string marker)
+ {
+ try
+ {
+ await using var connection = new SqlConnection(ConnectionString);
+ await connection.OpenAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = "DELETE FROM [AuditLogEntries] WHERE [EntityType] = @marker";
+ cmd.Parameters.AddWithValue("@marker", marker);
+ await cmd.ExecuteNonQueryAsync();
+ }
+ catch
+ {
+ // Best-effort — the marker carries a GUID so the rows are unique to
+ // this test run and won't collide on the next pass.
+ }
+ }
+
+ private static async Task InsertRowAsync(
+ SqlConnection connection,
+ string user,
+ string action,
+ string entityType,
+ string entityId,
+ string entityName,
+ string afterStateJson,
+ DateTimeOffset timestamp,
+ Guid? bundleImportId)
+ {
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = InsertSql;
+ cmd.Parameters.AddWithValue("@user", user);
+ cmd.Parameters.AddWithValue("@action", action);
+ cmd.Parameters.AddWithValue("@entityType", entityType);
+ cmd.Parameters.AddWithValue("@entityId", entityId);
+ cmd.Parameters.AddWithValue("@entityName", entityName);
+ cmd.Parameters.AddWithValue("@afterState", (object?)afterStateJson ?? DBNull.Value);
+ cmd.Parameters.AddWithValue("@ts", timestamp);
+ cmd.Parameters.AddWithValue("@bundleId", (object?)bundleImportId ?? DBNull.Value);
+ await cmd.ExecuteNonQueryAsync();
+ }
+}