texts fixes, search bar fixes, global menu fixes.
This commit is contained in:
@@ -189,6 +189,7 @@ builder.Services.AddHttpClient("AuthorityInternal", client =>
|
||||
|
||||
builder.Services.AddSingleton<PlatformMetadataService>();
|
||||
builder.Services.AddSingleton<PlatformContextService>();
|
||||
builder.Services.AddSingleton<IPlatformContextQuery>(sp => sp.GetRequiredService<PlatformContextService>());
|
||||
builder.Services.AddSingleton<TopologyReadModelService>();
|
||||
builder.Services.AddSingleton<ReleaseReadModelService>();
|
||||
builder.Services.AddSingleton<SecurityReadModelService>();
|
||||
|
||||
@@ -34,14 +34,14 @@ public sealed class IntegrationsReadModelService
|
||||
];
|
||||
|
||||
private readonly IReleaseControlBundleStore bundleStore;
|
||||
private readonly PlatformContextService contextService;
|
||||
private readonly IPlatformContextQuery contextQuery;
|
||||
|
||||
public IntegrationsReadModelService(
|
||||
IReleaseControlBundleStore bundleStore,
|
||||
PlatformContextService contextService)
|
||||
IPlatformContextQuery contextQuery)
|
||||
{
|
||||
this.bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
|
||||
this.contextService = contextService ?? throw new ArgumentNullException(nameof(contextService));
|
||||
this.contextQuery = contextQuery ?? throw new ArgumentNullException(nameof(contextQuery));
|
||||
}
|
||||
|
||||
public async Task<IntegrationPageResult<IntegrationFeedProjection>> ListFeedsAsync(
|
||||
@@ -110,7 +110,7 @@ public sealed class IntegrationsReadModelService
|
||||
PlatformRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var environments = await contextService.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false);
|
||||
var environments = await contextQuery.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false);
|
||||
var runs = await bundleStore.ListMaterializationRunsAsync(
|
||||
context.TenantId,
|
||||
RunScanLimit,
|
||||
|
||||
@@ -25,7 +25,17 @@ public interface IPlatformContextStore
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class PlatformContextService
|
||||
public interface IPlatformContextQuery
|
||||
{
|
||||
Task<IReadOnlyList<PlatformContextRegion>> GetRegionsAsync(
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<PlatformContextEnvironment>> GetEnvironmentsAsync(
|
||||
IReadOnlyList<string>? regionFilter,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class PlatformContextService : IPlatformContextQuery
|
||||
{
|
||||
private static readonly string[] AllowedTimeWindows = ["1h", "24h", "7d", "30d", "90d"];
|
||||
private const string DefaultTimeWindow = "24h";
|
||||
|
||||
@@ -57,16 +57,25 @@ public sealed class PostgresTranslationStore : ITranslationStore
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectAllSql, conn);
|
||||
cmd.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
cmd.Parameters.AddWithValue("locale", locale);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
result[reader.GetString(0)] = reader.GetString(1);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectAllSql, conn);
|
||||
cmd.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
cmd.Parameters.AddWithValue("locale", locale);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
result[reader.GetString(0)] = reader.GetString(1);
|
||||
}
|
||||
}
|
||||
catch (Npgsql.PostgresException ex) when (IsMissingTranslationsTable(ex))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"platform.translations table is missing; returning empty translation set for tenant {TenantId} locale {Locale}",
|
||||
tenantId,
|
||||
locale);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -78,17 +87,27 @@ public sealed class PostgresTranslationStore : ITranslationStore
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var prefix = keyPrefix.EndsWith('.') ? keyPrefix : keyPrefix + ".";
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectByPrefixSql, conn);
|
||||
cmd.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
cmd.Parameters.AddWithValue("locale", locale);
|
||||
cmd.Parameters.AddWithValue("prefix", prefix + "%");
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
result[reader.GetString(0)] = reader.GetString(1);
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectByPrefixSql, conn);
|
||||
cmd.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
cmd.Parameters.AddWithValue("locale", locale);
|
||||
cmd.Parameters.AddWithValue("prefix", prefix + "%");
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
result[reader.GetString(0)] = reader.GetString(1);
|
||||
}
|
||||
}
|
||||
catch (Npgsql.PostgresException ex) when (IsMissingTranslationsTable(ex))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"platform.translations table is missing; returning empty translation prefix set for tenant {TenantId} locale {Locale} prefix {Prefix}",
|
||||
tenantId,
|
||||
locale,
|
||||
keyPrefix);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -154,17 +173,28 @@ public sealed class PostgresTranslationStore : ITranslationStore
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var locales = new List<string>();
|
||||
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectLocalesSql, conn);
|
||||
cmd.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
locales.Add(reader.GetString(0));
|
||||
await using var conn = await _dataSource.OpenConnectionAsync(ct).ConfigureAwait(false);
|
||||
await using var cmd = new NpgsqlCommand(SelectLocalesSql, conn);
|
||||
cmd.Parameters.AddWithValue("tenant_id", tenantId);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
locales.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
catch (Npgsql.PostgresException ex) when (IsMissingTranslationsTable(ex))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"platform.translations table is missing; returning no DB locales for tenant {TenantId}",
|
||||
tenantId);
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
private static bool IsMissingTranslationsTable(Npgsql.PostgresException ex)
|
||||
=> string.Equals(ex.SqlState, "42P01", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ public sealed class SecurityReadModelService
|
||||
private static readonly string[] LicenseCatalog = ["Apache-2.0", "MIT", "BUSL-1.1", "BSD-3-Clause", "MPL-2.0"];
|
||||
|
||||
private readonly IReleaseControlBundleStore bundleStore;
|
||||
private readonly PlatformContextService contextService;
|
||||
private readonly IPlatformContextQuery contextQuery;
|
||||
|
||||
public SecurityReadModelService(
|
||||
IReleaseControlBundleStore bundleStore,
|
||||
PlatformContextService contextService)
|
||||
IPlatformContextQuery contextQuery)
|
||||
{
|
||||
this.bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
|
||||
this.contextService = contextService ?? throw new ArgumentNullException(nameof(contextService));
|
||||
this.contextQuery = contextQuery ?? throw new ArgumentNullException(nameof(contextQuery));
|
||||
}
|
||||
|
||||
public async Task<SecurityFindingsPageResult> ListFindingsAsync(
|
||||
@@ -204,7 +204,7 @@ public sealed class SecurityReadModelService
|
||||
PlatformRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var environments = await contextService.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false);
|
||||
var environments = await contextQuery.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false);
|
||||
var environmentById = environments.ToDictionary(item => item.EnvironmentId, StringComparer.Ordinal);
|
||||
|
||||
var bundles = await bundleStore.ListBundlesAsync(
|
||||
|
||||
@@ -17,14 +17,14 @@ public sealed class TopologyReadModelService
|
||||
private static readonly DateTimeOffset ProjectionEpoch = DateTimeOffset.UnixEpoch;
|
||||
|
||||
private readonly IReleaseControlBundleStore bundleStore;
|
||||
private readonly PlatformContextService contextService;
|
||||
private readonly IPlatformContextQuery contextQuery;
|
||||
|
||||
public TopologyReadModelService(
|
||||
IReleaseControlBundleStore bundleStore,
|
||||
PlatformContextService contextService)
|
||||
IPlatformContextQuery contextQuery)
|
||||
{
|
||||
this.bundleStore = bundleStore ?? throw new ArgumentNullException(nameof(bundleStore));
|
||||
this.contextService = contextService ?? throw new ArgumentNullException(nameof(contextService));
|
||||
this.contextQuery = contextQuery ?? throw new ArgumentNullException(nameof(contextQuery));
|
||||
}
|
||||
|
||||
public async Task<TopologyPageResult<TopologyRegionProjection>> ListRegionsAsync(
|
||||
@@ -248,8 +248,8 @@ public sealed class TopologyReadModelService
|
||||
PlatformRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var regions = await contextService.GetRegionsAsync(cancellationToken).ConfigureAwait(false);
|
||||
var environments = await contextService.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false);
|
||||
var regions = await contextQuery.GetRegionsAsync(cancellationToken).ConfigureAwait(false);
|
||||
var environments = await contextQuery.GetEnvironmentsAsync(null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var bundles = await bundleStore.ListBundlesAsync(
|
||||
context.TenantId,
|
||||
|
||||
@@ -46,4 +46,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| SPRINT_20260224_004-LOC-308 | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: platform locale catalog endpoint (`GET /api/v1/platform/localization/locales`) is now consumed by both UI and CLI locale-selection paths. |
|
||||
| SPRINT_20260226_230-LOC-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_230_Platform_locale_label_translation_corrections.md`: completed non-English translation correction across Platform/Web/shared localization bundles (`bg-BG`, `de-DE`, `es-ES`, `fr-FR`, `ru-RU`, `uk-UA`, `zh-CN`, `zh-TW`), including cleanup of placeholder/transliteration/malformed values (`Ezik`, leaked token markers, mojibake) and a context-quality pass for backend German resource bundles (`graph`, `policy`, `scanner`, `advisoryai`). |
|
||||
| PLATFORM-223-001 | DONE | Sprint `docs-archived/implplan/2026-03-03-completed-sprints/SPRINT_20260226_223_Platform_score_explain_contract_and_replay_alignment.md`: shipped deterministic score explain/replay contract completion (`unknowns`, `proof_ref`, deterministic replay envelope parsing/verification differences) and updated score API/module docs with contract notes. |
|
||||
| SPRINT_20260305_005-PLATFORM-BOUND-001 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: captured Platform runtime dependency inventory with explicit allowed runtime, migration-only, and prohibited coupling categories in architecture docs. |
|
||||
| SPRINT_20260305_005-PLATFORM-BOUND-003 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: introduced `IPlatformContextQuery` and switched topology/security/integrations read-model services to explicit query contracts; DI now binds read-model contract separately from context mutation service. |
|
||||
| SPRINT_20260305_005-PLATFORM-BOUND-004 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: documented runtime boundary policy, migration/runtime separation, and allowlisted exceptions in Platform dossiers. |
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using StellaOps.Platform.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
using System.Reflection;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Tests;
|
||||
|
||||
public sealed class PlatformRuntimeBoundaryGuardTests
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<Type, Type[]> ApprovedConstructorDependencies =
|
||||
new Dictionary<Type, Type[]>
|
||||
{
|
||||
[typeof(ReleaseReadModelService)] = [typeof(IReleaseControlBundleStore)],
|
||||
[typeof(TopologyReadModelService)] = [typeof(IReleaseControlBundleStore), typeof(IPlatformContextQuery)],
|
||||
[typeof(SecurityReadModelService)] = [typeof(IReleaseControlBundleStore), typeof(IPlatformContextQuery)],
|
||||
[typeof(IntegrationsReadModelService)] = [typeof(IReleaseControlBundleStore), typeof(IPlatformContextQuery)],
|
||||
};
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuntimeReadModelServices_UseOnlyApprovedConstructorContracts()
|
||||
{
|
||||
var violations = new List<string>();
|
||||
|
||||
foreach (var (serviceType, expectedDependencies) in ApprovedConstructorDependencies)
|
||||
{
|
||||
var constructors = serviceType
|
||||
.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(static ctor => !ctor.IsStatic)
|
||||
.ToArray();
|
||||
|
||||
if (constructors.Length != 1)
|
||||
{
|
||||
violations.Add($"{serviceType.Name}: expected exactly one public constructor, found {constructors.Length}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var actualDependencies = constructors[0]
|
||||
.GetParameters()
|
||||
.Select(static parameter => parameter.ParameterType)
|
||||
.ToArray();
|
||||
|
||||
if (actualDependencies.Length != expectedDependencies.Length)
|
||||
{
|
||||
violations.Add(
|
||||
$"{serviceType.Name}: expected {FormatTypeList(expectedDependencies)} but found {FormatTypeList(actualDependencies)}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < expectedDependencies.Length; i++)
|
||||
{
|
||||
if (actualDependencies[i] != expectedDependencies[i])
|
||||
{
|
||||
violations.Add(
|
||||
$"{serviceType.Name}: constructor contract mismatch at position {i + 1}; expected {expectedDependencies[i].FullName}, found {actualDependencies[i].FullName}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
violations.Count == 0,
|
||||
"Runtime read-model constructor contracts drifted from approved boundary.\n"
|
||||
+ string.Join(Environment.NewLine, violations));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void RuntimeSourceFiles_DoNotReferenceForeignPersistenceOutsideAllowlist()
|
||||
{
|
||||
var platformRoot = FindPlatformRoot();
|
||||
var scannedRoots = new[]
|
||||
{
|
||||
Path.Combine(platformRoot, "StellaOps.Platform.WebService"),
|
||||
Path.Combine(platformRoot, "__Libraries", "StellaOps.Platform.Database")
|
||||
};
|
||||
|
||||
var allowlistedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
NormalizePath(Path.Combine("StellaOps.Platform.WebService", "Endpoints", "SeedEndpoints.cs")),
|
||||
NormalizePath(Path.Combine("__Libraries", "StellaOps.Platform.Database", "MigrationModulePlugins.cs")),
|
||||
};
|
||||
|
||||
var violations = new List<string>();
|
||||
|
||||
foreach (var root in scannedRoots)
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(root, "*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativePath = NormalizePath(Path.GetRelativePath(platformRoot, file));
|
||||
|
||||
foreach (var (line, lineNumber) in File.ReadLines(file).Select((value, index) => (value, index + 1)))
|
||||
{
|
||||
if (!line.TrimStart().StartsWith("using StellaOps.", StringComparison.Ordinal)
|
||||
|| !line.Contains(".Persistence", StringComparison.Ordinal)
|
||||
|| allowlistedFiles.Contains(relativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
violations.Add($"{relativePath}:{lineNumber}: {line.Trim()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(
|
||||
violations.Count == 0,
|
||||
"Foreign persistence references are only allowed in explicit migration/seed boundaries. Violations:\n"
|
||||
+ string.Join(Environment.NewLine, violations));
|
||||
}
|
||||
|
||||
private static string FormatTypeList(IEnumerable<Type> types)
|
||||
{
|
||||
return string.Join(", ", types.Select(static type => type.FullName ?? type.Name));
|
||||
}
|
||||
|
||||
private static string FindPlatformRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
var candidate = Path.Combine(current.FullName, "src", "Platform");
|
||||
if (Directory.Exists(Path.Combine(candidate, "StellaOps.Platform.WebService"))
|
||||
&& Directory.Exists(Path.Combine(candidate, "__Libraries", "StellaOps.Platform.Database")))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate src/Platform root for runtime boundary guard tests.");
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
return path.Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
@@ -23,3 +23,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| SPRINT_20260224_004-LOC-302-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added language preference endpoint coverage in `PreferencesEndpointsTests` (round-trip persistence + invalid locale rejection) and expanded locale catalog verification in `LocalizationEndpointsTests`. |
|
||||
| SPRINT_20260224_004-LOC-305-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended `LocalizationEndpointsTests` to verify common-layer and `platform.*` namespace bundle availability for all supported locales. |
|
||||
| SPRINT_20260224_004-LOC-307-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended localization and preference endpoint tests for Ukrainian rollout (`uk-UA` locale catalog/bundle assertions and alias normalization to canonical `uk-UA`). |
|
||||
| SPRINT_20260305_005-PLATFORM-BOUND-002 | DONE | Sprint `docs-archived/implplan/2026-03-05-completed-sprints/SPRINT_20260305_005_Platform_read_model_boundary_enforcement.md`: added `PlatformRuntimeBoundaryGuardTests` to enforce approved read-model constructor contracts and disallow foreign persistence references outside explicit migration/seed allowlist files. |
|
||||
|
||||
Reference in New Issue
Block a user